From 3c6058ecc884ea053c9d8771aea10ec1107abaad Mon Sep 17 00:00:00 2001 From: eavanvalkenburg Date: Wed, 14 Oct 2026 09:27:36 +0200 Subject: [PATCH] Python: Keep tool arguments off invocation metrics --- .../packages/core/agent_framework/_tools.py | 15 +++++++------- python/packages/core/tests/core/test_tools.py | 20 ++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index eab23d5699f..e5ec4bbae37 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -1230,7 +1230,9 @@ async def invoke( logger.debug("Function result: None") return parsed - attributes = get_function_span_attributes(self, tool_call_id=tool_call_id) + span_attributes = get_function_span_attributes(self, tool_call_id=tool_call_id) + metric_attributes = dict(span_attributes) + metric_attributes[OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME] = self.name # Filter out framework kwargs that are not JSON serializable. serializable_kwargs = { k: v @@ -1250,13 +1252,12 @@ async def invoke( # as span attributes when that semconv version is active. emit_tool_call_attrs = OBSERVABILITY_SETTINGS.emit_tool_call_attributes if emit_tool_call_attrs: - attributes.update({ + span_attributes.update({ OtelAttr.TOOL_ARGUMENTS: ( json.dumps(serializable_kwargs, default=str, ensure_ascii=False) if serializable_kwargs else "None" ) }) - with get_function_span(attributes=attributes) as span: - attributes[OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME] = self.name + with get_function_span(attributes=span_attributes) as span: logger.info(f"Function name: {self.name}") if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED: logger.debug(f"Function arguments: {serializable_kwargs}") @@ -1267,7 +1268,7 @@ async def invoke( end_time_stamp = perf_counter() except Exception as exception: end_time_stamp = perf_counter() - attributes[OtelAttr.ERROR_TYPE] = type(exception).__name__ + metric_attributes[OtelAttr.ERROR_TYPE] = type(exception).__name__ capture_exception(span=span, exception=exception, timestamp=time_ns()) logger.error(f"Function failed. Error: {exception}") raise @@ -1294,7 +1295,7 @@ async def invoke( parsed = configured_parser(result) except Exception as exception: self.invocation_exception_count += 1 - attributes[OtelAttr.ERROR_TYPE] = type(exception).__name__ + metric_attributes[OtelAttr.ERROR_TYPE] = type(exception).__name__ if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED: capture_exception(span=span, exception=exception, timestamp=time_ns()) logger.error(f"Function {self.name}: result parser failed. Error: {exception}") @@ -1321,7 +1322,7 @@ async def invoke( finally: duration = (end_time_stamp or perf_counter()) - start_time_stamp span.set_attribute(OtelAttr.MEASUREMENT_FUNCTION_INVOCATION_DURATION, duration) - self._invocation_duration_histogram.record(duration, attributes=attributes) + self._invocation_duration_histogram.record(duration, attributes=metric_attributes) logger.info("Function duration: %fs", duration) @property diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index c75d3ec88a5..f0ebb0a1889 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -1003,20 +1003,21 @@ async def test_tool_invoke_telemetry_enabled(span_exporter: InMemorySpanExporter name="telemetry_test_tool", description="A test tool for telemetry", ) - def telemetry_test_tool(x: int, y: int) -> int: - """A function that adds two numbers for telemetry testing.""" - return x + y + def telemetry_test_tool(expression: str) -> str: + """A function that echoes an expression for telemetry testing.""" + return expression # Mock the histogram mock_histogram = Mock() telemetry_test_tool._invocation_duration_histogram = mock_histogram span_exporter.clear() + expression = "(12345 + 67890) / 3" # Call invoke - result = await telemetry_test_tool.invoke(x=1, y=2, tool_call_id="test_call_id") + result = await telemetry_test_tool.invoke(expression=expression, tool_call_id="test_call_id") # Verify result assert isinstance(result, list) - assert result[0].text == "3" + assert result[0].text == expression # Verify telemetry calls spans = span_exporter.get_finished_spans() @@ -1028,8 +1029,8 @@ def telemetry_test_tool(x: int, y: int) -> int: assert span.attributes[OtelAttr.TOOL_CALL_ID] == "test_call_id" # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable] assert span.attributes[OtelAttr.TOOL_TYPE] == "function" # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable] assert span.attributes[OtelAttr.TOOL_DESCRIPTION] == "A test tool for telemetry" # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable] - assert span.attributes[OtelAttr.TOOL_ARGUMENTS] == '{"x": 1, "y": 2}' # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable] - assert span.attributes[OtelAttr.TOOL_RESULT] == "3" # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable] + assert span.attributes[OtelAttr.TOOL_ARGUMENTS] == '{"expression": "(12345 + 67890) / 3"}' # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable] + assert span.attributes[OtelAttr.TOOL_RESULT] == expression # type: ignore[index] # pyrefly: ignore[unsupported-operation] # ty: ignore[not-subscriptable] # Verify histogram was called with correct attributes mock_histogram.record.assert_called_once() @@ -1038,6 +1039,9 @@ def telemetry_test_tool(x: int, y: int) -> int: attributes = call_args[1]["attributes"] assert attributes[OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME] == "telemetry_test_tool" assert attributes[OtelAttr.TOOL_CALL_ID] == "test_call_id" + assert OtelAttr.TOOL_ARGUMENTS not in attributes + assert OtelAttr.TOOL_RESULT not in attributes + assert expression not in attributes.values() @pytest.mark.parametrize("enable_sensitive_data", [False], indirect=True) @@ -1206,6 +1210,8 @@ def exception_test_tool(x: int, y: int) -> int: call_args = mock_histogram.record.call_args attributes = call_args[1]["attributes"] assert attributes[OtelAttr.ERROR_TYPE] == ValueError.__name__ + assert OtelAttr.TOOL_ARGUMENTS not in attributes + assert OtelAttr.TOOL_RESULT not in attributes async def test_tool_invoke_telemetry_async_function(span_exporter: InMemorySpanExporter):