mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
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:
parent
615ed7900f
commit
b6aad94707
2 changed files with 76 additions and 2 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue