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.
| Rule | Matches |
|---|---|
password | a key named password at any depth — body.password, user.profile.password |
headers.authorization | exactly that path from the root of the value |
user.*.ssn | user.profile.ssn, user.billing.ssn — * is one segment |
*.token | a 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.stackreads 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]' });