This commit is contained in:
Mihidum 2026-08-26 14:32:51 -04:00 committed by GitHub
commit 581d58d478
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 63 additions and 0 deletions

View file

@ -37,6 +37,18 @@ class GenAIEventRecorder:
self.event_logger.emit(
Event(
name=GenAIEvent.OPERATION_EXCEPTION,
# The body MUST be set. ``Event.body`` defaults to ``None``, and
# OTLP's ``AnyValue`` has no representation for ``None``: the
# exporter's ``_encode_value`` raises ``Invalid type <class
# 'NoneType'>``, and ``BatchLogRecordProcessor._export_batch``
# swallows that and discards the WHOLE batch — so one such event
# silently destroys every log record batched with it. The
# encoder grew a ``None`` branch in opentelemetry-exporter-otlp-
# proto-common 1.43.0, but litellm pins 1.28.0, so this event is
# unexportable as shipped. The message is the natural body for a
# WARN record and adds no data the attributes don't already
# carry.
body=message,
timestamp=timestamp_ns,
trace_id=span_context.trace_id,
span_id=span_context.span_id,

View file

@ -940,6 +940,57 @@ def test_operation_exception_log_event_always_carries_required_pair():
assert ExceptionEvent.STACKTRACE not in attributes
def test_operation_exception_log_event_is_otlp_encodable():
"""The event must survive the real OTLP encoder, not just an in-memory exporter.
``Event.body`` defaults to ``None`` and OTLP's ``AnyValue`` cannot represent
it, so an event emitted without a body raises ``Invalid type <class
'NoneType'>`` inside ``encode_logs``. That happens on the exporter's batch
thread, where ``BatchLogRecordProcessor._export_batch`` catches it, logs it
and drops the WHOLE batch every record batched alongside is lost silently.
Every other test here uses ``InMemoryLogExporter``, which never encodes, so
only an explicit encode step can catch this.
"""
encode_logs = pytest.importorskip(
"opentelemetry.exporter.otlp.proto.common._internal._log_encoder"
).encode_logs
engine, _, log_exporter = _engine_with_event_recorder()
engine.emit(
SpanRole.LLM_CALL,
_llm_call_data(SpanError(error_type="RateLimitError", message="rate limited")),
)
logs = log_exporter.get_finished_logs()
# Raises "Invalid type <class 'NoneType'> of value None" when body is unset.
request = encode_logs(logs)
assert request.SerializeToString()
(resource_logs,) = request.resource_logs
(scope_logs,) = resource_logs.scope_logs
(log_record,) = scope_logs.log_records
assert log_record.body.string_value == "rate limited"
def test_operation_exception_log_event_body_is_never_none():
"""A batch is dropped whole on an unencodable body, so this is asserted for
both event shapes: with and without a stacktrace."""
engine, _, log_exporter = _engine_with_event_recorder()
engine.emit(
SpanRole.LLM_CALL,
_llm_call_data(SpanError(error_type="APIError", message="boom")),
)
engine.emit(
SpanRole.LLM_CALL,
_llm_call_data(
SpanError(error_type="APIError", message="boom", stack_trace="Traceback ...")
),
)
bodies = [log.log_record.body for log in log_exporter.get_finished_logs()]
assert bodies == ["boom", "boom"]
def test_operation_exception_log_event_not_emitted_on_success():
engine, span_exporter, log_exporter = _engine_with_event_recorder()
engine.emit(SpanRole.LLM_CALL, _llm_call_data(None))