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:
ryan-crabbe-berri
2026-05-16 15:29:56 -07:00
committed by GitHub
parent 1b9acecbb3
commit 86f73e5e8a
2 changed files with 59 additions and 0 deletions
+24
View File
@@ -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