mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-19 04:54:37 +00:00
fix(otel): drop None from semantic log events to prevent OTLP export drops
_emit_semantic_logs emits gen_ai.content.prompt/completion log records whose attributes and body can contain None (finish_reason, gen_ai.system for an unresolved provider, and raw message fields carried by msg.copy() such as content/tool_calls). OTLP AnyValue cannot encode None, so the exporter raises "Invalid type <class 'NoneType'>" while encoding and BatchLogRecordProcessor drops the whole batch, silently losing the content events. Recursively drop None from the attributes and body before emitting.
This commit is contained in:
parent
10d5804b3e
commit
468e33ace7
2 changed files with 74 additions and 4 deletions
|
|
@ -1618,6 +1618,32 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
|
|||
)
|
||||
return LogRecord, SeverityNumber
|
||||
|
||||
@staticmethod
|
||||
def _drop_none_values(value):
|
||||
"""Recursively remove ``None`` from a log record's attributes/body.
|
||||
|
||||
OTLP ``AnyValue`` has no representation for ``None``. A single ``None``
|
||||
anywhere in an emitted log record makes the OTLP exporter raise
|
||||
``Invalid type <class 'NoneType'>`` while encoding and drop the whole
|
||||
batch, so the content events are silently lost. Sources of ``None``
|
||||
here include ``choice.finish_reason``, ``gen_ai.system`` (unresolved
|
||||
provider) and raw message fields carried by ``msg.copy()`` such as
|
||||
``content`` or ``tool_calls``.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: OpenTelemetry._drop_none_values(val)
|
||||
for key, val in value.items()
|
||||
if val is not None
|
||||
}
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [
|
||||
OpenTelemetry._drop_none_values(item)
|
||||
for item in value
|
||||
if item is not None
|
||||
]
|
||||
return value
|
||||
|
||||
def _emit_semantic_logs(self, kwargs, response_obj, span: Span):
|
||||
if not self.config.enable_events:
|
||||
return
|
||||
|
|
@ -1675,8 +1701,8 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
|
|||
trace_flags=parent_ctx.trace_flags,
|
||||
severity_number=SeverityNumber.INFO,
|
||||
severity_text="INFO",
|
||||
body=body,
|
||||
attributes=attrs,
|
||||
body=self._drop_none_values(body),
|
||||
attributes=self._drop_none_values(attrs),
|
||||
)
|
||||
otel_logger.emit(log_record)
|
||||
|
||||
|
|
@ -1707,8 +1733,8 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
|
|||
trace_flags=parent_ctx.trace_flags,
|
||||
severity_number=SeverityNumber.INFO,
|
||||
severity_text="INFO",
|
||||
body=body,
|
||||
attributes=attrs,
|
||||
body=self._drop_none_values(body),
|
||||
attributes=self._drop_none_values(attrs),
|
||||
)
|
||||
otel_logger.emit(log_record)
|
||||
|
||||
|
|
|
|||
|
|
@ -5852,3 +5852,47 @@ class TestOpenTelemetryMetricAttributeFiltering(unittest.TestCase):
|
|||
exporter="console", attributes=attributes
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestOpenTelemetrySemanticLogsNoneSafety(unittest.TestCase):
|
||||
def test_drop_none_values_recursively_removes_none(self):
|
||||
payload = {
|
||||
"gen_ai.system": None,
|
||||
"index": 0,
|
||||
"finish_reason": None,
|
||||
"message": {"role": "assistant", "content": None},
|
||||
"tool_calls": [None, {"id": "a", "name": None}],
|
||||
}
|
||||
self.assertEqual(
|
||||
OpenTelemetry._drop_none_values(payload),
|
||||
{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant"},
|
||||
"tool_calls": [{"id": "a"}],
|
||||
},
|
||||
)
|
||||
|
||||
def test_sanitized_payload_encodes_without_nonetype_error(self):
|
||||
"""A ``None`` anywhere in a semantic-log record makes the OTLP exporter
|
||||
raise ``Invalid type <class 'NoneType'>`` and drop the whole batch.
|
||||
``_drop_none_values`` removes them so the same payload encodes cleanly.
|
||||
"""
|
||||
from opentelemetry.exporter.otlp.proto.common._internal import (
|
||||
_encode_value,
|
||||
)
|
||||
|
||||
attrs = {
|
||||
"event_name": "gen_ai.content.completion",
|
||||
"gen_ai.system": None,
|
||||
"index": 0,
|
||||
"finish_reason": None,
|
||||
}
|
||||
body = {"role": "assistant", "content": None, "tool_calls": None}
|
||||
|
||||
with self.assertRaises(Exception):
|
||||
_encode_value(attrs)
|
||||
with self.assertRaises(Exception):
|
||||
_encode_value(body)
|
||||
|
||||
_encode_value(OpenTelemetry._drop_none_values(attrs))
|
||||
_encode_value(OpenTelemetry._drop_none_values(body))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue