3232"""
3333
3434from contextlib import contextmanager
35- from dataclasses import dataclass , field
36- from typing import Dict , List , Optional
35+ from contextvars import Token
36+ from typing import Dict , Optional
3737from uuid import UUID
3838
39+ from typing_extensions import TypeAlias
40+
41+ from opentelemetry import context as otel_context
3942from opentelemetry import trace
4043from opentelemetry .semconv ._incubating .attributes import (
4144 gen_ai_attributes as GenAI ,
4548 SpanKind ,
4649 Tracer ,
4750 set_span_in_context ,
48- use_span ,
4951)
5052
5153from .span_utils import (
5456)
5557from .types import Error , LLMInvocation
5658
57-
58- @dataclass
59- class _SpanState :
60- span : Span
61- children : List [UUID ] = field (default_factory = list )
59+ # Type alias matching the token type expected by opentelemetry.context.detach
60+ ContextToken : TypeAlias = Token [otel_context .Context ]
6261
6362
6463class BaseTelemetryGenerator :
@@ -87,43 +86,17 @@ def __init__(
8786 ):
8887 self ._tracer : Tracer = tracer or trace .get_tracer (__name__ )
8988
90- # TODO: Map from run_id -> _SpanState, to keep track of spans and parent/child relationships
91- self .spans : Dict [UUID , _SpanState ] = {}
92-
93- def _start_span (
94- self ,
95- name : str ,
96- kind : SpanKind ,
97- parent_run_id : Optional [UUID ] = None ,
98- ) -> Span :
99- parent_span = (
100- self .spans .get (parent_run_id )
101- if parent_run_id is not None
102- else None
103- )
104- if parent_span is not None :
105- ctx = set_span_in_context (parent_span .span )
106- span = self ._tracer .start_span (name = name , kind = kind , context = ctx )
107- else :
108- # top-level or missing parent
109- span = self ._tracer .start_span (name = name , kind = kind )
110- set_span_in_context (span )
111-
112- return span
113-
114- def _end_span (self , run_id : UUID ):
115- state = self .spans [run_id ]
116- for child_id in state .children :
117- child_state = self .spans .get (child_id )
118- if child_state :
119- child_state .span .end ()
120- state .span .end ()
121- del self .spans [run_id ]
89+ # Store the active span and its context attachment token
90+ self ._active : Dict [UUID , tuple [Span , ContextToken ]] = {}
12291
12392 def start (self , invocation : LLMInvocation ):
124- # Create/register the span; keep it active but do not end it here.
125- with self ._start_span_for_invocation (invocation ):
126- pass
93+ # Create a span and attach it as current; keep the token to detach later
94+ span = self ._tracer .start_span (
95+ name = f"{ GenAI .GenAiOperationNameValues .CHAT .value } { invocation .request_model } " ,
96+ kind = SpanKind .CLIENT ,
97+ )
98+ token = otel_context .attach (set_span_in_context (span ))
99+ self ._active [invocation .run_id ] = (span , token )
127100
128101 @contextmanager
129102 def _start_span_for_invocation (self , invocation : LLMInvocation ):
@@ -132,46 +105,46 @@ def _start_span_for_invocation(self, invocation: LLMInvocation):
132105 The span is not ended automatically on exiting the context; callers
133106 must finalize via _finalize_invocation.
134107 """
135- # Establish parent/child relationship if a parent span exists.
136- parent_state = (
137- self .spans .get (invocation .parent_run_id )
138- if invocation .parent_run_id is not None
139- else None
140- )
141- if parent_state is not None :
142- parent_state .children .append (invocation .run_id )
143- span = self ._start_span (
108+ # Create a span and attach it as current; keep the token to detach later
109+ span = self ._tracer .start_span (
144110 name = f"{ GenAI .GenAiOperationNameValues .CHAT .value } { invocation .request_model } " ,
145111 kind = SpanKind .CLIENT ,
146- parent_run_id = invocation .parent_run_id ,
147112 )
148- with use_span (span , end_on_exit = False ) as span :
149- span_state = _SpanState (
150- span = span ,
151- )
152- self .spans [invocation .run_id ] = span_state
153- yield span
113+ token = otel_context .attach (set_span_in_context (span ))
114+ # store active span and its context attachment token
115+ self ._active [invocation .run_id ] = (span , token )
116+ yield span
154117
155118 def finish (self , invocation : LLMInvocation ):
156- state = self .spans .get (invocation .run_id )
157- if state is None :
158- with self ._start_span_for_invocation (invocation ) as span :
119+ active = self ._active .get (invocation .run_id )
120+ if active is None :
121+ # If missing, create a quick span to record attributes and end it
122+ with self ._tracer .start_as_current_span (
123+ name = f"{ GenAI .GenAiOperationNameValues .CHAT .value } { invocation .request_model } " ,
124+ kind = SpanKind .CLIENT ,
125+ ) as span :
159126 _apply_finish_attributes (span , invocation )
160- self ._end_span (invocation .run_id )
161127 return
162128
163- span = state . span
129+ span , token = active
164130 _apply_finish_attributes (span , invocation )
165- self ._end_span (invocation .run_id )
131+ # Detach context and end span
132+ otel_context .detach (token )
133+ span .end ()
134+ del self ._active [invocation .run_id ]
166135
167136 def error (self , error : Error , invocation : LLMInvocation ):
168- state = self .spans .get (invocation .run_id )
169- if state is None :
170- with self ._start_span_for_invocation (invocation ) as span :
137+ active = self ._active .get (invocation .run_id )
138+ if active is None :
139+ with self ._tracer .start_as_current_span (
140+ name = f"{ GenAI .GenAiOperationNameValues .CHAT .value } { invocation .request_model } " ,
141+ kind = SpanKind .CLIENT ,
142+ ) as span :
171143 _apply_error_attributes (span , error )
172- self ._end_span (invocation .run_id )
173144 return
174145
175- span = state . span
146+ span , token = active
176147 _apply_error_attributes (span , error )
177- self ._end_span (invocation .run_id )
148+ otel_context .detach (token )
149+ span .end ()
150+ del self ._active [invocation .run_id ]
0 commit comments