2828Usage:
2929 handler = get_telemetry_handler()
3030
31- # Create an invocation object with your request data
32- # The span and context_token attributes are set by the TelemetryHandler, and
33- # managed by the TelemetryHandler during the lifecycle of the span.
31+ # Create an invocation object
32+ invocation = LLMInvocation(
33+ request_model="my-model",
34+ input_messages=[...],
35+ provider="my-provider"
36+ )
37+
3438
3539 # Use the context manager to manage the lifecycle of an LLM invocation.
3640 with handler.llm(invocation) as invocation:
3943 invocation.attributes.update({"more": "attrs"})
4044
4145 # Or, if you prefer to manage the lifecycle manually
42- invocation = LLMInvocation(
43- request_model="my-model",
44- input_messages=[...],
45- provider="my-provider",
46- attributes={"custom": "attr"},
47- )
48-
49- # Start the invocation (opens a span)
5046 handler.start_llm(invocation)
5147
5248 # Populate outputs and any additional attributes, then stop (closes the span)
5349 invocation.output_messages = [...]
5450 invocation.attributes.update({"more": "attrs"})
5551 handler.stop_llm(invocation)
5652
57- # Or, in case of error
58- handler.fail_llm(invocation, Error(type="...", message="..."))
53+ # Or, in case of error — pass an exception directly or an Error object
54+ handler.fail_llm(invocation, exc)
55+ handler.fail_llm(invocation, Error(type=MyError, message="..."))
5956"""
6057
6158from __future__ import annotations
6259
6360import logging
6461import timeit
6562from contextlib import contextmanager
66- from typing import Iterator , TypeVar
63+ from typing import Iterator , TypeVar , overload
6764
6865from opentelemetry import context as otel_context
6966from opentelemetry ._logs import (
104101
105102def _safe_detach (invocation : GenAIInvocation ) -> None :
106103 """Detach the context token if still present, as a safety net."""
107- if invocation .context_token is not None :
104+ if invocation ._context_token is not None :
108105 try :
109- otel_context .detach (invocation .context_token )
106+ otel_context .detach (invocation ._context_token )
110107 except Exception : # pylint: disable=broad-except
111108 pass
112109 if invocation .span is not None :
@@ -197,16 +194,16 @@ def _start(self, invocation: _T) -> _T:
197194 )
198195 # Record a monotonic start timestamp (seconds) for duration
199196 # calculation using timeit.default_timer.
200- invocation .monotonic_start_s = timeit .default_timer ()
197+ invocation ._monotonic_start_s = timeit .default_timer ()
201198 invocation .span = span
202- invocation .context_token = otel_context .attach (
199+ invocation ._context_token = otel_context .attach (
203200 set_span_in_context (span )
204201 )
205202 return invocation
206203
207204 def _stop (self , invocation : _T ) -> _T :
208205 """Finalize a GenAI invocation successfully and end its span."""
209- if invocation .context_token is None or invocation .span is None :
206+ if invocation ._context_token is None or invocation .span is None :
210207 # TODO: Provide feedback that this invocation was not started
211208 return invocation
212209
@@ -224,13 +221,13 @@ def _stop(self, invocation: _T) -> _T:
224221 # TODO: Add workflow metrics when supported
225222 finally :
226223 # Detach context and end span even if finishing fails
227- otel_context .detach (invocation .context_token )
224+ otel_context .detach (invocation ._context_token )
228225 span .end ()
229226 return invocation
230227
231228 def _fail (self , invocation : _T , error : Error ) -> _T :
232229 """Fail a GenAI invocation and end its span with error status."""
233- if invocation .context_token is None or invocation .span is None :
230+ if invocation ._context_token is None or invocation .span is None :
234231 # TODO: Provide feedback that this invocation was not started
235232 return invocation
236233
@@ -258,7 +255,7 @@ def _fail(self, invocation: _T, error: Error) -> _T:
258255 # TODO: Add workflow metrics when supported
259256 finally :
260257 # Detach context and end span even if finishing fails
261- otel_context .detach (invocation .context_token )
258+ otel_context .detach (invocation ._context_token )
262259 span .end ()
263260 return invocation
264261
@@ -273,8 +270,16 @@ def stop(self, invocation: _T) -> _T:
273270 """Finalize a GenAI invocation successfully and end its span."""
274271 return self ._stop (invocation )
275272
276- def fail (self , invocation : _T , error : Error ) -> _T :
273+ @overload
274+ def fail (self , invocation : _T , error : Error ) -> _T : ...
275+
276+ @overload
277+ def fail (self , invocation : _T , error : BaseException ) -> _T : ...
278+
279+ def fail (self , invocation : _T , error : Error | BaseException ) -> _T :
277280 """Fail a GenAI invocation and end its span with error status."""
281+ if isinstance (error , BaseException ):
282+ error = Error (type = type (error ), message = str (error ))
278283 return self ._fail (invocation , error )
279284
280285 # LLM-specific convenience methods
@@ -286,10 +291,22 @@ def stop_llm(self, invocation: LLMInvocation) -> LLMInvocation:
286291 """Finalize an LLM invocation successfully and end its span."""
287292 return self ._stop (invocation )
288293
294+ @overload
289295 def fail_llm (
290296 self , invocation : LLMInvocation , error : Error
297+ ) -> LLMInvocation : ...
298+
299+ @overload
300+ def fail_llm (
301+ self , invocation : LLMInvocation , error : BaseException
302+ ) -> LLMInvocation : ...
303+
304+ def fail_llm (
305+ self , invocation : LLMInvocation , error : Error | BaseException
291306 ) -> LLMInvocation :
292307 """Fail an LLM invocation and end its span with error status."""
308+ if isinstance (error , BaseException ):
309+ error = Error (type = type (error ), message = str (error ))
293310 return self ._fail (invocation , error )
294311
295312 @contextmanager
@@ -312,7 +329,7 @@ def llm(
312329 try :
313330 yield invocation
314331 except Exception as exc :
315- self .fail_llm (invocation , Error ( message = str ( exc ), type = type ( exc )) )
332+ self .fail_llm (invocation , exc )
316333 raise
317334 self .stop_llm (invocation )
318335
@@ -334,7 +351,7 @@ def embedding(
334351 try :
335352 yield invocation
336353 except Exception as exc :
337- self .fail (invocation , Error ( message = str ( exc ), type = type ( exc )) )
354+ self .fail (invocation , exc )
338355 raise
339356 self .stop (invocation )
340357
@@ -364,7 +381,7 @@ def workflow(
364381 yield invocation
365382 except Exception as exc :
366383 try :
367- self .fail (invocation , Error ( message = str ( exc ), type = type ( exc )) )
384+ self .fail (invocation , exc )
368385 except Exception : # pylint: disable=broad-except
369386 _logger .warning (
370387 "Failed to record workflow failure" , exc_info = True
0 commit comments