From fe0712ba41ef7bd3cd14f43858afc69b502a9822 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Mon, 20 Jul 2026 17:50:04 +0000 Subject: [PATCH 1/5] Minor changes to tools --- .../genai/langchain/callback_handler.py | 35 ++++++++++++------ .../genai/openai_agents/processor.py | 7 +++- .../google_genai/tool_call_wrapper.py | 14 +++---- .../util/genai/_tool_invocation.py | 37 +++++-------------- .../tests/test_toolcall.py | 21 +---------- 5 files changed, 47 insertions(+), 67 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py index 1ef791053..0d4e735cf 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py @@ -40,6 +40,7 @@ Text, ToolCallRequest, ) +from opentelemetry.util.genai.utils import gen_ai_json_dumps class OpenTelemetryLangChainCallbackHandler(BaseCallbackHandler): @@ -431,19 +432,25 @@ def on_tool_start( if serialized is not None: name = serialized.get("name") or "unknown" description = serialized.get("description") - - arguments: Any - if inputs is not None: - arguments = inputs - else: - try: - arguments = json.loads(input_str) - except (json.JSONDecodeError, ValueError): - arguments = input_str tool_invocation = self._telemetry_handler.tool( name=name, tool_description=description, tool_type="function" ) - tool_invocation.arguments = arguments + if self._telemetry_handler.should_capture_content(): + arguments: Any + if inputs is not None: + arguments = inputs + else: + try: + arguments = json.loads(input_str) + except (json.JSONDecodeError, ValueError): + arguments = input_str + + tool_invocation.arguments = ( + arguments + if isinstance(arguments, str) + else gen_ai_json_dumps(arguments) + ) + self._invocation_manager.add_invocation_state( run_id, parent_run_id, tool_invocation ) @@ -460,7 +467,13 @@ def on_tool_end( if not isinstance(tool_invocation, ToolInvocation): return tool_invocation.tool_call_id = getattr(output, "tool_call_id", None) - tool_invocation.tool_result = getattr(output, "content", None) + raw_result = getattr(output, "content", output) + if self._telemetry_handler.should_capture_content(): + tool_invocation.tool_result = ( + raw_result + if isinstance(raw_result, str) + else gen_ai_json_dumps(raw_result) + ) tool_invocation.stop() if not tool_invocation.span.is_recording(): self._invocation_manager.delete_invocation_state(run_id=run_id) diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai-agents/src/opentelemetry/instrumentation/genai/openai_agents/processor.py b/instrumentation/opentelemetry-instrumentation-genai-openai-agents/src/opentelemetry/instrumentation/genai/openai_agents/processor.py index 2e3dd977b..27f8f603a 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai-agents/src/opentelemetry/instrumentation/genai/openai_agents/processor.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai-agents/src/opentelemetry/instrumentation/genai/openai_agents/processor.py @@ -49,6 +49,7 @@ GenAIInvocation, ToolInvocation, ) +from opentelemetry.util.genai.utils import gen_ai_json_dumps # Non-semconv attribute: surfaces the workflow name on the workflow span # so callers can query/filter by it. util-genai's WorkflowInvocation @@ -126,9 +127,11 @@ def on_span_end(self, span: Span[Any]) -> None: span.span_data, FunctionSpanData ): output = span.span_data.output - if output is not None: + if self._handler.should_capture_content() and output is not None: invocation.tool_result = ( - output if isinstance(output, str) else str(output) + output + if isinstance(output, str) + else gen_ai_json_dumps(output) ) invocation.stop() diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py index 322f0f041..d8250b847 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py @@ -3,7 +3,6 @@ import functools import inspect -import json from typing import Any, Callable, Optional, Union from google.genai.types import ( @@ -13,6 +12,7 @@ ) from opentelemetry.util.genai.handler import TelemetryHandler +from opentelemetry.util.genai.utils import gen_ai_json_dumps ToolFunction = Callable[..., Any] @@ -83,13 +83,13 @@ async def wrapped_function(*args, **kwargs): tool_description=tool_function.__doc__, ) as tool_invocation: # Do this before calling the tool in case that crashes. - if tool_invocation.should_capture_content_on_span: - tool_invocation.arguments = json.dumps( + if telemetry_handler.should_capture_content(): + tool_invocation.arguments = gen_ai_json_dumps( _get_function_args(tool_function, args, kwargs) ) result = await tool_function(*args, **kwargs) - if tool_invocation.should_capture_content_on_span: - tool_invocation.tool_result = json.dumps( + if telemetry_handler.should_capture_content(): + tool_invocation.tool_result = gen_ai_json_dumps( _to_otel_value(result) ) return result @@ -103,12 +103,12 @@ def wrapped_function(*args, **kwargs): ) as tool_invocation: # Do this before calling the tool in case that crashes. if tool_invocation.should_capture_content_on_span: - tool_invocation.arguments = json.dumps( + tool_invocation.arguments = gen_ai_json_dumps( _get_function_args(tool_function, args, kwargs) ) result = tool_function(*args, **kwargs) if tool_invocation.should_capture_content_on_span: - tool_invocation.tool_result = json.dumps( + tool_invocation.tool_result = gen_ai_json_dumps( _to_otel_value(result) ) return result diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py index 6af59b3ca..a4bfd2624 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py @@ -11,23 +11,8 @@ from opentelemetry.util.genai._invocation import Error, GenAIInvocation from opentelemetry.util.genai.completion_hook import CompletionHook from opentelemetry.util.genai.metrics import InvocationMetricsRecorder -from opentelemetry.util.genai.utils import ( - gen_ai_json_dumps, - should_capture_content_on_spans, -) -from opentelemetry.util.types import AnyValue, AttributeValue - - -def _any_value_to_attribute_value(value: AnyValue) -> AttributeValue | None: - """Serialize an AnyValue to an AttributeValue for OTel span attributes.""" - if value is None: - return None - if isinstance(value, (bool, str, bytes, int, float)): - return value - try: - return gen_ai_json_dumps(value) - except (TypeError, ValueError): - return str(value) +from opentelemetry.util.genai.utils import should_capture_content_on_spans +from opentelemetry.util.types import AnyValue class ToolInvocation(GenAIInvocation): @@ -75,18 +60,18 @@ def __init__( ) self.should_capture_content_on_span = should_capture_content_on_spans() self.name = name - self.tool_result: AnyValue | None = None + self.tool_result: AnyValue = None # Since arguments and tool_result can be expensive to serialize, # it's recommended to check the content capture flag in the # instrumentation library before assigning these attributes # to the invocation. - self.arguments: AnyValue | None = None + self.arguments: AnyValue = None self.tool_call_id = tool_call_id self.tool_type = tool_type self.tool_description = tool_description self._start(self._get_base_attributes()) - def _get_base_attributes(self) -> dict[str, AttributeValue]: + def _get_base_attributes(self) -> dict[str, AnyValue]: """Return sampling-relevant attributes available at span creation time.""" optional_attrs = ( (GenAI.GEN_AI_TOOL_NAME, self.name), @@ -99,8 +84,8 @@ def _get_base_attributes(self) -> dict[str, AttributeValue]: **{k: v for k, v in optional_attrs if v is not None}, } - def _get_metric_attributes(self) -> dict[str, AttributeValue]: - attrs: dict[str, AttributeValue] = { + def _get_metric_attributes(self) -> dict[str, AnyValue]: + attrs: dict[str, AnyValue] = { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, } attrs.update(self.metric_attributes) @@ -116,20 +101,18 @@ def _apply_finish(self, error: Error | None = None) -> None: (GenAI.GEN_AI_TOOL_DESCRIPTION, self.tool_description), ( GenAI.GEN_AI_TOOL_CALL_ARGUMENTS, - _any_value_to_attribute_value(self.arguments) + self.arguments if self.should_capture_content_on_span - and self.arguments is not None else None, ), ( GenAI.GEN_AI_TOOL_CALL_RESULT, - _any_value_to_attribute_value(self.tool_result) + self.tool_result if self.should_capture_content_on_span - and self.tool_result is not None else None, ), ) - attributes: dict[str, AttributeValue] = { + attributes: dict[str, AnyValue] = { GenAI.GEN_AI_OPERATION_NAME: self._operation_name, **{k: v for k, v in optional_attrs if v is not None}, } diff --git a/util/opentelemetry-util-genai/tests/test_toolcall.py b/util/opentelemetry-util-genai/tests/test_toolcall.py index 6277b9681..be095860a 100644 --- a/util/opentelemetry-util-genai/tests/test_toolcall.py +++ b/util/opentelemetry-util-genai/tests/test_toolcall.py @@ -29,7 +29,6 @@ ServerToolCallResponse, ToolCallRequest, ) -from opentelemetry.util.genai.utils import gen_ai_json_dumps def _make_handler() -> TelemetryHandler: @@ -208,24 +207,6 @@ def _make_span_exporter_and_handler(): return span_exporter, handler -@patch.dict( - os.environ, - {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "SPAN_ONLY"}, -) -def test_arguments_dict_serialized_to_json(): - """dict arguments are JSON-serialized onto the span attribute.""" - span_exporter, handler = _make_span_exporter_and_handler() - invocation = handler.tool("get_weather") - invocation.arguments = {"location": "Paris", "unit": "celsius"} - invocation.stop() - - attrs = span_exporter.get_finished_spans()[0].attributes - assert GenAI.GEN_AI_TOOL_CALL_ARGUMENTS in attrs - assert attrs[GenAI.GEN_AI_TOOL_CALL_ARGUMENTS] == gen_ai_json_dumps( - {"location": "Paris", "unit": "celsius"} - ) - - @patch.dict( os.environ, {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "SPAN_ONLY"}, @@ -275,7 +256,7 @@ def test_arguments_omitted_when_content_capture_disabled(): """arguments must not appear on the span when content capture is off.""" span_exporter, handler = _make_span_exporter_and_handler() invocation = handler.tool("get_weather") - invocation.arguments = {"location": "Paris"} + invocation.arguments = "abasfasf" invocation.stop() attrs = span_exporter.get_finished_spans()[0].attributes From b3a268286a4901672e48cbae10933f1cbaf42f58 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Mon, 20 Jul 2026 17:55:09 +0000 Subject: [PATCH 2/5] Cleanup --- .../instrumentation/genai/langchain/callback_handler.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py index 0d4e735cf..0c0af3c44 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py @@ -440,11 +440,8 @@ def on_tool_start( if inputs is not None: arguments = inputs else: - try: - arguments = json.loads(input_str) - except (json.JSONDecodeError, ValueError): - arguments = input_str - + arguments = input_str + tool_invocation.arguments = ( arguments if isinstance(arguments, str) From 8f3b1893b3886d7720d3a537e2aa44cb6a6166fe Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Mon, 20 Jul 2026 18:07:12 +0000 Subject: [PATCH 3/5] Revert small thing --- .../instrumentation/google_genai/tool_call_wrapper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py index d8250b847..38f9a0f73 100644 --- a/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py +++ b/instrumentation/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py @@ -83,12 +83,12 @@ async def wrapped_function(*args, **kwargs): tool_description=tool_function.__doc__, ) as tool_invocation: # Do this before calling the tool in case that crashes. - if telemetry_handler.should_capture_content(): + if tool_invocation.should_capture_content_on_span: tool_invocation.arguments = gen_ai_json_dumps( _get_function_args(tool_function, args, kwargs) ) result = await tool_function(*args, **kwargs) - if telemetry_handler.should_capture_content(): + if tool_invocation.should_capture_content_on_span: tool_invocation.tool_result = gen_ai_json_dumps( _to_otel_value(result) ) From b6c453301faa371ebb26d1e6ebe3071a08faf854 Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Mon, 20 Jul 2026 18:10:27 +0000 Subject: [PATCH 4/5] Changelog and precommit --- .../.changelog/292.changed | 1 + .../instrumentation/genai/langchain/callback_handler.py | 3 +-- .../.changelog/292.changed | 0 util/opentelemetry-util-genai/.changelog/292.changed | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/292.changed create mode 100644 instrumentation/opentelemetry-instrumentation-google-genai/.changelog/292.changed create mode 100644 util/opentelemetry-util-genai/.changelog/292.changed diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/292.changed b/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/292.changed new file mode 100644 index 000000000..c7d03f45f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/.changelog/292.changed @@ -0,0 +1 @@ +Avoid redundant JSON encoding of tool arguments. diff --git a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py index 0c0af3c44..3aff26b6c 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py +++ b/instrumentation/opentelemetry-instrumentation-genai-langchain/src/opentelemetry/instrumentation/genai/langchain/callback_handler.py @@ -3,7 +3,6 @@ from __future__ import annotations -import json from typing import Any, Optional, cast from uuid import UUID @@ -441,7 +440,7 @@ def on_tool_start( arguments = inputs else: arguments = input_str - + tool_invocation.arguments = ( arguments if isinstance(arguments, str) diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/292.changed b/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/292.changed new file mode 100644 index 000000000..e69de29bb diff --git a/util/opentelemetry-util-genai/.changelog/292.changed b/util/opentelemetry-util-genai/.changelog/292.changed new file mode 100644 index 000000000..bb04d3a89 --- /dev/null +++ b/util/opentelemetry-util-genai/.changelog/292.changed @@ -0,0 +1 @@ +Update ``ToolInvocation`` arguments and results to accept ``AnyValue`` attribute types directly. From dabd5979073a43c5441b69e98efb968ee49d407b Mon Sep 17 00:00:00 2001 From: Dylan Russell Date: Mon, 20 Jul 2026 18:12:24 +0000 Subject: [PATCH 5/5] Remove empty file --- .../.changelog/292.changed | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 instrumentation/opentelemetry-instrumentation-google-genai/.changelog/292.changed diff --git a/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/292.changed b/instrumentation/opentelemetry-instrumentation-google-genai/.changelog/292.changed deleted file mode 100644 index e69de29bb..000000000