Skip to content

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:

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/redaction
npm add @logtape/redaction
pnpm add @logtape/redaction
yarn add @logtape/redaction
bun add @logtape/redaction

Pattern-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";

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

  1. 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
  2. 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
  3. 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.)
  4. 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
  5. Document your approach:

    • Make sure your team understands which data is being redacted and how
    • Include redaction strategies in your security documentation

Released under the MIT License.