Data redaction
The @logtape/redaction package is available since LogTape 0.10.0.
Sensitive data in logs can pose security and privacy risks. LogTape provides robust redaction capabilities through the @logtape/redaction package to help protect sensitive information from being exposed in your logs.
LogTape has two distinct approaches to redact sensitive data:
- Pattern-based redaction: Uses regular expressions to identify and redact sensitive data in formatted log output
- Field-based redaction: Identifies and redacts sensitive fields by their names in structured log data
Both approaches have their strengths and use cases. This guide will help you understand when and how to use each.
WARNING
No redaction system is perfect. Even with redaction in place, be cautious about what information you log. It's better to avoid logging sensitive data in the first place when possible.
Installation
LogTape provides data redaction capabilities through a separate package @logtape/redaction:
deno add jsr:@logtape/redactionnpm add @logtape/redactionpnpm add @logtape/redactionyarn add @logtape/redactionbun add @logtape/redactionPattern-based redaction
Pattern-based redaction uses regular expressions to identify and redact sensitive data patterns like credit card numbers, email addresses, and tokens in the formatted output of logs.
How it works
The redactByPattern() function wraps a formatter (either a TextFormatter or ConsoleFormatter) and scans its output for matching patterns:
import { , } from "@logtape/logtape";
import {
,
,
,
} from "@logtape/redaction";
const = (, [
,
,
]);
const = ({ });When a log is formatted, any text matching the provided patterns is replaced with a redacted value.
For console formatters that return arrays containing objects, pattern-based redaction recursively scans object and array values. To keep malformed or very large records from consuming unbounded resources, recursive traversal is capped at 20 levels and 1,000 properties or array elements per object by default. You can override these limits:
import { } from "@logtape/logtape";
import { , } from "@logtape/redaction";
const = (, [
,
], {
: 10,
: 200,
});When a limit is exceeded, LogTape emits a warning through the meta logger and truncates or omits the unprocessed portion of the formatted output.
Built-in patterns
The @logtape/redaction package includes several built-in patterns:
import {
,
,
,
,
,
} from "@logtape/redaction";EMAIL_ADDRESS_PATTERN: Redacts email addressesCREDIT_CARD_NUMBER_PATTERN: Redacts credit card numbersJWT_PATTERN: Redacts JSON Web TokensUS_SSN_PATTERN: Redacts U.S. Social Security numbersKR_RRN_PATTERN: Redacts South Korean resident registration numbers
Creating custom patterns
You can create custom patterns to match your specific needs:
import { type RedactionPattern, } from "@logtape/redaction";
import { , } from "@logtape/logtape";
const : RedactionPattern = {
: /xz([a-zA-Z0-9_-]{32})/g,
: "REDACTED_API_KEY",
};
const = (, [
,
]);
const = ({ });IMPORTANT
Regular expressions must have the global (g) flag set, otherwise a TypeError will be thrown.
Field-based redaction
Field-based redaction identifies and redacts sensitive data by field names in structured log data. It works by examining the property names in the log record and redacting those that match specified patterns.
How it works
The redactByField() function wraps a sink and redacts properties in the log record before passing it to the sink:
import { } from "@logtape/logtape";
import { } from "@logtape/redaction";
const = (()); By default, it uses DEFAULT_REDACT_FIELDS, which includes common sensitive field patterns like password, secret, token, etc.
Customizing field patterns
You can provide your own field patterns:
import { } from "@logtape/logtape";
import { , } from "@logtape/redaction";
const = ((), [
/pass(?:code|phrase|word)/i,
/api[-_]?key/i,
"secret",
...
]);Field patterns can be strings (exact matches) or regular expressions.
Customizing redaction behavior
By default, field-based redaction removes matching fields. You can customize this behavior to replace them instead:
import { } from "@logtape/logtape";
import { } from "@logtape/redaction";
const = ((), {
: [/password/i, /secret/i],
: () => "[REDACTED]" // Replace with "[REDACTED]" instead of removing
});Field redaction is recursive and will redact sensitive fields in nested objects and arrays as well. To keep malformed or very large records from consuming unbounded resources, recursive traversal is capped at 20 levels and 1,000 properties or array elements per object by default. You can override these limits with maxDepth and maxProperties:
import { } from "@logtape/logtape";
import { } from "@logtape/redaction";
const = ((), {
: [/password/i, /secret/i],
: 10,
: 200,
});When a limit is exceeded, LogTape emits a warning through the meta logger and truncates or omits the unprocessed portion of the properties. For tagged template messages, interpolated values that cannot be matched safely after truncation are replaced with "[truncated]".
Pseudonymizing fields for correlation
This API is available since LogTape 2.1.0.
Sometimes you may want to hide a sensitive identifier while still keeping a stable value for correlation. For example, the same user ID or email address can be replaced with the same pseudonym across log records, without writing the original value to the log.
Use createHmacPseudonymizer() with redactByFieldAsync() for this:
import { , , } from "@logtape/logtape";
import {
,
,
} from "@logtape/redaction";
const = await ({
: "replace-with-a-secret-key",
});
const = ((), {
: [/userId/i, /email/i],
: ,
});
await ({
: {
: ,
},
: [
{ : "my-app", : ["console"] },
],
});
// Later, before shutdown:
await ();createHmacPseudonymizer() uses keyed HMAC through the Web Crypto API. This is safer than a plain salted hash for values with small input spaces, such as email addresses or numeric user IDs. By default, it returns values prefixed with hmac-sha256: and encoded as base64url.
Because the Web Crypto API is asynchronous, redactByFieldAsync() returns a sink with asynchronous disposal semantics. Use it with configure() and call dispose() during shutdown so pending redaction work can finish.
Comparing redaction approaches
Each redaction approach has its strengths and weaknesses depending on your specific use case.
Pattern-based redaction
Pros:
- More accurate at detecting structured patterns (credit cards, SSNs, etc.)
- Works with any formatter regardless of data structure
- Can redact data within message strings
- Catches sensitive data even if it appears in unexpected places
Cons:
- Performance impact can be higher, especially with many patterns
- Regex matching is applied to all output text
- May produce false positives (redacting text that resembles sensitive data)
- Operates after formatting, so sensitive data might exist in memory temporarily
Field-based redaction
Pros:
- More efficient as it only checks field names, not values
- Redacts data before it reaches the sink or formatter
- Less likely to cause false positives
- Works with any sink, regardless of formatter
Cons:
- Cannot detect sensitive data in free-form text or message templates
- Only works for structured data fields
- Requires knowledge of field names that contain sensitive data
- May miss sensitive data with unexpected field names
Usage examples
Basic pattern-based redaction
import {
,
,
,
} from "@logtape/logtape";
import {
,
,
,
} from "@logtape/redaction";
const = ({
: (
// Wrap the default formatter with pattern-based redaction
,
[, ]
),
});
await ({
: {
: ,
},
: [
{ : "my-app", : ["console"] },
],
});
// Later in your code:
import { } from "@logtape/logtape";
const = ("my-app");
.(
"User email: user@example.com, token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
);
// Output will show: "User email: REDACTED@EMAIL.ADDRESS, token: [JWT REDACTED]"Basic field-based redaction
import { , } from "@logtape/logtape";
import { } from "@logtape/redaction";
await ({
: {
: (()),
},
: [
{ : "my-app", : ["console"] },
],
});
// Later in your code:
import { } from "@logtape/logtape";
const = ("my-app");
.("User authenticated", {
: "johndoe",
: "supersecret", // This field will be removed from the logged output
: "johndoe@example.com", // This field will be removed too
});
.("Loaded configs", {
: [
{ : "secret", : "user1" }, // Sensitive fields in arrays will also be removed
{ : "abc", : "user2" },
],
});Combining both approaches
For maximum security, you can combine both approaches:
import {
,
,
,
} from "@logtape/logtape";
import {
,
,
,
,
} from "@logtape/redaction";
// First apply field-based redaction to the sink
const = (
({
// Then apply pattern-based redaction to the formatter
: (
,
[, ]
)
})
);
await ({
: {
: ,
},
: [
{ : "my-app", : ["console"] },
],
});File sink with redaction
import { } from "@logtape/file";
import { , } from "@logtape/logtape";
import {
,
,
,
} from "@logtape/redaction";
const = ("app.log", {
: (
(),
[, ]
),
});
await ({
: {
: ,
},
: [
{ : "my-app", : ["file"] },
],
});Best practices
Choose the right approach:
- Use pattern-based redaction when you need to catch sensitive data in message strings and have well-defined patterns
- Use field-based redaction for structured data with known field names
- Combine both approaches for maximum security
Be comprehensive:
- Define patterns for all types of sensitive data your application handles
- Regularly review and update your redaction patterns as new types of sensitive data are introduced
Test your redaction:
- Verify that sensitive data is properly redacted by examining your logs
- Include edge cases in your testing (partial matches, data spanning multiple lines, etc.)
Balance performance and security:
- For high-volume logs, consider using field-based redaction which is generally more efficient
- For security-critical applications, use both approaches even if it means some performance overhead
Document your approach:
- Make sure your team understands which data is being redacted and how
- Include redaction strategies in your security documentation