fix(otel): put the error message on the failed span status

The legacy OTel logger (used by otel, langfuse_otel, arize and others) ended a failed call with Status(StatusCode.ERROR) and no description, so backends that read the span status, such as Langfuse's status message, showed an empty error. The text was only in the error.message attribute. Use the same message as that attribute, as the OTel v2 emitter already does.
This commit is contained in:
HardMax71 2026-10-02 16:49:50 +02:00
parent 615ed7900f
commit b6aad94707
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