[opentelemetry-instrumentation-genai-openai-agents] Record gen_ai.tool.call.arguments on execute_tool spans - #588
Conversation
|
|
Pull request dashboard statusMerged · refreshed 2026-09-03 23:16 UTC Status above doesn't look right?
|
…tool.call.arguments` on execute_tool spans The processor read `FunctionSpanData.input` in `on_span_start`, but the agents library assigns it inside the `with function_span(...)` block, so the read always saw `None`. Read it in `on_span_end` instead, next to the existing `output` read, and deserialize it to an object. Assisted-by: Claude Opus 5
8b0f2f9 to
2e1dd59
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The processor currently performs JSON parsing / result assignment even when span content capture is disabled, creating avoidable overhead on every tool invocation.
Pull request overview
This PR updates the OpenAI Agents GenAI instrumentation to reliably record gen_ai.tool.call.arguments on execute_tool spans by reading tool input at span end (when the agents library has populated it) and normalizing JSON-object argument strings to a consistent representation.
Changes:
- Move tool input (
FunctionSpanData.input) handling fromon_span_starttoon_span_endto match the agents library’s lifecycle. - Normalize tool argument values by JSON-deserializing object-shaped inputs (while preserving non-object / non-JSON inputs as strings).
- Add/adjust unit and VCR-backed integration tests validating span-end capture and argument-shape behavior.
File summaries
| File | Description |
|---|---|
| instrumentation/opentelemetry-instrumentation-genai-openai-agents/src/opentelemetry/instrumentation/genai/openai_agents/processor.py | Reads tool input at span end and normalizes JSON-object argument strings before assigning to ToolInvocation.arguments. |
| instrumentation/opentelemetry-instrumentation-genai-openai-agents/tests/test_processor.py | Updates existing unit test to reflect span-end population and adds new tests for capture timing and argument-shape normalization. |
| instrumentation/opentelemetry-instrumentation-genai-openai-agents/tests/test_instrumentor.py | Adds a VCR-backed runner test asserting gen_ai.tool.call.arguments and result are exported on the execute_tool span. |
| instrumentation/opentelemetry-instrumentation-genai-openai-agents/tests/cassettes/test_runner_records_tool_call_arguments.yaml | Adds cassette data for the new integration test. |
| instrumentation/opentelemetry-instrumentation-genai-openai-agents/.changelog/588.fixed | Adds a changelog fragment documenting the fix. |
Review details
Suppressed comments (1)
instrumentation/opentelemetry-instrumentation-genai-openai-agents/src/opentelemetry/instrumentation/genai/openai_agents/processor.py:148
on_span_enddeserializes tool arguments and assignstool_resulteven when content capture on spans is disabled, which adds avoidable overhead (JSON parsing/stringification) for every tool call. SinceToolInvocationalready snapshotsshould_capture_content_on_span, gate the expensive work on that flag.
arguments = span.span_data.input
if arguments:
invocation.arguments = _tool_arguments(arguments)
output = span.span_data.output
if output is not None:
invocation.tool_result = (
output if isinstance(output, str) else str(output)
)
- Files reviewed: 5/5 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
util-genai recommends checking the flag in the instrumentation before assigning `arguments` and `tool_result`, since both can be expensive to serialize. `_apply_finish` checks it again before writing the attributes, so the exported spans are unchanged. Assisted-by: Claude Opus 5
| if ( | ||
| isinstance(invocation, ToolInvocation) | ||
| and isinstance(span.span_data, FunctionSpanData) | ||
| and invocation.should_capture_content_on_span |
There was a problem hiding this comment.
JFYI - we should probably switch to more generic check, but let's get this one in and I'll update #593
Description
execute_toolspans never carriedgen_ai.tool.call.arguments, even withcontent capture enabled. The processor read
FunctionSpanData.inputinon_span_start, but the agents library assigns it inside thewith function_span(...)block, so the read always sawNone. This reads it inon_span_endinstead, next to the existingoutputread, and deserializes itto the object semconv specifies.
inputstaysNonewhen a run setsRunConfig.trace_include_sensitive_data=False,so the attribute is absent for those runs. A string that does not parse to a
JSON object is recorded as-is.
Type of change
How has this been tested?
New
test_runner_records_tool_call_argumentsdrivesRunner.runagainst acassette and asserts the attribute on the exported span. New parametrized
test_tool_arguments_value_shapescovers the argument string shapes providersproduce, plus the
Noneand empty cases. Three existing tests pre-setFunctionSpanData.inputat construction, which the real SDK never does andwhich is why they passed while the attribute was missing; they now assign it
after
on_span_start.openai-agentssuite: 30 passed at the declared floor(
openai-agents==0.3.3) and at latest (openai-agents==0.22.0)py314-test-instrumentation-genai-openai_agents-conformance: 1 passedtox -e precommit(ruff, ruff-format, rstcheck) andpyrightcleanChecklist