6262
6363import timeit
6464from contextlib import contextmanager
65- from typing import Iterator
65+ from typing import Iterator , TypeVar
6666
6767from opentelemetry import context as otel_context
6868from opentelemetry ._logs import (
7878 get_tracer ,
7979 set_span_in_context ,
8080)
81+ from opentelemetry .trace .status import Status , StatusCode
8182from opentelemetry .util .genai .metrics import InvocationMetricsRecorder
8283from opentelemetry .util .genai .span_utils import (
8384 _apply_agent_finish_attributes ,
9091 AgentCreation ,
9192 AgentInvocation ,
9293 Error ,
94+ GenAIInvocation ,
9395 LLMInvocation ,
9496)
9597from opentelemetry .util .genai .version import __version__
9698
99+ _T = TypeVar ("_T" , bound = GenAIInvocation )
100+
97101
98102class TelemetryHandler :
99103 """
@@ -138,14 +142,10 @@ def _record_llm_metrics(
138142 error_type = error_type ,
139143 )
140144
141- def start_llm (
142- self ,
143- invocation : LLMInvocation ,
144- ) -> LLMInvocation :
145- """Start an LLM invocation and create a pending span entry."""
146- # Create a span and attach it as current; keep the token to detach later
145+ def _start (self , invocation : _T ) -> _T :
146+ """Start a GenAI invocation and create a pending span entry."""
147147 span = self ._tracer .start_span (
148- name = f" { invocation .operation_name } { invocation . request_model } " ,
148+ name = invocation .operation_name or " " ,
149149 kind = SpanKind .CLIENT ,
150150 )
151151 # Record a monotonic start timestamp (seconds) for duration
@@ -157,40 +157,106 @@ def start_llm(
157157 )
158158 return invocation
159159
160- def stop_llm (self , invocation : LLMInvocation ) -> LLMInvocation : # pylint: disable=no-self-use
161- """Finalize an LLM invocation successfully and end its span."""
160+ def _stop (self , invocation : _T ) -> _T :
161+ """Finalize a GenAI invocation successfully and end its span."""
162162 if invocation .context_token is None or invocation .span is None :
163163 # TODO: Provide feedback that this invocation was not started
164164 return invocation
165165
166166 span = invocation .span
167- _apply_llm_finish_attributes (span , invocation )
168- self ._record_llm_metrics (invocation , span )
169- _maybe_emit_llm_event (self ._logger , span , invocation )
170- # Detach context and end span
171- otel_context .detach (invocation .context_token )
172- span .end ()
167+ try :
168+ if isinstance (invocation , LLMInvocation ):
169+ _apply_llm_finish_attributes (span , invocation )
170+ self ._record_llm_metrics (invocation , span )
171+ _maybe_emit_llm_event (self ._logger , span , invocation )
172+ elif isinstance (invocation , AgentInvocation ):
173+ _apply_agent_finish_attributes (span , invocation )
174+ elif isinstance (invocation , AgentCreation ):
175+ _apply_creation_finish_attributes (span , invocation )
176+ else :
177+ span .set_status (
178+ Status (
179+ StatusCode .ERROR ,
180+ f"Unsupported invocation type: { type (invocation )!r} " ,
181+ )
182+ )
183+ raise TypeError (
184+ f"Unsupported invocation type: { type (invocation )!r} "
185+ )
186+ finally :
187+ otel_context .detach (invocation .context_token )
188+ span .end ()
173189 return invocation
174190
175- def fail_llm ( # pylint: disable=no-self-use
176- self , invocation : LLMInvocation , error : Error
177- ) -> LLMInvocation :
178- """Fail an LLM invocation and end its span with error status."""
191+ def _fail (self , invocation : _T , error : Error ) -> _T :
192+ """Fail a GenAI invocation and end its span with error status."""
179193 if invocation .context_token is None or invocation .span is None :
180194 # TODO: Provide feedback that this invocation was not started
181195 return invocation
182196
183197 span = invocation .span
184- _apply_llm_finish_attributes (invocation .span , invocation )
185- _apply_error_attributes (invocation .span , error )
186- error_type = getattr (error .type , "__qualname__" , None )
187- self ._record_llm_metrics (invocation , span , error_type = error_type )
188- _maybe_emit_llm_event (self ._logger , span , invocation , error )
189- # Detach context and end span
190- otel_context .detach (invocation .context_token )
191- span .end ()
198+ try :
199+ if isinstance (invocation , LLMInvocation ):
200+ _apply_llm_finish_attributes (span , invocation )
201+ _apply_error_attributes (span , error )
202+ error_type = getattr (error .type , "__qualname__" , None )
203+ self ._record_llm_metrics (
204+ invocation , span , error_type = error_type
205+ )
206+ _maybe_emit_llm_event (self ._logger , span , invocation , error )
207+ elif isinstance (invocation , AgentInvocation ):
208+ _apply_agent_finish_attributes (span , invocation )
209+ _apply_error_attributes (span , error )
210+ elif isinstance (invocation , AgentCreation ):
211+ _apply_creation_finish_attributes (span , invocation )
212+ _apply_error_attributes (span , error )
213+ else :
214+ span .set_status (
215+ Status (
216+ StatusCode .ERROR ,
217+ f"Unsupported invocation type: { type (invocation )!r} " ,
218+ )
219+ )
220+ raise TypeError (
221+ f"Unsupported invocation type: { type (invocation )!r} "
222+ )
223+ finally :
224+ otel_context .detach (invocation .context_token )
225+ span .end ()
192226 return invocation
193227
228+ def start (
229+ self ,
230+ invocation : _T ,
231+ ) -> _T :
232+ """Start a GenAI invocation and create a pending span entry."""
233+ return self ._start (invocation )
234+
235+ def stop (self , invocation : _T ) -> _T :
236+ """Finalize a GenAI invocation successfully and end its span."""
237+ return self ._stop (invocation )
238+
239+ def fail (self , invocation : _T , error : Error ) -> _T :
240+ """Fail a GenAI invocation and end its span with error status."""
241+ return self ._fail (invocation , error )
242+
243+ def start_llm (
244+ self ,
245+ invocation : LLMInvocation ,
246+ ) -> LLMInvocation :
247+ """Start an LLM invocation and create a pending span entry."""
248+ return self .start (invocation )
249+
250+ def stop_llm (self , invocation : LLMInvocation ) -> LLMInvocation :
251+ """Finalize an LLM invocation successfully and end its span."""
252+ return self .stop (invocation )
253+
254+ def fail_llm (
255+ self , invocation : LLMInvocation , error : Error
256+ ) -> LLMInvocation :
257+ """Fail an LLM invocation and end its span with error status."""
258+ return self .fail (invocation , error )
259+
194260 @contextmanager
195261 def llm (
196262 self , invocation : LLMInvocation | None = None
@@ -222,43 +288,17 @@ def start_agent(
222288 invocation : AgentInvocation ,
223289 ) -> AgentInvocation :
224290 """Start an agent invocation and create a pending span entry."""
225- span_name = f"{ invocation .operation_name } { invocation .agent_name } " .strip ()
226- kind = SpanKind .CLIENT if invocation .is_remote else SpanKind .INTERNAL
227- span = self ._tracer .start_span (
228- name = span_name ,
229- kind = kind ,
230- )
231- invocation .monotonic_start_s = timeit .default_timer ()
232- invocation .span = span
233- invocation .context_token = otel_context .attach (
234- set_span_in_context (span )
235- )
236- return invocation
291+ return self .start (invocation )
237292
238- def stop_agent (self , invocation : AgentInvocation ) -> AgentInvocation : # pylint: disable=no-self-use
293+ def stop_agent (self , invocation : AgentInvocation ) -> AgentInvocation :
239294 """Finalize an agent invocation successfully and end its span."""
240- if invocation .context_token is None or invocation .span is None :
241- return invocation
242-
243- span = invocation .span
244- _apply_agent_finish_attributes (span , invocation )
245- otel_context .detach (invocation .context_token )
246- span .end ()
247- return invocation
295+ return self .stop (invocation )
248296
249- def fail_agent ( # pylint: disable=no-self-use
297+ def fail_agent (
250298 self , invocation : AgentInvocation , error : Error
251299 ) -> AgentInvocation :
252300 """Fail an agent invocation and end its span with error status."""
253- if invocation .context_token is None or invocation .span is None :
254- return invocation
255-
256- span = invocation .span
257- _apply_agent_finish_attributes (span , invocation )
258- _apply_error_attributes (span , error )
259- otel_context .detach (invocation .context_token )
260- span .end ()
261- return invocation
301+ return self .fail (invocation , error )
262302
263303 @contextmanager
264304 def agent (
@@ -291,42 +331,17 @@ def start_create_agent(
291331 creation : AgentCreation ,
292332 ) -> AgentCreation :
293333 """Start an agent creation and create a pending span entry."""
294- span_name = f"{ creation .operation_name } { creation .agent_name } " .strip ()
295- span = self ._tracer .start_span (
296- name = span_name ,
297- kind = SpanKind .CLIENT ,
298- )
299- creation .monotonic_start_s = timeit .default_timer ()
300- creation .span = span
301- creation .context_token = otel_context .attach (
302- set_span_in_context (span )
303- )
304- return creation
334+ return self .start (creation )
305335
306- def stop_create_agent (self , creation : AgentCreation ) -> AgentCreation : # pylint: disable=no-self-use
336+ def stop_create_agent (self , creation : AgentCreation ) -> AgentCreation :
307337 """Finalize an agent creation successfully and end its span."""
308- if creation .context_token is None or creation .span is None :
309- return creation
310-
311- span = creation .span
312- _apply_creation_finish_attributes (span , creation )
313- otel_context .detach (creation .context_token )
314- span .end ()
315- return creation
338+ return self .stop (creation )
316339
317- def fail_create_agent ( # pylint: disable=no-self-use
340+ def fail_create_agent (
318341 self , creation : AgentCreation , error : Error
319342 ) -> AgentCreation :
320343 """Fail an agent creation and end its span with error status."""
321- if creation .context_token is None or creation .span is None :
322- return creation
323-
324- span = creation .span
325- _apply_creation_finish_attributes (span , creation )
326- _apply_error_attributes (span , error )
327- otel_context .detach (creation .context_token )
328- span .end ()
329- return creation
344+ return self .fail (creation , error )
330345
331346 @contextmanager
332347 def create_agent (
0 commit comments