Guides

Redaction

Keys, dotted paths and wildcards; policies for whole types; what is not covered.

Secrets reach logs through objects: a request body with a password, headers with an authorization, a user record with an ssn. logit redacts by key, at capture time — before the value exists on the event, so no sink, formatter or enricher ever sees it.

configure({
  destructuring: {
    redact: ['password', 'authorization', 'cookie', 'user.*.ssn', '*.token'],
    redactedValue: '[Redacted]',   // the default
  },
});

log.info('Login {@Body}', { email: 'ada@x.y', password: 'hunter2' });
// … "Body":{"email":"ada@x.y","password":"[Redacted]"}

With the fluent builder: .destructure.redact('password', 'authorization').

The rules#

Matching is case-insensitive on the key path from the root of the captured value.

RuleMatches
passworda key named password at any depth — body.password, user.profile.password
headers.authorizationexactly that path from the root of the value
user.*.ssnuser.profile.ssn, user.billing.ssn — * is one segment
*.tokena token one level down — auth.token, not token or a.b.token

The root is the hole or property: for log.info('{@Body}', body) the path of body.password is password; for a forContext({ user }) it is ssn under user. Keys inside Maps with string keys and inside errors' own properties are covered too. Array indices are not path segments — users.*.ssn matches users[0].ssn, since arrays pass their parent path through.

Whole types#

For a class that must never be logged in full, a transforming policy picks the fields:

import { byTransforming } from '@unhingged/logit';

configure({
  destructuring: {
    policies: [
      byTransforming(User, (u) => ({ id: u.id, name: u.name })),
      byTransforming(Credentials, () => '[Credentials]'),
    ],
  },
});

Policies run before the default rules and before redaction, so what they return is redacted too.

What is not covered#

  • Patterns inside strings. An email address or a card number embedded in a message or a string value is not masked; that is Serilog.Enrichers.Sensitive's job and is on the roadmap. Until then, do not put raw user input into templates as text — capture it as a property under a redacted key, or transform it.
  • Values a sink adds. Redaction is a capture step; a sink that reads event.error.stack reads the raw error. The built-in formatters only ever render what was captured.
  • The error's message. It is captured as written — a library that puts a secret in an error message has put it in your logs. Wrap such errors.

Checking it#

import { createLogger, memorySink } from '@unhingged/logit';

const mem = memorySink();
const log = createLogger({ sinks: [mem], destructuring: { redact: ['password'] } });
log.info('{@Body}', { password: 'x' });
expect(mem.events[0].properties.Body).toEqual({ password: '[Redacted]' });