Test That Logs Redact Sensitive Fields
Add an automated test that captures structured log output and fails if emails, tokens, or card numbers appear in plain text.
Task
Keep PII and credentials out of logs with a regression test that runs on every pull request.
When to use this
A logger upgrade changes how nested error objects are serialized. The redaction path still matches the top-level field, but the email inside the error context is now printed in full, and the leak is discovered only in a support bundle.
Prerequisites
- A structured logger such as pino or winston
- A test runner such as node:test or Vitest
- Redaction paths defined for known sensitive fields
Steps
1. Configure redaction centrally so every logger instance shares the same paths.
import pino from 'pino';
export const logger = pino({
redact: {
paths: [
'req.headers.authorization',
'req.headers.cookie',
'user.email',
'user.password',
'payment.card.number',
'err.context.email',
],
censor: '[REDACTED]',
},
});
Expected output: The logger censors known sensitive paths, including nested fields inside error context.
2. Capture serialized output in a test and assert that raw values never appear.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import pino from 'pino';
test('logs never contain raw email or token', () => {
const lines = [];
const stream = { write: (line) => lines.push(line) };
const log = pino({
redact: {
paths: ['user.email', 'token'],
censor: '[REDACTED]',
},
}, stream);
log.info({ user: { email: '[email protected]' }, token: 'sk_live_abc123' }, 'login');
const output = lines.join('');
assert.equal(output.includes('[email protected]'), false);
assert.equal(output.includes('sk_live_abc123'), false);
});
Expected output: The test passes only when both values are censored in the serialized JSON line.
3. Add a CI grep gate over captured fixtures and error samples for common PII patterns.
grep -nE "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}" tests/fixtures/*.log && exit 1 || echo "no email patterns found"Expected output: The gate fails when an email appears in fixtures and prints no email patterns found otherwise.
4. Prove the test catches a regression by removing one redaction path, running the test, and restoring it.
node --test tests/logging/redaction.test.mjsExpected output: With the path removed the assertion fails with the leaked value; with the path restored it passes.
Troubleshooting
Problem
Nested fields are not redacted
Solution
Use the correct path syntax for your logger, such as *.email for any immediate parent, and test the actual serialized output rather than the object.
Problem
Redaction misses query strings in URLs
Solution
Redact req.url or strip known parameters in a serializer; tokens commonly leak through query strings that never appear as named fields.
Limitations
- Redaction only covers configured field names; sensitive data in free-text messages is not detected.
- Pattern-based tests catch shapes, not semantics; a national id without a known format can still slip through.
Logs are a data store
Logs are copied to disks, shipped to aggregators, indexed, and retained. A leaked email or token in a log line bypasses every database access control you built. Redaction must be automated and tested, because the failure mode is silent: the application works and only the log content is wrong.
Test the serialized line
Assert on the output string, not on the logger’s internal options. Serialization is where regressions happen: a new error serializer, a changed nested object shape, or a framework that logs the whole request. Include at least one nested object and one header-based credential in the test. Keep the test close to the logger configuration so an edit to redaction paths requires a matching test update.
const output = lines.join('');
assert.equal(output.includes('[email protected]'), false);
assert.equal(output.includes('sk_live_abc123'), false);
Add pattern gates
Field-based redaction cannot catch everything. A grep gate over fixtures and sample logs for emails, card-like digit sequences, and token prefixes provides a second layer. Run it on captured outputs from integration tests, not on source code. Treat a match as a failure until the fixture is sanitized or the redaction path is added.
Watch the edge cases
Error objects often carry request context that is serialized differently from normal logs. Loggers with custom serializers can bypass redaction entirely. Test error paths explicitly. Also check that crash reporters and access logs use the same policy; an unredacted access log can undo a fully redacted application log. Verify the gate by committing a fixture with a fake address, confirming CI fails, then replacing it with a redacted sample and confirming the pipeline passes.