Skip to content

🐛 Bug Report: Anthropic event emitter leaks NOT_GIVEN sentinel into log Body, breaking OTLP log export #4230

@marians

Description

@marians

Which component is this bug for?

Anthropic Instrumentation

📜 Description

When the Anthropic instrumentation is run in event-logging mode (use_legacy_attributes=False, with an event_logger_provider configured), a messages.create() call that omits tools causes the emitted GenAI input event to contain the Anthropic SDK sentinel anthropic.NOT_GIVEN in its log record Body. The OTLP log exporter cannot serialize anthropic.NotGiven, so the entire log batch fails to export with:

opentelemetry.sdk._shared_internal - ERROR - Exception while exporting Log.
...
Exception: Invalid type <class 'anthropic.NotGiven'> of value NOT_GIVEN

Root cause

In opentelemetry/instrumentation/anthropic/event_emitter.py, emit_input_events():

    if kwargs.get("system"):                       # truthiness — NOT_GIVEN is falsy, correctly skipped
        emit_event(MessageEvent(content=kwargs.get("system"), role="system"), event_logger)
    for message in kwargs.get("messages"):
        emit_event(MessageEvent(content=message.get("content"), role=message.get("role")), event_logger)
    if kwargs.get("tools") is not None:            # BUG: `is not None` lets NOT_GIVEN through
        emit_event(
            MessageEvent(content={"tools": kwargs.get("tools")}, role="user"),   # NOT_GIVEN placed in Body
            event_logger,
        )

When a caller omits tools, the Anthropic SDK passes tools=anthropic.NOT_GIVEN. NOT_GIVEN is not None evaluates to True, so the guard passes and the sentinel object is put verbatim into the MessageEvent body
({"tools": NOT_GIVEN}). It then flows into the OTel LogRecord body, and the OTLP protobuf encoder
(opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder) raises on the unsupported type.

Note the inconsistency: the system branch immediately above uses a truthiness check (if kwargs.get("system"):), which correctly drops NOT_GIVEN (bool(NOT_GIVEN) is False). Only the tools branch uses is not None, so
only tools leaks the sentinel.

👟 Reproduction steps

  1. Instrument Anthropic in event mode with an OTLP log exporter:
    from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
    from opentelemetry.sdk._events import EventLoggerProvider
    from opentelemetry.sdk._logs import LoggerProvider
    from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
    from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
    
    logger_provider = LoggerProvider()
    logger_provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
    AnthropicInstrumentor().instrument(
        event_logger_provider=EventLoggerProvider(logger_provider),
    )
    # equivalent to use_legacy_attributes=False + event logging enabled
  2. Make a messages.create() call without the tools argument (so the SDK defaults it to NOT_GIVEN):
    import anthropic
    client = anthropic.Anthropic()
    client.messages.create(
        model="claude-sonnet-4-...",
        max_tokens=256,
        system="You are a helpful assistant.",
        messages=[{"role": "user", "content": "Hi"}],
        # note: no `tools=` argument
    )
  3. On the next log batch export, the exporter crashes with Invalid type <class 'anthropic.NotGiven'> of value NOT_GIVEN and the batch is dropped.

👍 Expected behavior

Omitted optional parameters (represented by anthropic.NOT_GIVEN) should not be emitted into event bodies. The input event should simply not include a tools entry when no tools were passed — mirroring the existing system handling.

👎 Actual Behavior with Screenshots

MessageEvent(content={"tools": NOT_GIVEN}, role="user") is emitted, placing a non-serializable anthropic.NotGiven into the OTel log Body. The OTLP log exporter then fails to encode the batch and all log records in that batch are lost. A full traceback is logged on every export interval.

🤖 Python Version

3.13

📃 Provide any additional context for the Bug.

Suggested fix

Guard tools with a truthiness check, consistent with system (handles both
NOT_GIVEN and empty lists):

    if kwargs.get("tools"):        # NOT_GIVEN and [] are both falsy → skipped
        emit_event(
            MessageEvent(content={"tools": kwargs.get("tools")}, role="user"),
            event_logger,
        )

(Alternatively, sanitize all NOT_GIVEN values out of kwargs before building events, which also future-proofs emit_response_events and any other sentinel leakage.)

Stack trace

opentelemetry.sdk._shared_internal - ERROR - Exception while exporting Log.
Traceback (most recent call last):
  File ".../opentelemetry/sdk/_shared_internal/__init__.py", line 179, in _export
    self._exporter.export(...)
  File ".../opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py", line 116, in export
    return OTLPExporterMixin._export(self, batch)
  File ".../opentelemetry/exporter/otlp/proto/grpc/exporter.py", line 392, in _export
    request=self._translate_data(data),
  File ".../opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py", line 110, in _translate_data
    return encode_logs(data)
  File ".../_log_encoder/__init__.py", line 58, in _encode_log
    body=_encode_value(body, allow_null=True),
  File ".../_internal/__init__.py", line 95, in _encode_value
    _encode_key_value(str(k), v, allow_null=allow_null)
  File ".../_internal/__init__.py", line 107, in _encode_key_value
    key=key, value=_encode_value(value, allow_null=allow_null)
  File ".../_internal/__init__.py", line 100, in _encode_value
    raise Exception(f"Invalid type {type(value)} of value {value}")
Exception: Invalid type <class 'anthropic.NotGiven'> of value NOT_GIVEN

Additional context

  • Surfaced via kagent (v0.9.6), which enables event-mode GenAI logging and routes events to an OTLP LoggerProvider. The crash reproduces on the direct Anthropic API path.
  • It reliably fires on requests with no tools — e.g. ADK's event-compaction summarizer call (which sends no tools) — while normal tool-carrying turns serialize fine.
  • Functional impact is limited to telemetry: the LLM calls themselves succeed (HTTP 200); only the GenAI log export is lost, plus recurring traceback noise. Traces (separate exporter) are unaffected.

Versions

  • opentelemetry-instrumentation-anthropic: 0.52.5
  • anthropic: 0.104.1
  • opentelemetry-exporter-otlp-proto-common: 1.38.0

👀 Have you spent some time to check if this bug has been raised before?

  • I checked and didn't find similar issue

Are you willing to submit PR?

None

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions