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 a restricted, opt-in selector language: a whole * segment matches one object key or array index. Exact paths and glob paths are combined. Within target_path_globs, * is a wildcard only when it is the complete pointer segment. Segments such as content* and foo*bar match those literal object keys.

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.

Globs are not recursive: /output/* selects only a direct child of output. It does not select /output/0/content/0/text; use /output/*/content/*/text for that shape. Relay does not support **, predicates, or arbitrary JSONPath expressions. target_paths = ["/*"] continues to select a literal * object key; use target_path_globs = ["/*"] when * should be a wildcard.

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.

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. 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.