This commit is contained in:
Max Azatian 2026-10-04 18:27:58 -04:00 • committed by GitHub
commit 40f6a955de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 76 additions and 2 deletions

View file

@ -2162,6 +2162,15 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
# OR if USE_OTEL_LITELLM_REQUEST_SPAN is explicitly enabled
should_create_primary_span: Final = parent_otel_span is None or get_secret_bool("USE_OTEL_LITELLM_REQUEST_SPAN")
standard_logging_payload: Final[StandardLoggingPayload | None] = kwargs.get("standard_logging_object")
error_information: Final = (
standard_logging_payload.get("error_information") if standard_logging_payload else None
)
error_message: Final = (error_information.get("error_message") if error_information else None) or (
standard_logging_payload.get("error_str") if standard_logging_payload else None
)
error_status: Final = Status(StatusCode.ERROR, error_message if isinstance(error_message, str) else None)
span = None
if should_create_primary_span:
# Span 1: Request sent to litellm SDK
@ -2174,7 +2183,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
if self._gen_ai_semconv_latest_experimental:
span_kwargs["kind"] = self.span_kind.CLIENT
span = otel_tracer.start_span(**span_kwargs)
span.set_status(Status(StatusCode.ERROR))
span.set_status(error_status)
self.set_attributes(span, kwargs, response_obj)
# Record exception information using OTEL standard method
@ -2187,7 +2196,7 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
# Only set attributes if the span is still recording (not closed)
# Note: parent_otel_span is guaranteed to be not None here
if parent_otel_span.is_recording():
parent_otel_span.set_status(Status(StatusCode.ERROR))
parent_otel_span.set_status(error_status)
self.set_attributes(parent_otel_span, kwargs, response_obj)
self._record_exception_on_span(span=parent_otel_span, kwargs=kwargs)

View file

@ -3558,6 +3558,71 @@ class TestOpenTelemetrySemanticConventions138(unittest.TestCase):
error_spans = [s for s in spans if s.status.status_code == StatusCode.ERROR]
self.assertTrue(error_spans, "Expected at least one span with ERROR status")
@parameterized.expand(
[
(
"own_span_error_information",
False,
{
"error_str": "litellm.ServiceUnavailableError: upstream returned 503",
"error_information": {
"error_class": "ServiceUnavailableError",
"error_message": "upstream returned 503",
},
},
"upstream returned 503",
),
(
"own_span_error_str_only",
False,
{"error_str": "litellm.ServiceUnavailableError: upstream returned 503"},
"litellm.ServiceUnavailableError: upstream returned 503",
),
(
"parent_span_error_information",
True,
{
"error_information": {
"error_class": "ServiceUnavailableError",
"error_message": "upstream returned 503",
}
},
"upstream returned 503",
),
]
)
def test_handle_failure_error_status_carries_the_error_message(
self, _name: str, has_parent_span: bool, error_fields: dict, expected_description: str
):
span_exporter = InMemorySpanExporter()
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(SimpleSpanProcessor(span_exporter))
otel = OpenTelemetry(tracer_provider=tracer_provider)
otel.tracer = tracer_provider.get_tracer("litellm")
active_parent_span = (
trace.use_span(tracer_provider.get_tracer("proxy").start_span("parent_span"), end_on_exit=True)
if has_parent_span
else contextlib.nullcontext()
)
start = datetime.now(timezone.utc)
end = start + timedelta(seconds=1)
kwargs = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello"}],
"optional_params": {},
"litellm_params": {"custom_llm_provider": "openai"},
"standard_logging_object": {"id": "test-id", "call_type": "completion", "metadata": {}, **error_fields},
"exception": Exception("upstream returned 503"),
}
with patch.dict(os.environ, {"USE_OTEL_LITELLM_REQUEST_SPAN": "false"}), active_parent_span:
otel._handle_failure(kwargs, None, start, end)
error_spans = [s for s in span_exporter.get_finished_spans() if s.status.status_code == trace.StatusCode.ERROR]
self.assertEqual(len(error_spans), 1)
self.assertEqual(error_spans[0].name, "parent_span" if has_parent_span else "litellm_request")
self.assertEqual(error_spans[0].status.description, expected_description)
class TestRawSpanAttributeIsolation(unittest.TestCase):
"""Issue #3: raw_gen_ai_request span should only contain provider-specific