fix(otel): emit litellm_request span when caller parent span already ended

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo 2026-08-13 19:45:47 +00:00
parent a947019eb0
commit e7ce773008
2 changed files with 16 additions and 8 deletions

View file

@ -1213,9 +1213,12 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
ctx = None
# Decide whether to create a primary span
# Always create if no parent span exists (backward compatibility)
# Always create if no parent span exists (backward compatibility), or if the
# parent already ended, since writes to it would be dropped by the SDK
# OR if USE_OTEL_LITELLM_REQUEST_SPAN is explicitly enabled
should_create_primary_span: Final = parent_span is None or get_secret_bool("USE_OTEL_LITELLM_REQUEST_SPAN")
should_create_primary_span: Final = (
parent_span is None or not is_recording(parent_span) or get_secret_bool("USE_OTEL_LITELLM_REQUEST_SPAN")
)
if should_create_primary_span:
# Create a new litellm_request span
@ -1231,9 +1234,8 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger):
from opentelemetry.trace import Status, StatusCode
span = None
if is_recording(parent_span):
parent_span.set_status(Status(StatusCode.OK))
self.set_attributes(parent_span, kwargs, response_obj)
parent_span.set_status(Status(StatusCode.OK))
self.set_attributes(parent_span, kwargs, response_obj)
# Raw-request as direct child of parent_span
self._maybe_log_raw_request(kwargs, response_obj, start_time, end_time, parent_span)

View file

@ -6047,9 +6047,15 @@ class TestEndedParentSpanNotWritten(unittest.TestCase):
finally:
otel_context.detach(token)
exported_server_spans = [s for s in span_exporter.get_finished_spans() if s.name == "POST /chat"]
self.assertEqual(len(exported_server_spans), 1)
self.assertNotIn("gen_ai.request.model", exported_server_spans[0].attributes or {})
exported = span_exporter.get_finished_spans()
server_spans = [s for s in exported if s.name == "POST /chat"]
self.assertEqual(len(server_spans), 1)
self.assertNotIn("gen_ai.request.model", server_spans[0].attributes or {})
litellm_spans = [s for s in exported if s.name == "litellm_request"]
self.assertEqual(len(litellm_spans), 1, "attributes must land on a fresh child span, not be dropped")
self.assertIn("gen_ai.request.model", litellm_spans[0].attributes or {})
self.assertEqual(litellm_spans[0].parent.span_id, server_spans[0].context.span_id)
def test_safe_set_attribute_skips_ended_span(self):
tracer_provider = TracerProvider()