Skip to content

Proposal: TelemetrySanitizer interface, built-in StandardTelemetrySanitizer, and composable sanitizer chain on TelemetryConfig #7845

Description

@juliays

** Please make sure you read the contribution guide and file the issues in the right place. **
Contribution guide.

🔴 Required Information

Is your feature request related to a specific problem?

Yes. Today, ADK's telemetry and logging layers face two related redaction/sanitization problems:

  1. Fragmented built-in credential/secret redaction across ADK modules:
  2. No extensible sanitizer interface between raw ALL_CONTENT and blind NO_CONTENT on TelemetryConfig:
    • In src/google/adk/telemetry/context.py, TelemetryConfig and ContentCapturingMode only offer a binary choice between capturing raw payloads (ALL_CONTENT / SPAN_ONLY / EVENT_ONLY) and dropping all payload content (NO_CONTENT, where legacy attributes record "{}" and OTel GenAI events omit message bodies; see docs(telemetry): disable all content paths in the admin lock recipe #7739).
    • Regulated enterprise deployments (healthcare, finance, customer support) need gcp.vertex.agent.llm_request, llm_response, tool_call_args, tool_response, and gen_ai.* events to retain structural JSON (schema keys, tool names, non-sensitive fields, and redacted placeholders like [REDACTED_EMAIL]) while scrubbing domain-specific PII/secrets before attributes are serialized onto the span or log record.
    • Furthermore, even under ContentCapturingMode.NO_CONTENT, unhandled tool or model exceptions flow through src/google/adk/telemetry/_instrumentation.py (_finish_scope_with_error / ToolScope) into span.record_exception(error) and Status(StatusCode.ERROR, description=str(error)). Because Pydantic ValidationError, HTTP client errors, and database exceptions routinely embed raw input values in str(error), sensitive payloads leak into span status descriptions and exception events even when content capture is disabled.

Describe the Solution You'd Like

Introduce a pluggable sanitizer architecture in google.adk.telemetry with three parts:

  1. TelemetrySanitizer interface (Protocol / ABC):
    Defines a standard contract for pre-export telemetry sanitization covering both structured content payloads and exception descriptions:

    • sanitize_content(self, value: Any) -> Any: Transforms/redacts LLM request/response payloads, tool args/responses, and GenAI event bodies before safe_json_serialize writes them to spans or OTel logs.
    • sanitize_exception(self, error: BaseException) -> str: Scrubs exception message text before it is recorded on Status(StatusCode.ERROR, description=...) and exception.message span events (active even under ContentCapturingMode.NO_CONTENT).
  2. Built-in StandardTelemetrySanitizer shipped by ADK:
    Consolidates ADK's standard credential and secret hygiene (AuthCredential / AuthConfig secret fields from safe_json_serialize dumps AuthCredential secret fields into span attributes; MCP headers are redacted, these are not #7311, Bearer/Basic auth tokens, PEM private keys, and sensitive header/dict keys) into a reusable, fail-closed built-in implementation of TelemetrySanitizer.

  3. Composable configuration on TelemetryConfig (extend or replace):
    Allow teams to either append additional custom sanitizers on top of StandardTelemetrySanitizer or replace the sanitizer pipeline entirely on TelemetryConfig:

    • sanitizers: tuple[TelemetrySanitizer, ...] = (StandardTelemetrySanitizer(),) (or extra_sanitizers to append to the standard sanitizer vs. sanitizers to replace it).
    • Sanitizers execute in sequence (s1 -> s2 -> ...), with fail-closed error handling (if a sanitizer raises on malformed input, return a safe "[REDACTION_ERROR]" marker rather than leaking raw content or crashing the agent invocation).

Impact on your work

In production ticket-routing and enterprise support agents, we currently maintain a custom ADK plugin + custom OTel SpanProcessor that reaches into private ReadableSpan._attributes and scrubs StatusCode.ERROR descriptions post-hoc to pass compliance checks. Having ADK supply the TelemetrySanitizer interface and StandardTelemetrySanitizer—while letting teams chain domain-specific PII sanitizers or swap in their own enterprise DLP sanitizer via TelemetryConfig—eliminates private OTel SDK state mutation and unifies redaction across ADK.

Willingness to contribute

Yes — happy to submit a focused PR with unit tests once we align on the interface and TelemetryConfig API.


🟡 Recommended Information

Describe Alternatives You've Considered

  1. ContentCapturingMode.NO_CONTENT (docs(telemetry): disable all content paths in the admin lock recipe #7739): Drops all prompt, response, and tool payload structure ("{}"), making it impossible to inspect non-PII fields in Cloud Trace / Dev UI, and still leaves Status(StatusCode.ERROR, description=str(error)) unsanitized.
  2. Custom OTel SpanProcessor: Requires mutating ReadableSpan._attributes (a private attribute on an immutable-by-contract OTel object) and parsing already-serialized JSON strings (gcp.vertex.agent.llm_request) back into dicts on every span end.
  3. Raw single-callable hook without a standard sanitizer: Forces every team to re-implement baseline credential/token masking from scratch instead of layering domain rules onto ADK's built-in secret hygiene.

Proposed API / Implementation

from typing import Any, Protocol, runtime_checkable
from google.adk.telemetry.context import ContentCapturingMode, TelemetryConfig
from google.adk.telemetry.sanitizers import (
    StandardTelemetrySanitizer,
    TelemetrySanitizer,
)


@runtime_checkable
class TelemetrySanitizer(Protocol):
  """Contract for pre-export telemetry content and exception sanitization."""

  def sanitize_content(self, value: Any) -> Any:
    """Sanitizes structured/text payloads before JSON serialization onto spans/logs."""
    ...

  def sanitize_exception(self, error: BaseException) -> str:
    """Sanitizes exception messages before recording StatusCode.ERROR / exception events."""
    ...


class CustomerPiiSanitizer:
  """Team-specific sanitizer layered on top of ADK's StandardTelemetrySanitizer."""

  def sanitize_content(self, value: Any) -> Any:
    # Redact SSNs, emails, payment cards in dicts/lists/strings
    ...

  def sanitize_exception(self, error: BaseException) -> str:
    # Scrub PII patterns from str(error)
    ...


# 1. Default: uses (StandardTelemetrySanitizer(),) out of the box
default_cfg = TelemetryConfig()

# 2. Extend: keep ADK's StandardTelemetrySanitizer and chain custom sanitizers after it
extended_cfg = TelemetryConfig(
    capture_message_content=ContentCapturingMode.SPAN_ONLY,
    sanitizers=(StandardTelemetrySanitizer(), CustomerPiiSanitizer()),
)

# 3. Replace: supply a completely custom enterprise sanitizer
replaced_cfg = TelemetryConfig(
    capture_message_content=ContentCapturingMode.SPAN_ONLY,
    sanitizers=(EnterpriseDlpSanitizer(),),
)

Additional Context

Activity

  1. changed the title [-]Proposal: Optional pre-export content and exception sanitizer hooks on `TelemetryConfig`[/-] [+]Proposal: `TelemetrySanitizer` interface, built-in `StandardTelemetrySanitizer`, and composable sanitizer chain on `TelemetryConfig`[/+] on Oct 29, 2026
  2. added
    tracing[Component] This issue is related to OpenTelemetry tracing
    on Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

tracing[Component] This issue is related to OpenTelemetry tracing

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions