|
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_error_attributes, |
84 | 88 | _apply_llm_finish_attributes, |
| 89 | + _apply_tool_call_attributes, |
| 90 | + _finish_tool_call_span, |
| 91 | + _get_tool_call_span_name, |
85 | 92 | _maybe_emit_llm_event, |
86 | 93 | ) |
87 | | -from opentelemetry.util.genai.types import Error, LLMInvocation |
| 94 | +from opentelemetry.util.genai.types import Error, LLMInvocation, ToolCall |
88 | 95 | from opentelemetry.util.genai.version import __version__ |
89 | 96 |
|
90 | 97 |
|
@@ -187,6 +194,138 @@ def fail_llm( # pylint: disable=no-self-use |
187 | 194 | span.end() |
188 | 195 | return invocation |
189 | 196 |
|
| 197 | + def start_tool_call( |
| 198 | + self, |
| 199 | + tool_call: ToolCall, |
| 200 | + ) -> ToolCall: |
| 201 | + """Start a tool call execution and create a span. |
| 202 | +
|
| 203 | + Creates an execute_tool span per span.gen_ai.execute_tool.internal spec: |
| 204 | + - Span kind: INTERNAL |
| 205 | + - Span name: "execute_tool {tool_name}" |
| 206 | + - Required attribute: gen_ai.operation.name = "execute_tool" |
| 207 | +
|
| 208 | + Args: |
| 209 | + tool_call: ToolCall instance to track |
| 210 | +
|
| 211 | + Returns: |
| 212 | + The same ToolCall with span and context_token set |
| 213 | + """ |
| 214 | + # Create span with INTERNAL kind per spec |
| 215 | + span = self._tracer.start_span( |
| 216 | + name=_get_tool_call_span_name(tool_call), |
| 217 | + kind=SpanKind.INTERNAL, |
| 218 | + ) |
| 219 | + |
| 220 | + # Apply initial attributes (but not result yet) |
| 221 | + # capture_content=False for start, only structure attributes |
| 222 | + _apply_tool_call_attributes(span, tool_call, capture_content=False) |
| 223 | + |
| 224 | + # Record monotonic start time for duration calculation |
| 225 | + tool_call.monotonic_start_s = timeit.default_timer() |
| 226 | + |
| 227 | + # Attach to context |
| 228 | + tool_call.span = span |
| 229 | + tool_call.context_token = otel_context.attach( |
| 230 | + set_span_in_context(span) |
| 231 | + ) |
| 232 | + |
| 233 | + return tool_call |
| 234 | + |
| 235 | + def stop_tool_call(self, tool_call: ToolCall) -> ToolCall: # pylint: disable=no-self-use |
| 236 | + """Finalize a tool call execution successfully. |
| 237 | +
|
| 238 | + Applies final attributes including tool_result, sets OK status, and ends span. |
| 239 | +
|
| 240 | + Args: |
| 241 | + tool_call: ToolCall instance with span to finalize |
| 242 | +
|
| 243 | + Returns: |
| 244 | + The same ToolCall |
| 245 | + """ |
| 246 | + if tool_call.context_token is None or tool_call.span is None: |
| 247 | + # TODO: Provide feedback that this invocation was not started |
| 248 | + return tool_call |
| 249 | + |
| 250 | + span = tool_call.span |
| 251 | + |
| 252 | + # Finalize span with result (capture_content=True allows result if mode permits) |
| 253 | + _finish_tool_call_span(span, tool_call, capture_content=True) |
| 254 | + |
| 255 | + # Detach context and end span |
| 256 | + otel_context.detach(tool_call.context_token) |
| 257 | + span.end() |
| 258 | + |
| 259 | + return tool_call |
| 260 | + |
| 261 | + def fail_tool_call( # pylint: disable=no-self-use |
| 262 | + self, tool_call: ToolCall, error: Error |
| 263 | + ) -> ToolCall: |
| 264 | + """Fail a tool call execution with error. |
| 265 | +
|
| 266 | + Sets error attributes, ERROR status, and ends span. |
| 267 | +
|
| 268 | + Args: |
| 269 | + tool_call: ToolCall instance with span to fail |
| 270 | + error: Error details |
| 271 | +
|
| 272 | + Returns: |
| 273 | + The same ToolCall |
| 274 | + """ |
| 275 | + if tool_call.context_token is None or tool_call.span is None: |
| 276 | + # TODO: Provide feedback that this invocation was not started |
| 277 | + return tool_call |
| 278 | + |
| 279 | + span = tool_call.span |
| 280 | + |
| 281 | + # Set error_type on tool_call so it's included in attributes |
| 282 | + tool_call.error_type = error.type.__qualname__ |
| 283 | + |
| 284 | + # Finalize span with error |
| 285 | + _finish_tool_call_span(span, tool_call, capture_content=True) |
| 286 | + |
| 287 | + # Apply additional error status with message |
| 288 | + span.set_status(Status(StatusCode.ERROR, error.message)) |
| 289 | + |
| 290 | + # Detach context and end span |
| 291 | + otel_context.detach(tool_call.context_token) |
| 292 | + span.end() |
| 293 | + |
| 294 | + return tool_call |
| 295 | + |
| 296 | + @contextmanager |
| 297 | + def tool_call( |
| 298 | + self, tool_call: ToolCall | None = None |
| 299 | + ) -> Iterator[ToolCall]: |
| 300 | + """Context manager for tool call invocations. |
| 301 | +
|
| 302 | + Only set data attributes on the tool_call object, do not modify the span or context. |
| 303 | +
|
| 304 | + Starts the span on entry. On normal exit, finalizes the tool call and ends the span. |
| 305 | + If an exception occurs inside the context, marks the span as error, ends it, and |
| 306 | + re-raises the original exception. |
| 307 | +
|
| 308 | + Example: |
| 309 | + with handler.tool_call(ToolCall(name="get_weather", arguments={"location": "Paris"})) as tc: |
| 310 | + # Execute tool logic |
| 311 | + tc.tool_result = {"temp": 20, "condition": "sunny"} |
| 312 | + """ |
| 313 | + if tool_call is None: |
| 314 | + tool_call = ToolCall( |
| 315 | + name="", |
| 316 | + arguments={}, |
| 317 | + id=None, |
| 318 | + ) |
| 319 | + self.start_tool_call(tool_call) |
| 320 | + try: |
| 321 | + yield tool_call |
| 322 | + except Exception as exc: |
| 323 | + self.fail_tool_call( |
| 324 | + tool_call, Error(message=str(exc), type=type(exc)) |
| 325 | + ) |
| 326 | + raise |
| 327 | + self.stop_tool_call(tool_call) |
| 328 | + |
190 | 329 | @contextmanager |
191 | 330 | def llm( |
192 | 331 | self, invocation: LLMInvocation | None = None |
|
0 commit comments