Skip to content

Commit 25483cb

Browse files
committed
unify handler start/stop/fail
1 parent 25411ca commit 25483cb

4 files changed

Lines changed: 188 additions & 179 deletions

File tree

util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py

Lines changed: 104 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262

6363
import timeit
6464
from contextlib import contextmanager
65-
from typing import Iterator
65+
from typing import Iterator, TypeVar
6666

6767
from opentelemetry import context as otel_context
6868
from opentelemetry._logs import (
@@ -78,6 +78,7 @@
7878
get_tracer,
7979
set_span_in_context,
8080
)
81+
from opentelemetry.trace.status import Status, StatusCode
8182
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
8283
from opentelemetry.util.genai.span_utils import (
8384
_apply_agent_finish_attributes,
@@ -90,10 +91,13 @@
9091
AgentCreation,
9192
AgentInvocation,
9293
Error,
94+
GenAIInvocation,
9395
LLMInvocation,
9496
)
9597
from opentelemetry.util.genai.version import __version__
9698

99+
_T = TypeVar("_T", bound=GenAIInvocation)
100+
97101

98102
class 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

Comments
 (0)