Logging & Observability

Redact Sensitive Data from Telemetry

Applies to
Gatewayruntime for API, AI, and MCP gateways
Hosting
Managed EdgeManaged DedicatedSelf-Hosted

Requests that pass through your gateway can carry sensitive data such as card numbers, government IDs, and health data. Zuplo records values from each request in logs, traces, analytics, and audit events. A telemetry redactor removes sensitive data from those values before Zuplo records them.

Register a redactor once in zuplo.runtime.ts, and every log entry, span, analytics record, request logger entry, and audit event passes through it. Redaction applies only to what Zuplo records. The request, the response, and the values your code reads stay unchanged.

TypeScriptmodules/zuplo.runtime.ts
import { RuntimeExtensions, createRedactor } from "@zuplo/runtime"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addTelemetryRedactor( createRedactor({ entities: ["pii", "secret"], queryParameters: ["iban"], fieldNames: ["dob"], }), ); }

With this redactor, a request to /accounts?iban=DE89370400440532013000&page=2 whose handler calls context.log.info("charging", { card: "4111 1111 1111 1111", dob: "1990-01-01" }) is recorded like this:

Code
- "url": "https://api.example.com/accounts?iban=DE89370400440532013000&page=2" + "url": "https://api.example.com/accounts?iban=%5BREDACTED%5D&page=2" - "messages": ["charging", { "card": "4111 1111 1111 1111", "dob": "1990-01-01" }] + "messages": ["charging", { "card": "[REDACTED]", "dob": "[REDACTED]" }]

The response to the client is the same with or without the redactor.

Register a redactor

Call runtime.addTelemetryRedactor inside runtimeInit in zuplo.runtime.ts. A redactor is either the result of createRedactor or your own function.

  • Register redactors only in runtimeInit. Calling addTelemetryRedactor after the gateway starts, for example inside a request hook, throws an error and doesn't add the redactor.
  • You can register up to 16 redactors. Registering a 17th throws an error.
  • Passing anything other than a function throws an error.

An error thrown in runtimeInit stops the gateway from starting, so a misconfigured redactor never lets data through unredacted.

Combine several redactors

Redactors run in the order you register them. Each redactor receives the output of the previous one for the same value.

