Learn

Auto-Redaction

Automatically scrub PII from wide events before console output and drains. Built-in smart masking for credit cards, emails, IPs, phone numbers, JWTs, and more.

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,
  },
})

You can also enable redaction explicitly in development with redact: true.

wide event·raw
user.email"alice@example.com" ...
payment.card"4111111111111111" ...
user.ip"192.168.1.42" ...
auth"Bearer sk_live_abc123def" ...
metadata.password"hunter2-correct-horse" ...
user.phone"+33 6 12 34 56 78" ...
user.id42 ...
cart.total9999 ...
smart mask path redact untouched ready for drain · 0 PII leaked

Smart Masking

Built-in patterns use partial masking instead of flat [REDACTED] — preserving enough context for debugging while protecting the actual data.

PatternExample InputMasked Output
creditCard4111111111111111****1111
emailalice@example.coma***@***.com
ipv4192.168.1.100***.***.***.100
phone+33 6 12 34 56 78+33 ****5678
jwteyJhbGciOiJIUzI1NiIs...eyJ***.***
bearerBearer sk_live_abc123...Bearer ***
ibanFR76 3000 6000 0112 ...189FR76****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
    ],
  }
}
PatternMatches
user.emailExact path only
password or **.passwordpassword key at any depth
*_tokenKey names like access_token, refresh_token
user.*user.email, user.password, etc.
audit.changes.*.passwordMixed 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:

FieldTypeDescription
pathstringDot-notation path from the event root (user.email, items.0.token)
keystringLeaf key of the field (email)
groupsstring[]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.

Function-valued 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

OptionTypeDefaultDescription
redactboolean | RedactConfigtrue in productionEnabled by default in production. false to disable. Object for fine-grained control
pathsstring[]undefinedDot-notation paths with globs (password, **.password, *_token, user.*)
patternsRegExp[]undefinedCustom regex on string values. Uses flat replacement string
builtinsfalse | string[]All enabledfalse disables built-ins. Array selects specific ones
replacementstring | (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) => voidundefinedEscape 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:

  1. Transform — your transform hook, if any, sees the raw event first
  2. Path redaction — exact paths and globs replaced with [REDACTED]
  3. Smart masking — built-in patterns scan all string values recursively with partial masking
  4. Pattern redaction — custom regex patterns scan all string values with flat replacement
  5. Console output — masked event printed to stdout
  6. 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.

Redaction runs after the HTTP response is sent, so it adds zero latency to your API responses.

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 }],
      },
    },
  },
})

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