Auto-Redaction
Wide events capture comprehensive context, which makes it easy to accidentally log sensitive data. Auto-redaction scrubs PII from events before console output and before any drain sees the data.
Redaction is enabled by default in production (NODE_ENV === 'production'). In development, it is off so you see full values for debugging. No configuration needed — just deploy.
Opting Out
If you need to disable redaction in production:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
redact: false,
},
})
import { createEvlog } from 'evlog/next'
export const { withEvlog, useLogger } = createEvlog({
service: 'my-app',
redact: false,
})
import { initLogger } from 'evlog'
initLogger({
env: { service: 'my-app' },
redact: false,
})
You can also enable redaction explicitly in development with redact: true.
Smart Masking
Built-in patterns use partial masking instead of flat [REDACTED] — preserving enough context for debugging while protecting the actual data.
| Pattern | Example Input | Masked Output |
|---|---|---|
creditCard | 4111111111111111 | ****1111 |
email | alice@example.com | a***@***.com |
ipv4 | 192.168.1.100 | ***.***.***.100 |
phone | +33 6 12 34 56 78 | +33 ****5678 |
jwt | eyJhbGciOiJIUzI1NiIs... | eyJ***.*** |
bearer | Bearer sk_live_abc123... | Bearer *** |
iban | FR76 3000 6000 0112 ...189 | FR76****189 |
127.0.0.1 and 0.0.0.0 are excluded from IPv4 masking since they are not real client addresses.Configuration
Path Patterns
Use a single paths array with dot-notation and globs. A bare segment like password is shorthand for **.password — it redacts that key at any nesting depth:
evlog: {
redact: {
paths: [
'password', // same as '**.password'
'*_token', // key-name glob at any depth
'headers.x-forwarded-for', // exact path
'user.*', // everything directly under user
],
}
}
| Pattern | Matches |
|---|---|
user.email | Exact path only |
password or **.password | password key at any depth |
*_token | Key names like access_token, refresh_token |
user.* | user.email, user.password, etc. |
audit.changes.*.password | Mixed exact + wildcard segments |
Path redaction replaces the entire value (including nested objects) with replacement. Use patterns when you need regex on string values inside fields.
This matches auditDiff({ redactPaths: ['password'] }) — same glob syntax, applied globally at emit time.
Selective Built-ins
Pick only the patterns you need:
evlog: {
redact: {
builtins: ['email', 'creditCard'],
}
}
Custom Patterns
Add your own regex patterns. These use the flat replacement string, not smart masking:
evlog: {
redact: {
patterns: [/SECRET_\w+/g, /sk_live_\w+/g],
replacement: '***',
}
}
Computed Replacements
When the replacement has to be derived from the value it replaces, pass a function instead of a string. It runs at the same point as the rest of redaction — before the console write, before any drain.
The common case is keeping requests correlatable without exposing the credential that identifies them:
initLogger({
redact: {
patterns: [/\/public\/claim\/([A-Za-z0-9._-]{12,})/g],
replacement: (_match, ctx) => `/public/claim/[tok:${fingerprint(ctx.groups[0])}]`,
},
})
// /public/claim/eyJhbGciOi... → /public/claim/[tok:9f3a1c]
The function receives the matched value and a context object:
| Field | Type | Description |
|---|---|---|
path | string | Dot-notation path from the event root (user.email, items.0.token) |
key | string | Leaf key of the field (email) |
groups | string[] | Capture groups of the matching patterns entry. Only set for patterns |
For paths, the matched value is the whole field value — any type, since path redaction replaces entire subtrees. For patterns, it is the matched substring.
If the function throws or returns a non-string, redaction falls back to [REDACTED] and logs the failure. A broken policy degrades to over-redaction, never to leaking the value it was meant to scrub.
Conditional Policies
Some policies cannot be expressed as a list of paths — redact a field only for certain tenants, only when a sibling field has a given value, or keep an allowlist rather than a denylist. Use transform:
initLogger({
redact: {
transform: (event) => {
if (event.tenant === 'regulated') delete event.query
},
},
})
transform runs before paths, builtins, and patterns, so it sees raw values and the declarative rules still apply to whatever it leaves behind — a hook that misses a field is not your last line of defence. Mutate the event in place; it is already a private clone, so the object you logged is never touched.
It must be synchronous, since it runs on the emit path before the console write. Errors are caught and reported like drain failures: the declarative stages still run and the event is still logged.
replacement and transform cannot be declared in nuxt.config.ts or a Nitro module's options — that config is serialized to JSON at build time, which drops functions. Declare them at runtime with initLogger() from a server plugin, or with createEvlog(). The modules emit a build-time warning if you do it anyway.Disable Built-ins
If you only want custom redaction:
evlog: {
redact: {
builtins: false,
paths: ['user.ssn'],
patterns: [/INTERNAL_\w+/g],
}
}
Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
redact | boolean | RedactConfig | true in production | Enabled by default in production. false to disable. Object for fine-grained control |
paths | string[] | undefined | Dot-notation paths with globs (password, **.password, *_token, user.*) |
patterns | RegExp[] | undefined | Custom regex on string values. Uses flat replacement string |
builtins | false | string[] | All enabled | false disables built-ins. Array selects specific ones |
replacement | string | (matched, ctx) => string | '[REDACTED]' | Replacement for paths and custom patterns. Built-ins use smart masking instead. A function computes it from the matched value |
transform | (event) => void | undefined | Escape hatch for policies that are conditional, tenant-scoped, or allowlist-shaped. Runs before the declarative stages |
Available built-in names: creditCard, email, ipv4, phone, jwt, bearer, iban.
How It Works
Redaction runs inside the emit pipeline, after the wide event is fully built but before any output:
- Transform — your
transformhook, if any, sees the raw event first - Path redaction — exact paths and globs replaced with
[REDACTED] - Smart masking — built-in patterns scan all string values recursively with partial masking
- Pattern redaction — custom regex patterns scan all string values with flat replacement
- Console output — masked event printed to stdout
- Drain — masked event sent to external services
Redaction is the only stage that runs before the console write. enrich and drains run after it, so they cannot scrub what has already reached stdout — anything that needs to happen before output belongs in transform or a function-valued replacement.
Production Example
Redaction is already on by default in production. Combine with sampling for a typical setup:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
},
$production: {
evlog: {
sampling: {
rates: { info: 10, debug: 0 },
keep: [{ status: 400 }, { duration: 1000 }],
},
},
},
})
import { createEvlog } from 'evlog/next'
export const { withEvlog, useLogger } = createEvlog({
service: 'my-app',
sampling: {
rates: { info: 10, debug: 0 },
keep: [{ status: 400 }, { duration: 1000 }],
},
})
import { initLogger } from 'evlog'
initLogger({
env: { service: 'my-app' },
sampling: {
rates: { info: 10, debug: 0 },
keep: [{ status: 400 }, { duration: 1000 }],
},
})
Before / After
Without redaction, sensitive data lands in your logs and drains:
{
"user": { "email": "alice@example.com", "ip": "192.168.1.42" },
"payment": { "card": "4111111111111111" },
"auth": "Bearer sk_live_abc123def456"
}
With redact: true:
{
"user": { "email": "a***@***.com", "ip": "***.***.***.42" },
"payment": { "card": "****1111" },
"auth": "Bearer ***"
}
Same debugging context, no PII in your Axiom/Datadog/Sentry.
Next Steps
- Best Practices - Security guidelines and production checklist
- Sampling - Control log volume in production
- Configuration - Full configuration reference
Sampling
Control log volume with two-tier sampling. Head sampling drops noise by level, tail sampling rescues critical events based on outcome. Never miss errors, slow requests, or critical paths.
Typed Fields
Add compile-time type safety to your wide events with TypeScript module augmentation. Prevent typos and ensure consistent field names across your codebase.