TypeScriptmodules/zuplo.runtime.ts
import { RuntimeExtensions, createRedactor } from "@zuplo/runtime"; export function runtimeInit(runtime: RuntimeExtensions) { // Runs first: built-in patterns runtime.addTelemetryRedactor( createRedactor({ entities: ["finance-credit-card", "id-us-ssn"] }), ); // Runs second: receives the output of the first redactor runtime.addTelemetryRedactor((value, { key }) => key === "accountNumber" ? "[REDACTED]" : value, ); }

Use the built-in redactor

createRedactor(options) returns a redactor that applies only the options you set. It detects sensitive values with the same built-in data types and groups as the Data Loss Prevention policy, so entities: ["pii", "secret"] selects the same data types in both places.

OptionWhat it redacts
entitiesMatches of built-in data types by ID (finance-credit-card, id-us-ssn), group (pii, secret, region-eu), or ID prefix (id-us, secret-aws). When omitted, no built-in data type runs.
customPatternsMatches of your own regular expressions. Each entry is { name, pattern, confidence?, context? }.
minConfidenceThe confidence, from 0 to 1, a match from entities or customPatterns needs to be redacted. Defaults to 0.5.
allowValuesMatched text that's never redacted, compared exactly, for example a test card number.
queryParametersThe value of each named query parameter wherever a query string appears: request URLs, span attributes such as url.full, and URLs inside log messages. Names match case-insensitively, after URL decoding.
fieldNamesThe whole value of any property whose name is in the list, for example ["password", "dob"]. Names match case-insensitively.
replacementThe text recorded in place of a match. It's used literally. Defaults to [REDACTED]. In query parameter values, it's URL-encoded, so [REDACTED] is recorded as %5BREDACTED%5D and the URL stays valid.

createRedactor throws an error in runtimeInit for an unknown entity, a custom pattern that isn't a valid regular expression, or a minConfidence outside 0 to 1. A typo stops the gateway from starting instead of silently turning redaction off.

fieldNames redacts a value only when it sits directly under the matching property name. In { dob: "1990-01-01" } the value is redacted, but in { dob: ["1990-01-01"] } or { card: { number: "..." } } the nested values are checked only by the other options. Property names themselves are recorded as written.

Custom patterns

A custom pattern has a name, a regular expression pattern, an optional base confidence (defaults to 0.85), and optional context words. Context words that appear near a match in the same value raise its confidence. Use a low confidence with context for values that are sensitive only in context.

TypeScriptmodules/zuplo.runtime.ts
import { RuntimeExtensions, createRedactor } from "@zuplo/runtime"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addTelemetryRedactor( createRedactor({ entities: ["pii"], customPatterns: [ // Always redacted: confidence 0.85 is above the 0.5 default { name: "member-id", pattern: "MBR-\\d{8}" }, // Redacted only when "patient" or "mrn" appears nearby { name: "medical-record-number", pattern: "\\b\\d{7}\\b", confidence: 0.3, context: ["patient", "mrn"], }, ], allowValues: ["4111 1111 1111 1111"], }), ); }

Pattern matching can't find every form sensitive data takes. Add a custom redactor for data that's specific to your API.

Write a custom redactor

A redactor is a synchronous function that takes a string and a context and returns the string to record: the input unchanged, or a redacted copy.

TypeScriptCode
interface TelemetryRedactor { (value: string, context: RedactionContext): string; } interface RedactionContext { readonly kind: "log" | "span" | "analytics" | "request-log" | "audit-log"; readonly field: string; readonly key: string | undefined; }
PropertyDescription
kindThe kind of record the value is written to. See What's redacted.
fieldThe path of the value in the record, for example message[0], message[1].user.email, properties.customerId, attributes.url.full, or url.
keyThe last property name in field, for example email or url.full. undefined when the value isn't an object property, such as a log message string or array element.

For example, context.log.info("charging", { card: "4111 1111 1111 1111" }) calls the redactor twice:

valuekindfieldkey
"charging"logmessage[0]undefined
"4111 1111 1111 1111"logmessage[1].cardcard

The url.full attribute on a span is passed with kind: "span", field: "attributes.url.full", and key: "url.full".

Use the context to apply a rule to one kind of record or one property:

TypeScriptmodules/zuplo.runtime.ts
import { RuntimeExtensions } from "@zuplo/runtime"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addTelemetryRedactor((value, { kind, key }) => { if (key === "ssn") { return "[REDACTED]"; } if (kind === "audit-log" && key === "email") { return value.replace(/^[^@]+/, "***"); } return value; }); }

How values are passed

  • Objects, arrays, and errors are walked the way the log serializers write them, and each string inside is passed to the redactor separately. For an error, that includes its message, stack, cause, and custom properties. A Map or Set is recorded the way the serializers print it, usually {}.
  • Numbers are passed as their decimal string. If the redactor returns the same string, the record keeps the number. If it returns a different string, the record holds that string. Dates work the same way, using their serialized string.
  • A span attribute array must hold a single type. If the redactor changes any number in such an array, every element is recorded as text.
  • When the redacted request URL given to log transports is no longer a valid URL, transports receive the original origin followed by /[REDACTED].
  • In audit events, the IP address is left out of the event when the redactor changes it.

Keep redactors pure and fast

A redactor runs on the request path for every value Zuplo records, so keep it fast. It must also be pure: the same value and context must always return the same output. Zuplo caches results for values of 256 characters or fewer and can reuse an earlier result instead of calling the redactor again.

Anything a redactor logs is dropped, so a redactor can't write sensitive values into the logs it's meant to protect.

Failures

Redaction fails closed. When a redactor throws an error, returns anything other than a string, or is async (returns a promise), Zuplo records the value as [REDACTION_FAILED]. Later redactors don't run for that value, and the original value is never recorded. The REDACTION_FAILED_PLACEHOLDER constant exported from @zuplo/runtime holds this text.

Each failure writes a warning to your gateway's logs, at most once a minute for each kind of record:

Code
A redactor registered with runtime.addTelemetryRedactor threw or did not return a string while redacting a log record. The value was recorded as [REDACTION_FAILED]. Further failures are reported at most once a minute.

The warning never contains the value or the redactor's error message.

Limits

LimitValue
Redactors16, registered only in runtimeInit
Value length32,768 characters. Longer values, and longer redactor output, are cut to that length and marked [Truncated].
Values per record10,000, shared by every field of the record. Values past the limit are recorded as [Truncated].
Nesting depth20 levels. Deeper values are recorded as [Truncated].
Values that repeatA value that contains itself is recorded as [Circular] where it repeats.

What's redacted

When a redactor is registered, these values pass through it before any log transport, tracing exporter, plugin, or Zuplo service receives the record. Every destination receives the same redacted record. With no redactor registered, records are unchanged.

kindRecordValues passed to the redactor
logLogs from Zuplo and from your codeEvery message argument, log properties, custom log fields, and the request URL given to log transports
spanOpenTelemetry spansThe span name, every attribute, every event name and event attribute, every link attribute, and the status message
analyticsAnalytics request records and eventsThe request URL (path and query string), properties your code sets with setLogProperties, and the resource URI on analytics events
request-logRequest logger plugins: Azure Blob, Azure Event Hubs, and HydrolixEvery value of each entry
audit-logEvents from the Audit Logs policyEvery field except the fields listed in the next section

Values that aren't redacted

Zuplo computes usage, analytics, and billing from some recorded values, and generates others itself. These values are always recorded as measured, even when a redactor is registered:

RecordValues recorded as measured
Analytics request recordRoute pattern, method, status code, sizes, timings, user, client IP, geographic fields, ray ID, request IDs, and properties Zuplo sets
Analytics eventsEvery field except the resource URI, including the model, provider, app, team, and consumer that AI Gateway usage is metered by
Audit eventsspecversion, id, time, datacontenttype, bucketid, actortype, requestid, success, httpmethod, and httpstatus

Keep sensitive data out of route paths and user identifiers, because a redactor can't remove it from these fields.

Data that redactors don't cover

Redactors apply only to records the gateway writes through the paths listed earlier. They don't cover:

  • Data that metering and analytics policies send to their services, such as Moesif, ReadMe, Amberflo, and OpenMeter. Those services receive the raw values.
  • Request and response bodies sent to Akamai API Security.
  • Output written directly to the console instead of through context.log, and uncaught errors captured by the hosting platform.
  • Logs written before runtimeInit runs.
  • OpenTelemetry resource attributes and other static configuration.
  • Data the gateway stores to serve requests, such as cache entries and rate limit keys.
  • Records after they leave the gateway.
Last modified on