From 94668950caf5ca0d5f08e51c1bdd72904b20833b Mon Sep 17 00:00:00 2001 From: erijo Date: Wed, 19 Aug 2026 00:09:58 +0200 Subject: [PATCH] fix(otel): do not set AS_ROOT on child spans with parent_span_id --- langfuse/_client/client.py | 20 +++++++++++++++++--- tests/unit/test_otel.py | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index 2b2889545..496b1fcc0 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -723,7 +723,8 @@ def start_observation( cast(otel_trace_api.Span, remote_parent_span) ): otel_span = self._otel_tracer.start_span(name=name) - otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) + if parent_span_id is None: + otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) return self._create_observation_from_otel_span( otel_span=otel_span, @@ -1099,6 +1100,7 @@ def start_as_current_observation( as_type=as_type, name=name, remote_parent_span=remote_parent_span, + is_root=parent_span_id is None, parent=None, end_on_exit=end_on_exit, input=input, @@ -1164,6 +1166,7 @@ def start_as_current_observation( as_type=as_type, name=name, remote_parent_span=remote_parent_span, + is_root=parent_span_id is None, parent=None, end_on_exit=end_on_exit, input=input, @@ -1270,6 +1273,15 @@ def _get_observation_type_from_otel_span(otel_span: otel_trace_api.Span) -> str: return observation_type if isinstance(observation_type, str) else "span" + def _get_observation_type( + self, + span_class: Type[Any], + ) -> ObservationTypeLiteralNoEvent: + """Get the observation type string from a span class.""" + observation_type = getattr(span_class, "_observation_type", "span") + + return observation_type if isinstance(observation_type, str) else "span" + @_agnosticcontextmanager def _create_span_with_parent_context( self, @@ -1277,6 +1289,7 @@ def _create_span_with_parent_context( name: str, parent: Optional[otel_trace_api.Span] = None, remote_parent_span: Optional[otel_trace_api.Span] = None, + is_root: bool = False, as_type: ObservationTypeLiteralNoEvent, end_on_exit: Optional[bool] = None, input: Optional[Any] = None, @@ -1312,7 +1325,7 @@ def _create_span_with_parent_context( cost_details=cost_details, prompt=prompt, ) as langfuse_span: - if remote_parent_span is not None: + if is_root: langfuse_span._otel_span.set_attribute( LangfuseOtelSpanAttributes.AS_ROOT, True ) @@ -1693,7 +1706,8 @@ def create_event( otel_span = self._otel_tracer.start_span( name=name, start_time=timestamp ) - otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) + if parent_span_id is None: + otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) return cast( LangfuseEvent, diff --git a/tests/unit/test_otel.py b/tests/unit/test_otel.py index 46a085a71..5b0c5e910 100644 --- a/tests/unit/test_otel.py +++ b/tests/unit/test_otel.py @@ -882,7 +882,7 @@ def test_custom_parent_span_id(self, langfuse_client, memory_exporter): spans = self.get_spans_by_name(memory_exporter, "custom-parent-span") assert len(spans) == 1, "Expected one span" assert spans[0]["trace_id"] == trace_id - assert spans[0]["attributes"][LangfuseOtelSpanAttributes.AS_ROOT] is True + assert LangfuseOtelSpanAttributes.AS_ROOT not in spans[0]["attributes"] def test_multiple_generations_in_trace(self, langfuse_client, memory_exporter): """Test creating multiple generation spans within the same trace."""