mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 08:23:17 +00:00
feat(otel): set http.response.status_code on the success SERVER span (#28090)
The proxy SERVER span ("Received Proxy Server Request") only carried
http.response.status_code on failures (set in _record_exception_on_span),
so success traces had no 2xx bucket — error-ratio and status-breakdown
dashboards were missing their denominator and the span violated the HTTP
semconv (the attribute is required whenever a response is sent). Add a
set_response_status_code_attribute helper and call it from
async_post_call_success_hook with 200, symmetric with the failure path
and the existing route/preprocessing-duration SERVER-span attributes.
This commit is contained in:
@@ -736,6 +736,11 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
|
||||
# Pre-request latency on the SERVER span (success path).
|
||||
self.set_preprocessing_duration_attribute(parent_span, kwargs)
|
||||
|
||||
# http.response.status_code on the SERVER span (success path).
|
||||
# A successful proxy response is HTTP 200; the failure path sets
|
||||
# this from the error code in _record_exception_on_span.
|
||||
self.set_response_status_code_attribute(parent_span, 200)
|
||||
|
||||
# 3. Guardrail span
|
||||
self._create_guardrail_span(kwargs=kwargs, context=ctx)
|
||||
|
||||
@@ -2957,6 +2962,25 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
|
||||
span=span, key=HTTP_ROUTE_ATTRIBUTE, value=http_route
|
||||
)
|
||||
|
||||
def set_response_status_code_attribute(
|
||||
self, span: Optional[Span], status_code: Optional[int]
|
||||
) -> None:
|
||||
"""
|
||||
Set OTel-standard ``http.response.status_code`` (int) on the proxy
|
||||
SERVER span. The failure path sets this from the error code in
|
||||
``_record_exception_on_span``; this is the success-path counterpart
|
||||
so the attribute is present on every SERVER span regardless of
|
||||
outcome (required by the HTTP semconv, and needed for error-ratio /
|
||||
status-breakdown dashboards). No-op if span/value missing.
|
||||
"""
|
||||
if span is None or status_code is None:
|
||||
return
|
||||
self.safe_set_attribute(
|
||||
span=span,
|
||||
key=HTTP_RESPONSE_STATUS_CODE_ATTRIBUTE,
|
||||
value=int(status_code),
|
||||
)
|
||||
|
||||
def set_preprocessing_duration_attribute(
|
||||
self, span: Optional[Span], container: Any
|
||||
) -> None:
|
||||
|
||||
@@ -4808,6 +4808,41 @@ class TestOpenTelemetrySetProxyRequestRouteAttributes(unittest.TestCase):
|
||||
otel.set_proxy_request_route_attributes(None, url_path="/x", http_route="/x")
|
||||
|
||||
|
||||
class TestOpenTelemetrySetResponseStatusCodeAttribute(unittest.TestCase):
|
||||
"""http.response.status_code must land on the SERVER span on the
|
||||
success path too (failure path sets it in _record_exception_on_span).
|
||||
Without this the attribute is failure-only, so error-ratio /
|
||||
status-breakdown dashboards have no 2xx bucket.
|
||||
"""
|
||||
|
||||
def _set(self, status_code):
|
||||
exporter = InMemorySpanExporter()
|
||||
provider = TracerProvider()
|
||||
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
||||
tracer = provider.get_tracer(__name__)
|
||||
|
||||
otel = OpenTelemetry()
|
||||
span = tracer.start_span("Received Proxy Server Request")
|
||||
otel.set_response_status_code_attribute(span, status_code)
|
||||
span.end()
|
||||
return exporter.get_finished_spans()[0]
|
||||
|
||||
def test_success_sets_int_200(self):
|
||||
span = self._set(200)
|
||||
# Exact OTel-standard name, stored as int (regression guard).
|
||||
assert span.attributes["http.response.status_code"] == 200
|
||||
assert isinstance(span.attributes["http.response.status_code"], int)
|
||||
|
||||
def test_none_status_code_omits_attribute(self):
|
||||
span = self._set(None)
|
||||
assert "http.response.status_code" not in span.attributes
|
||||
|
||||
def test_none_span_is_noop(self):
|
||||
otel = OpenTelemetry()
|
||||
# Mirrors the Langfuse-override path (create span returns None).
|
||||
otel.set_response_status_code_attribute(None, 200)
|
||||
|
||||
|
||||
class TestOpenTelemetryPreprocessingDuration(unittest.TestCase):
|
||||
"""litellm.preprocessing.duration_ms (proxy-receive -> first provider
|
||||
handoff) on the SERVER span. Read from container metadata so the
|
||||
|
||||
Reference in New Issue
Block a user