Skip to navigation

PII Redaction Behavior

View as Markdown

Use this page to understand which values a policy selects and what Relay publishes after sanitization. For configuration keys and complete before/after examples, refer to Configuration. For runtime placement and backend availability, refer to About.

Observability Semantics

Payload boundaries and event publication

The plugin uses different payload boundaries for tools and LLMs:

  • Tools use JSON-native payloads. Paths point into the emitted tool args directly. For built-in action policies, including detector-backed policies, tool response paths apply independently to the emitted ToolExecutionResult.result and ToolExecutionResult.annotation JSON documents. A path is relative to each document; it does not start with /result or /annotation.
  • LLM requests use the active resolved codec for that managed call. LLM responses use the active built-in codec for normalized projection. Prefer normalized Relay paths such as:
    • /headers/authorization for an exact request header field
    • /messages/0/content for request message content
    • /message for the normalized assistant response text
  • Marks and non-tool, non-LLM scopes sanitize data, category_profile, and metadata as separate JSON values. A target path is evaluated independently inside each value; it does not start with /data, /category_profile, or /metadata.

When using LLM selectors, supply the matching codec on the call, or configure a codec fallback for calls without an active codec. If a required codec is unavailable or Relay cannot safely sanitize and convert the payload, it omits the LLM payload and its annotation from the recorded event.

The PII policy changes observability data only. It leaves inputs sent to providers and tools, and results returned to your application, unchanged. Subscribers and exporters receive sanitized events; values removed by the policy are unavailable to them.

Metadata is sanitized separately from request and result payloads. In the single-policy configuration, use the surface flags to choose which event surfaces are sanitized.

Action Semantics

How each action changes a selected value

remove

remove is structural.

Configure at least one target_paths or target_path_globs selector when using remove. Relay rejects an unscoped remove policy during validation to prevent accidental removal of every string value in the selected surfaces.

When a target matches:

  • Object fields are removed.
  • Array elements become null on JSON-native surfaces. If a normalized LLM array removal cannot be safely re-encoded, Relay omits the event payload and its annotation instead.
  • Targeted scalar or root values become null.

regex_replace

regex_replace applies the configured regex to matching string leaves and replaces matches with the configured replacement.

If you set detector instead of pattern, the built-in backend uses the detector’s stock matcher regex.

redact

redact is the deterministic whole-match replacement lane.

It uses the same pattern or detector matcher flow as regex_replace, but defaults the replacement token to [REDACTED] and is intended for cases where you do not want to preserve any matched secret characters.

Use redact when you want:

  • credential-style secrets fully replaced
  • a consistent redaction token across detectors
  • clearer policy intent than a custom regex_replace

hash (SHA-256)

hash uses SHA-256 on the UTF-8 bytes of the selected text, without a salt. It returns the digest as 64 lowercase hexadecimal characters.

Without pattern or detector, the entire selected string is hashed. With either matcher, each matching substring gets its own SHA-256 digest; unmatched text is preserved.

mask

mask replaces the middle portion of each matching string leaf with the configured mask_char.

Use unmasked_prefix and unmasked_suffix when you want to preserve a small leading or trailing segment for correlation or debugging, such as the last four characters of a token.

When pattern or detector is set, mask only masks matching substrings inside the string leaf.

When detector is set and you do not specify unmasked_prefix or unmasked_suffix, the built-in backend applies detector-aware defaults:

  • email: Preserves the domain and the first local-part character
  • phone: Preserves the last four digits while keeping separators intact
  • api_key: Preserves the vendor-style prefix such as sk- and the last four characters
  • ip_address: Preserves the last octet
  • ipv6: Preserves the last segment
  • url: Preserves the scheme and host, then collapses the path/query tail
  • uuid: Preserves the last four characters
  • bearer_token: Preserves the auth scheme and the last four characters
  • jwt: Preserves the header segment and the tail of the signature
  • credit_card: Preserves the last four digits while keeping separators intact
  • aws_access_key_id: Preserves the provider prefix and the last four characters
  • aws_secret_access_key: Preserves the last four characters
  • gcp_api_key: Preserves the AIza-style prefix and the last four characters
  • azure_storage_account_key: Preserves the last four characters

Detector Matching

Built-in detectors are deterministic regex-backed matchers. Refer to Detector Presets for the supported detector names.

bearer_token is heuristic rather than vendor-specific. It can still match benign bearer-style values, so prefer a narrower detector when you know the credential family.

For actions other than remove, if both target-selector lists are empty, the built-in backend sanitizes every matching string leaf in the selected payload boundary. The remove action requires at least one selector.

Path Semantics

Exact pointers, globs, and container selection

target_paths are exact RFC 6901 JSON-pointer matches. target_path_globs adds an opt-in selector language: a whole * segment matches exactly one object key or array index, and a whole ** segment matches zero or more path segments. Exact paths and glob paths are combined; a node selected by multiple selectors is counted and sanitized once per document.

Wildcards are special only when they occupy the complete pointer segment. Segments such as content*, content**, and foo*bar match those literal object keys. Predicates and arbitrary JSONPath expressions are not supported.

For example, these selectors cover every message and provider-output text block without capping the number of array items:

target_path_globs = [
"/messages/*/content/*/text",
"/output/*/content/*/text",
]

For example, /messages/*/content matches both /messages/A/content and /messages/B/content. If either content value is an array or object, redact does not redact its descendant strings: it acts only on selected string values. For content parts such as {"messages":[{"content":[{"text":"alice@example.com"}]}]}, use /messages/*/content/*/text to select the text string. The same glob also matches a text key in an object one level below content.

A single * does not cross path segments: /output/* selects only direct children of output, not /output/0/content/0/text. Use /output/*/content/*/text for that specific shape, or /output/**/text when the nesting depth can vary.

Relay validates every configured path as RFC 6901 syntax before activating the component. The empty string selects the document root. Every other pointer must start with /, and ~ escapes must use ~0 for ~ or ~1 for /. Invalid pointers prevent activation instead of silently leaving the intended values unsanitized.

Recursive globs

Use ** when the same field can appear at different depths within a payload. It matches zero or more complete segments, including object keys and array indices. The entire path must still match the selector:

SelectorExample Matches
/messages/*/text/messages/0/text.
/messages/**/text/messages/text, /messages/0/content/0/text.
/**/email/email, /customer/contact/email.
/messages/**/messages and every descendant.
/messages/*/**Every descendant of /messages; excludes /messages itself.

/messages/**/text does not select /other/text or /messages/0/text/child: the path must start with messages and end with text.

Use the narrowest subtree and field name that meet your policy. For example, /messages/**/text selects text values at varying depths without selecting fields outside messages. It does not select a plain-string content field; use /messages/**/content for that field. LLM selectors still refer to the active codec’s representation, as described in Payload Boundaries.

** can match zero segments. With action = "remove", /messages/** removes the messages field itself, including the container and all its contents. Use /messages/*/** to keep that container while removing its children. On ordinary JSON payloads, removed array elements become null.

/** selects the document root and every descendant. With remove, an ordinary JSON payload becomes null; with a string action, every string leaf is selected. For broad trajectory removal that preserves supported analytics, use the trajectory_context preset.

Refer to the Recursive content example in Targeting and Composition for a complete configuration and before/after payloads.

Literal wildcard keys and migration

target_paths always treats * and ** as literal object keys. Use target_paths = ["/metadata/**"] to select the field named ** under metadata; use target_path_globs = ["/metadata/**"] to select metadata and every descendant.

Earlier versions treated a whole ** segment in target_path_globs as a literal key. This behavior has changed. Before upgrading, move selectors intended to match literal ** keys to target_paths. If a selector combines wildcard segments with a literal ** key, enumerate the required concrete pointers in target_paths; glob selectors have no escape for a literal ** segment. The RFC 6901 escapes ~0 and ~1 still represent ~ and /.

Trajectory Context Preset

What the trajectory preset removes and preserves

trajectory_context is a fail-closed policy for metrics-oriented trajectory export. It discards original LLM request and response payloads and generates minimal provider-shaped projections from sanitized normalized annotations. For supported built-in codecs, the published LLM request or response retains supported structure and analytics, including message roles, tool names, and tool-call correlation IDs. Conversational text is replaced with [REDACTED] by default. Tool arguments and parameter schemas are emptied. Refer to the Trajectory removal worked example for the request, response, and separate tool events before and after sanitization.

A configured legacy codec can also provide this projection when no active codec is present. Custom runtime and opaque codec identities publish {} instead, even when their payload resembles a supported provider’s JSON shape.

Unknown fields and unstructured payloads are discarded or emptied. This includes provider extensions, additional choices or candidates, headers, provider-native values, generic event data, and category-profile extras. Present opaque typed JSON values become {}. Metadata retains only explicitly supported fields, including recognized scope status values and valid nemo_relay.log.severity values for marks. Unknown custom-mark payloads are emptied by default; custom_mark_payload_policy = "preserve" explicitly retains them.

Scope metadata retains only recognized otel.status_code values (OK, ERROR, UNSET) and, for ERROR, Relay’s fixed error.type classifications or _OTHER. Unrecognized error types are discarded; the full and gen_ai OpenTelemetry projections use _OTHER for errors without a classification. Free-form status descriptions, exception types, and other metadata remain discarded, so redaction preserves success/error reporting without exporting error messages or application exception names.

Typed LLM annotations retain message roles and content-part kinds; provider, model, and tool names; request tuning settings; finish reasons; token and cache usage; and normalized cost amounts, currency, and source classification. Conversational content and other application identifiers use the configured replacement, which defaults to [REDACTED]. Opaque typed fields become {}; approved typed numeric and boolean analytics remain available. Routing metadata, pricing provenance, optimization contribution evidence, and unknown fields are discarded. Relay lifecycle identifiers remain unchanged.

Tool-call correlation IDs remain unchanged across LLM messages and tool lifecycle events so exporters can associate results with the correct calls. Response IDs and other application identifiers still use the replacement marker.

Provider, model, tool, and metric names are intentional free-form exceptions. Do not place user content, secrets, or personal identifiers in these fields. All redacted application identifiers collapse to the same marker and are not suitable correlation keys. To retain an unknown custom mark unchanged, set custom_mark_payload_policy = "preserve" explicitly and only for a trusted producer.

Typed Metric Marks

Typed metric sanitization and allowed dimensions

Relay recognizes marks that use its nemo.relay.metric_measurements schema as typed metric envelopes. PII sanitization preserves the measurement name, kind, value type, numeric value, and histogram boundaries so the OpenTelemetry metric exporter can validate and export the mark.

The plugin sanitizes optional text without invalidating the envelope:

  • With neither target_paths nor target_path_globs, measurement descriptions and string-valued metric attributes follow the configured built-in action.
  • With target selectors, those fields are sanitized only when their payload-relative JSON Pointer matches, such as /measurements/0/description or /measurements/0/attributes/owner.
  • With action = "remove", targeting a whole string-array attribute removes that attribute. Targeting an individual element removes that element and compacts the remaining array; Relay cannot use a null placeholder because typed OpenTelemetry metric attributes must remain valid homogeneous arrays.
  • Outside trajectory_context, numeric and boolean metric attributes remain analytical telemetry.

The trajectory_context preset replaces descriptions with its configured replacement while retaining required measurement fields. It drops every metric attribute except an exact string value admitted by metric_string_attribute_allowlist. That preset does not accept target selectors. Its custom_mark_payload_policy applies only to opaque marks in the custom category; it does not control Relay typed metric envelopes.

Matching is case-sensitive and requires both the attribute name and value. A string-array attribute is retained only when every element is allowed; one unexpected value drops the whole attribute. Values under an unlisted key, unexpected values under a listed key, and numeric or boolean attributes are dropped. This allowlist does not apply to descriptions, mark metadata, category profiles, or non-metric marks. Configure fixed constants or bounded enums only; do not list free-form identifiers or user-provided text.

On every mark, the preset preserves the reserved nemo_relay.log.severity metadata field in canonical form when it contains a supported severity. For opaque custom marks using redact_all_leaves, unsupported severity values and all other opaque fields are discarded.

Refer to the Metric dimensions example in Targeting and Composition for a complete configuration and payload comparison.

Selection Accounting

Enable runtime diagnostics to distinguish a valid selector from a policy that changed content. Refer to Verify the Policy for the diagnostic fields and setup.

With action = "remove", a child string inside a selected container can count as selected even though removing the container prevents the sanitizer from visiting the child. Only the removed container counts as affected. This also applies when one recursive selector matches both nodes. For example, /messages/** applied to {"messages":{"text":"alice@example.com"}} with remove reports matched_paths=2, eligible_string_leaves=1, and affected_values=1.

For recursive remove policies, enabling match diagnostics adds a selection-counting pass before removal. A selected string can also remain unchanged when its detector finds nothing.

With neither selector list configured, matched_paths counts all visited JSON nodes and eligible_string_leaves counts the eligible string nodes.

The trajectory_context preset uses its semantic sanitization path rather than this selector diagnostic. Use the preset when the goal is broad payload removal; refer to Trajectory Context Preset.