feat(telemetry): emit execute_tool spans for tool calls#1430
Conversation
|
Two follow-ups from this work:
|
Add OpenTelemetry tracing coverage for tool calls. A new ToolTracingPlugin
emits one `execute_tool {name}` span per tool invocation, opening it on the
tool_pre_invoke hook and closing it on tool_post_invoke, following the OTel
Gen-AI tool-execution semantic convention.
- ToolTracingPlugin opens the span on tool_pre_invoke and closes it (success
or error) on tool_post_invoke, correlated via a new tool_invocation_id;
span helpers route through the shared application-span core.
- Capture the provider tool-call id on ModelToolCall where available, emitted
as gen_ai.tool.call.id.
- Extract the content gate + truncation into get_capture_content_value, shared
by the action response and tool argument/result attributes.
- Rename mellea.tool.calls metric label `tool` -> `gen_ai.tool.name` to match
the span attribute (breaking; no shim).
- Rename _tracing_setters -> _tracing_helpers (private) now that it holds
non-setter helpers.
Closes generative-computing#1049
Assisted-by: Claude Code
Signed-off-by: Alex Bozarth <ajbozart@us.ibm.com>
1923775 to
524bd5f
Compare
AngeloDanducci
left a comment
There was a problem hiding this comment.
Overall looks good, may be worth adding a test for an edge case that asserts no execute_tool span nests under the action span.
The integration test asserts tool spans parent under session, but doesn't negatively assert they are not children of action — the specific parentage claim in the PR description and docs ("not nested inside the action that requested them").
If a refactor re-parents tool spans under action the current assertions would still pass (session is an ancestor of action). A boom_span.parent.span_id != action_span.context.span_id assertion would lock the intended shape.
Scope: test/telemetry/test_tracing_tools.py — add the action span to _spans_by_name lookup and one negative-parentage assertion.
This is actually a bit intentional, you would normally expect it to nest there, but that's not how our code path works, so I would say if we changed it in the future it would be improving it. The nesting test is to check that the attach to context part of tracing is working correctly. The note in the docs is actually to clarify that it doesn't nest the intuitive way (tool under action) rather than an actual rule |
|
cc @jakelorocco if you want to double check that the non-telemetry addition of |
d024969
Pull Request
Issue
Fixes #1049
Description
Adds OpenTelemetry tracing coverage for tool calls. A new
ToolTracingPluginemits oneexecute_tool {name}span per tool invocation via the existingtool_pre_invoke/tool_post_invokehooks, following the OTel Gen-AI tool-execution semantic convention.What it adds
ToolTracingPlugin(mellea/telemetry/tracing_plugins.py) — opens the span ontool_pre_invoke, closes it (success/error) ontool_post_invoke, correlated via a newtool_invocation_id. Registered alongside the Backend, Component, and Streaming tracing plugins. Hooks run SEQUENTIAL (the OTel context token must detach on the same task).execute_toolspan attributes:gen_ai.operation.name,gen_ai.tool.name(required),gen_ai.tool.type,gen_ai.tool.description,gen_ai.tool.call.id— semconv request-side attrsgen_ai.tool.call.arguments/gen_ai.tool.call.result— content-gated (MELLEA_TRACES_CONTENT), truncated to 500 charsmellea.tool.status,mellea.tool.execution_time_ms,mellea.tool.is_control_flow,mellea.tool.arguments_hash— Mellea extensionstracing.py(start_tool_span/finish_tool_span_*) routed through the shared_start_application_span/_finish_application_span_*core, mirroring theactionspan. Object-unpacking lives inget_tool_call_attrs.ModelToolCallgains an optionaltool_call_id, populated from the provider response where available (OpenAI-compatible, LiteLLM, Ollama, Watsonx);Nonefor raw-string parsing.get_capture_content_valuehelper now handles the gate + 500-char truncation formellea.response,gen_ai.tool.call.arguments, andgen_ai.tool.call.result(previously duplicated inline).execute_toolspan toobservability/tracing.md(with a restructure of the Application spans section) and updated the tool counter's label inobservability/metrics.md.Parenting
Tool execution happens after the generating
action/chatcompletes, soexecute_toolspans are not nested inside the requestingaction— they are siblings under the long-livedsessionspan (or root spans outside a session). Verified end-to-end in the integration test.Breaking changes
mellea.tool.callsnow tags the tool name undergen_ai.tool.name(wastool), aligning the metric with the new span attribute. No deprecation shim, as telemetry is still early enough that dashboards are unlikely to depend on the old label.mellea/telemetry/_tracing_setters.py→_tracing_helpers.py(private module; not a public API change). It now holds non-setter helpers too (content_capture_enabled,get_capture_content_value,get_tool_call_attrs).Testing
Test coverage:
test_tracing_plugins.py, mock tracer) — fullToolTracingPluginlifecycle: pre/post, success/error, content gating on/off, no-op when disabled, skip-without-id, missing optional attrs.test_tracing_helpers.py) —get_tool_call_attrsunpacking + hash stability + arg gating;get_capture_content_valuetruncation boundary.test_tracing_tools.py, real OTel SDK + in-memory exporter) — onem.transform()with two tool calls (one raises), only inference faked: asserts bothexecute_toolspans emit, per-call success/error status, and parenting under the realsessionspan.test_metrics_operational.pyfor thegen_ai.tool.namelabel andtest_all_payloads.pyfor the newtool_invocation_idpayload field.Attribution
Assisted-by: Claude Code
Adding a new component, requirement, sampling strategy, or tool?