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.
modules/zuplo.runtime.ts
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
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. CallingaddTelemetryRedactorafter 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.
modules/zuplo.runtime.ts
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.
| Option | What it redacts |
|---|---|
entities | Matches 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. |
customPatterns | Matches of your own regular expressions. Each entry is { name, pattern, confidence?, context? }. |
minConfidence | The confidence, from 0 to 1, a match from entities or customPatterns needs to be redacted. Defaults to 0.5. |
allowValues | Matched text that's never redacted, compared exactly, for example a test card number. |
queryParameters | The 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. |
fieldNames | The whole value of any property whose name is in the list, for example ["password", "dob"]. Names match case-insensitively. |
replacement | The 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.
modules/zuplo.runtime.ts
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.
Code
| Property | Description |
|---|---|
kind | The kind of record the value is written to. See What's redacted. |
field | The path of the value in the record, for example message[0], message[1].user.email, properties.customerId, attributes.url.full, or url. |
key | The 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:
value | kind | field | key |
|---|---|---|---|
"charging" | log | message[0] | undefined |
"4111 1111 1111 1111" | log | message[1].card | card |
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:
modules/zuplo.runtime.ts
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
MaporSetis 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
The warning never contains the value or the redactor's error message.
Limits
| Limit | Value |
|---|---|
| Redactors | 16, registered only in runtimeInit |
| Value length | 32,768 characters. Longer values, and longer redactor output, are cut to that length and marked [Truncated]. |
| Values per record | 10,000, shared by every field of the record. Values past the limit are recorded as [Truncated]. |
| Nesting depth | 20 levels. Deeper values are recorded as [Truncated]. |
| Values that repeat | A 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.
kind | Record | Values passed to the redactor |
|---|---|---|
log | Logs from Zuplo and from your code | Every message argument, log properties, custom log fields, and the request URL given to log transports |
span | OpenTelemetry spans | The span name, every attribute, every event name and event attribute, every link attribute, and the status message |
analytics | Analytics request records and events | The request URL (path and query string), properties your code sets with setLogProperties, and the resource URI on analytics events |
request-log | Request logger plugins: Azure Blob, Azure Event Hubs, and Hydrolix | Every value of each entry |
audit-log | Events from the Audit Logs policy | Every 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:
| Record | Values recorded as measured |
|---|---|
| Analytics request record | Route pattern, method, status code, sizes, timings, user, client IP, geographic fields, ray ID, request IDs, and properties Zuplo sets |
| Analytics events | Every field except the resource URI, including the model, provider, app, team, and consumer that AI Gateway usage is metered by |
| Audit events | specversion, 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
runtimeInitruns. - 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.