diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 8d588896b2f..a8ddb0a84be 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -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) diff --git a/tests/unit/integrations/test_opentelemetry.py b/tests/unit/integrations/test_opentelemetry.py index 175bd95c263..9b10f7ba650 100644 --- a/tests/unit/integrations/test_opentelemetry.py +++ b/tests/unit/integrations/test_opentelemetry.py @@ -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