|
17 | 17 |
|
18 | 18 | This module exposes the `TelemetryHandler` class, which manages the lifecycle of |
19 | 19 | GenAI (Generative AI) invocations and emits telemetry data (spans and related attributes). |
20 | | -It supports starting, stopping, and failing LLM invocations. |
| 20 | +It supports starting, stopping, and failing LLM invocations and tool call executions. |
21 | 21 |
|
22 | 22 | Classes: |
23 | 23 | - TelemetryHandler: Manages GenAI invocation lifecycles and emits telemetry. |
24 | 24 |
|
25 | 25 | Functions: |
26 | 26 | - get_telemetry_handler: Returns a singleton `TelemetryHandler` instance. |
27 | 27 |
|
28 | | -Usage: |
| 28 | +Usage - LLM Invocations: |
29 | 29 | handler = get_telemetry_handler() |
30 | 30 |
|
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. |
34 | | -
|
35 | 31 | # Use the context manager to manage the lifecycle of an LLM invocation. |
36 | 32 | with handler.llm(invocation) as invocation: |
37 | 33 | # Populate outputs and any additional attributes |
|
45 | 41 | provider="my-provider", |
46 | 42 | attributes={"custom": "attr"}, |
47 | 43 | ) |
48 | | -
|
49 | | - # Start the invocation (opens a span) |
50 | 44 | handler.start_llm(invocation) |
51 | | -
|
52 | | - # Populate outputs and any additional attributes, then stop (closes the span) |
53 | 45 | invocation.output_messages = [...] |
54 | | - invocation.attributes.update({"more": "attrs"}) |
55 | 46 | handler.stop_llm(invocation) |
56 | 47 |
|
57 | | - # Or, in case of error |
58 | | - handler.fail_llm(invocation, Error(type="...", message="...")) |
| 48 | +Usage - Tool Call Executions: |
| 49 | + handler = get_telemetry_handler() |
| 50 | +
|
| 51 | + # Use the context manager to manage the lifecycle of a tool call. |
| 52 | + tool = ToolCall(name="get_weather", arguments={"location": "Paris"}, id="call_123") |
| 53 | + with handler.tool_call(tool) as tc: |
| 54 | + # Execute tool logic |
| 55 | + tc.tool_result = {"temp": 20, "condition": "sunny"} |
| 56 | +
|
| 57 | + # Or, manage the lifecycle manually |
| 58 | + tool = ToolCall(name="get_weather", arguments={"location": "Paris"}) |
| 59 | + handler.start_tool_call(tool) |
| 60 | + tool.tool_result = {"temp": 20} |
| 61 | + handler.stop_tool_call(tool) |
59 | 62 | """ |
60 | 63 |
|
61 | 64 | from __future__ import annotations |
|
78 | 81 | get_tracer, |
79 | 82 | set_span_in_context, |
80 | 83 | ) |
| 84 | +from opentelemetry.trace.status import Status, StatusCode |
81 | 85 | from opentelemetry.util.genai.metrics import InvocationMetricsRecorder |
82 | 86 | from opentelemetry.util.genai.span_utils import ( |
83 | 87 | _apply_embedding_finish_attributes, |
84 | 88 | _apply_error_attributes, |
85 | 89 | _apply_llm_finish_attributes, |
86 | 90 | _get_embedding_span_name, |
87 | 91 | _get_llm_span_name, |
| 92 | + _apply_tool_call_attributes, |
| 93 | + _finish_tool_call_span, |
| 94 | + _get_tool_call_span_name, |
88 | 95 | _maybe_emit_llm_event, |
89 | 96 | ) |
90 | 97 | from opentelemetry.util.genai.types import ( |
91 | 98 | EmbeddingInvocation, |
92 | 99 | Error, |
93 | 100 | GenAIInvocation, |
94 | 101 | LLMInvocation, |
| 102 | + ToolCall, |
95 | 103 | ) |
96 | 104 | from opentelemetry.util.genai.version import __version__ |
97 | 105 |
|
@@ -258,6 +266,138 @@ def fail_llm( |
258 | 266 | """Fail an LLM invocation and end its span with error status.""" |
259 | 267 | return self._fail(invocation, error) |
260 | 268 |
|
| 269 | + def start_tool_call( |
| 270 | + self, |
| 271 | + tool_call: ToolCall, |
| 272 | + ) -> ToolCall: |
| 273 | + """Start a tool call execution and create a span. |
| 274 | +
|
| 275 | + Creates an execute_tool span per span.gen_ai.execute_tool.internal spec: |
| 276 | + - Span kind: INTERNAL |
| 277 | + - Span name: "execute_tool {tool_name}" |
| 278 | + - Required attribute: gen_ai.operation.name = "execute_tool" |
| 279 | +
|
| 280 | + Args: |
| 281 | + tool_call: ToolCall instance to track |
| 282 | +
|
| 283 | + Returns: |
| 284 | + The same ToolCall with span and context_token set |
| 285 | + """ |
| 286 | + # Create span with INTERNAL kind per spec |
| 287 | + span = self._tracer.start_span( |
| 288 | + name=_get_tool_call_span_name(tool_call), |
| 289 | + kind=SpanKind.INTERNAL, |
| 290 | + ) |
| 291 | + |
| 292 | + # Apply initial attributes (but not result yet) |
| 293 | + # capture_content=False for start, only structure attributes |
| 294 | + _apply_tool_call_attributes(span, tool_call, capture_content=False) |
| 295 | + |
| 296 | + # Record monotonic start time for duration calculation |
| 297 | + tool_call.monotonic_start_s = timeit.default_timer() |
| 298 | + |
| 299 | + # Attach to context |
| 300 | + tool_call.span = span |
| 301 | + tool_call.context_token = otel_context.attach( |
| 302 | + set_span_in_context(span) |
| 303 | + ) |
| 304 | + |
| 305 | + return tool_call |
| 306 | + |
| 307 | + def stop_tool_call(self, tool_call: ToolCall) -> ToolCall: # pylint: disable=no-self-use |
| 308 | + """Finalize a tool call execution successfully. |
| 309 | +
|
| 310 | + Applies final attributes including tool_result, sets OK status, and ends span. |
| 311 | +
|
| 312 | + Args: |
| 313 | + tool_call: ToolCall instance with span to finalize |
| 314 | +
|
| 315 | + Returns: |
| 316 | + The same ToolCall |
| 317 | + """ |
| 318 | + if tool_call.context_token is None or tool_call.span is None: |
| 319 | + # TODO: Provide feedback that this invocation was not started |
| 320 | + return tool_call |
| 321 | + |
| 322 | + span = tool_call.span |
| 323 | + |
| 324 | + # Finalize span with result (capture_content=True allows result if mode permits) |
| 325 | + _finish_tool_call_span(span, tool_call, capture_content=True) |
| 326 | + |
| 327 | + # Detach context and end span |
| 328 | + otel_context.detach(tool_call.context_token) |
| 329 | + span.end() |
| 330 | + |
| 331 | + return tool_call |
| 332 | + |
| 333 | + def fail_tool_call( # pylint: disable=no-self-use |
| 334 | + self, tool_call: ToolCall, error: Error |
| 335 | + ) -> ToolCall: |
| 336 | + """Fail a tool call execution with error. |
| 337 | +
|
| 338 | + Sets error attributes, ERROR status, and ends span. |
| 339 | +
|
| 340 | + Args: |
| 341 | + tool_call: ToolCall instance with span to fail |
| 342 | + error: Error details |
| 343 | +
|
| 344 | + Returns: |
| 345 | + The same ToolCall |
| 346 | + """ |
| 347 | + if tool_call.context_token is None or tool_call.span is None: |
| 348 | + # TODO: Provide feedback that this invocation was not started |
| 349 | + return tool_call |
| 350 | + |
| 351 | + span = tool_call.span |
| 352 | + |
| 353 | + # Set error_type on tool_call so it's included in attributes |
| 354 | + tool_call.error_type = error.type.__qualname__ |
| 355 | + |
| 356 | + # Finalize span with error |
| 357 | + _finish_tool_call_span(span, tool_call, capture_content=True) |
| 358 | + |
| 359 | + # Apply additional error status with message |
| 360 | + span.set_status(Status(StatusCode.ERROR, error.message)) |
| 361 | + |
| 362 | + # Detach context and end span |
| 363 | + otel_context.detach(tool_call.context_token) |
| 364 | + span.end() |
| 365 | + |
| 366 | + return tool_call |
| 367 | + |
| 368 | + @contextmanager |
| 369 | + def tool_call( |
| 370 | + self, tool_call: ToolCall | None = None |
| 371 | + ) -> Iterator[ToolCall]: |
| 372 | + """Context manager for tool call invocations. |
| 373 | +
|
| 374 | + Only set data attributes on the tool_call object, do not modify the span or context. |
| 375 | +
|
| 376 | + Starts the span on entry. On normal exit, finalizes the tool call and ends the span. |
| 377 | + If an exception occurs inside the context, marks the span as error, ends it, and |
| 378 | + re-raises the original exception. |
| 379 | +
|
| 380 | + Example: |
| 381 | + with handler.tool_call(ToolCall(name="get_weather", arguments={"location": "Paris"})) as tc: |
| 382 | + # Execute tool logic |
| 383 | + tc.tool_result = {"temp": 20, "condition": "sunny"} |
| 384 | + """ |
| 385 | + if tool_call is None: |
| 386 | + tool_call = ToolCall( |
| 387 | + name="", |
| 388 | + arguments={}, |
| 389 | + id=None, |
| 390 | + ) |
| 391 | + self.start_tool_call(tool_call) |
| 392 | + try: |
| 393 | + yield tool_call |
| 394 | + except Exception as exc: |
| 395 | + self.fail_tool_call( |
| 396 | + tool_call, Error(message=str(exc), type=type(exc)) |
| 397 | + ) |
| 398 | + raise |
| 399 | + self.stop_tool_call(tool_call) |
| 400 | + |
261 | 401 | @contextmanager |
262 | 402 | def llm( |
263 | 403 | self, invocation: LLMInvocation | None = None |
|
0 commit comments