4848
4949from __future__ import annotations
5050
51- import timeit
5251from contextlib import contextmanager
53- from typing import Iterator , TypeVar
52+ from typing import Iterator
5453
5554from typing_extensions import deprecated
5655
57- from opentelemetry import context as otel_context
5856from opentelemetry ._logs import (
5957 LoggerProvider ,
6058 get_logger ,
6462from opentelemetry .trace import (
6563 TracerProvider ,
6664 get_tracer ,
67- set_span_in_context ,
6865)
6966from opentelemetry .util .genai .embedding_invocation import EmbeddingInvocation
7067from opentelemetry .util .genai .inference_invocation import (
7370)
7471from opentelemetry .util .genai .metrics import InvocationMetricsRecorder
7572from opentelemetry .util .genai .tool_invocation import ToolInvocation
76- from opentelemetry .util .genai .types import (
77- Error ,
78- GenAIInvocation ,
79- )
73+ from opentelemetry .util .genai .types import Error
8074from opentelemetry .util .genai .version import __version__
8175from opentelemetry .util .genai .workflow_invocation import WorkflowInvocation
8276
8377
84- def _safe_detach (invocation : GenAIInvocation ) -> None :
85- """Detach the context token if still present, as a safety net."""
86- if invocation ._context_token is not None :
87- try :
88- otel_context .detach (invocation ._context_token )
89- except Exception : # pylint: disable=broad-except
90- pass
91- try :
92- invocation .span .end ()
93- except Exception : # pylint: disable=broad-except
94- pass
95-
96-
97- _T = TypeVar ("_T" , bound = GenAIInvocation )
98-
99-
10078class TelemetryHandler :
10179 """
10280 High-level handler managing GenAI invocation lifecycles and emitting
@@ -127,40 +105,6 @@ def __init__(
127105 schema_url = schema_url ,
128106 )
129107
130- def _start (self , invocation : _T ) -> _T :
131- """Start a GenAI invocation and create a pending span entry."""
132-
133- invocation ._handler = self
134-
135- invocation .span = self ._tracer .start_span (
136- name = invocation ._span_name ,
137- kind = invocation ._span_kind ,
138- )
139-
140- # Record a monotonic start timestamp (seconds) for duration
141- # calculation using timeit.default_timer.
142- invocation ._monotonic_start_s = timeit .default_timer ()
143- invocation ._context_token = otel_context .attach (
144- set_span_in_context (invocation .span )
145- )
146-
147- return invocation
148-
149- def _finish ( # pylint: disable=no-self-use
150- self , invocation : _T , error : Error | None = None
151- ) -> _T :
152- """Finalize a GenAI invocation and end its span."""
153- if invocation ._context_token is None :
154- # TODO: Provide feedback that this invocation was not started
155- return invocation
156-
157- try :
158- invocation ._apply_finish (error )
159- finally :
160- # Detach context and end span even if finishing fails
161- _safe_detach (invocation )
162- return invocation
163-
164108 # New-style factory methods: construct + start in one call, handler stored on invocation
165109
166110 def start_inference (
@@ -177,7 +121,9 @@ def start_inference(
177121 returned invocation, then call invocation.stop() or invocation.fail().
178122 """
179123 return InferenceInvocation (
180- self ,
124+ self ._tracer ,
125+ self ._metrics_recorder ,
126+ self ._logger ,
181127 provider ,
182128 request_model = request_model ,
183129 server_address = server_address ,
@@ -194,9 +140,12 @@ def start_llm(self, invocation: LLMInvocation) -> LLMInvocation: # pyright: ign
194140 Use ``handler.start_inference()`` instead.
195141 """
196142 if invocation ._context_token is not None :
197- # Already started (e.g. via InferenceInvocation .__init__)
143+ # Already started (e.g. tracer passed to LLMInvocation .__init__)
198144 return invocation
199- return self ._start (invocation )
145+ invocation ._start_with_handler (
146+ self ._tracer , self ._metrics_recorder , self ._logger
147+ )
148+ return invocation
200149
201150 def start_embedding (
202151 self ,
@@ -212,7 +161,9 @@ def start_embedding(
212161 invocation, then call invocation.stop() or invocation.fail().
213162 """
214163 return EmbeddingInvocation (
215- self ,
164+ self ._tracer ,
165+ self ._metrics_recorder ,
166+ self ._logger ,
216167 provider ,
217168 request_model = request_model ,
218169 server_address = server_address ,
@@ -234,7 +185,9 @@ def start_tool(
234185 invocation.stop() or invocation.fail().
235186 """
236187 return ToolInvocation (
237- self ,
188+ self ._tracer ,
189+ self ._metrics_recorder ,
190+ self ._logger ,
238191 name ,
239192 arguments = arguments ,
240193 tool_call_id = tool_call_id ,
@@ -251,7 +204,9 @@ def start_workflow(
251204 Set remaining attributes on the returned invocation, then call
252205 invocation.stop() or invocation.fail().
253206 """
254- return WorkflowInvocation (self , name )
207+ return WorkflowInvocation (
208+ self ._tracer , self ._metrics_recorder , self ._logger , name
209+ )
255210
256211 @deprecated (
257212 "handler.stop_llm() is deprecated. Use invocation.stop() instead."
@@ -262,7 +217,8 @@ def stop_llm(self, invocation: LLMInvocation) -> LLMInvocation: # pyright: igno
262217 .. deprecated::
263218 Use ``invocation.stop()`` instead.
264219 """
265- return self ._finish (invocation )
220+ invocation .stop ()
221+ return invocation
266222
267223 @deprecated (
268224 "handler.fail_llm() is deprecated. Use invocation.fail(error) instead."
@@ -277,7 +233,8 @@ def fail_llm(
277233 .. deprecated::
278234 Use ``invocation.fail(error)`` instead.
279235 """
280- return self ._finish (invocation , error )
236+ invocation .fail (error )
237+ return invocation
281238
282239 @contextmanager
283240 def inference (
0 commit comments