From 6df4e27d8254cbf13d586ecfd5bf18639158e01d Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 22 Oct 2026 12:27:29 +0200 Subject: [PATCH 1/2] Python: Remove span-only tool metric attributes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5e52ce1a-cfca-43c6-990b-1dcf74052845 --- .../packages/core/agent_framework/_tools.py | 9 +++-- python/packages/core/tests/core/test_tools.py | 35 +++++++++++++------ 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 068830040ff..98cf2d0709a 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -1231,8 +1231,13 @@ async def invoke( return parsed 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 + # Keep metric dimensions explicit so span-only attributes cannot flow into the histogram. + metric_attributes: dict[str, str] = { + OtelAttr.OPERATION: span_attributes[OtelAttr.OPERATION], + OtelAttr.TOOL_NAME: span_attributes[OtelAttr.TOOL_NAME], + OtelAttr.TOOL_TYPE: span_attributes[OtelAttr.TOOL_TYPE], + OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME: self.name, + } # Filter out framework kwargs that are not JSON serializable. serializable_kwargs = { k: v diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index 8dd1da6f053..0e6fa2e2d1b 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -1037,10 +1037,12 @@ def telemetry_test_tool(expression: str) -> str: call_args = mock_histogram.record.call_args assert call_args[0][0] > 0 # duration should be positive 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 attributes == { + OtelAttr.OPERATION: OtelAttr.TOOL_EXECUTION_OPERATION, + OtelAttr.TOOL_NAME: "telemetry_test_tool", + OtelAttr.TOOL_TYPE: "function", + OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME: "telemetry_test_tool", + } assert expression not in attributes.values() @@ -1085,8 +1087,12 @@ def telemetry_test_tool(x: int, y: int) -> int: call_args = mock_histogram.record.call_args assert call_args[0][0] > 0 # duration should be positive 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 attributes == { + OtelAttr.OPERATION: OtelAttr.TOOL_EXECUTION_OPERATION, + OtelAttr.TOOL_NAME: "telemetry_test_tool", + OtelAttr.TOOL_TYPE: "function", + OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME: "telemetry_test_tool", + } @pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True) @@ -1228,9 +1234,13 @@ def exception_test_tool(x: int, y: int) -> int: mock_histogram.record.assert_called_once() 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 + assert attributes == { + OtelAttr.OPERATION: OtelAttr.TOOL_EXECUTION_OPERATION, + OtelAttr.TOOL_NAME: "exception_test_tool", + OtelAttr.TOOL_TYPE: "function", + OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME: "exception_test_tool", + OtelAttr.ERROR_TYPE: ValueError.__name__, + } async def test_tool_invoke_telemetry_async_function(span_exporter: InMemorySpanExporter): @@ -1268,7 +1278,12 @@ async def async_telemetry_test(x: int, y: int) -> int: mock_histogram.record.assert_called_once() call_args = mock_histogram.record.call_args attributes = call_args[1]["attributes"] - assert attributes[OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME] == "async_telemetry_test" + assert attributes == { + OtelAttr.OPERATION: OtelAttr.TOOL_EXECUTION_OPERATION, + OtelAttr.TOOL_NAME: "async_telemetry_test", + OtelAttr.TOOL_TYPE: "function", + OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME: "async_telemetry_test", + } async def test_tool_invoke_invalid_pydantic_args(): From 4505e795816997f0bc6330c05a2df70a99eb1716 Mon Sep 17 00:00:00 2001 From: Eduard van Valkenburg Date: Thu, 22 Oct 2026 13:17:39 +0200 Subject: [PATCH 2/2] Address tool metric review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5e52ce1a-cfca-43c6-990b-1dcf74052845 --- python/packages/core/agent_framework/_tools.py | 9 +++++---- python/packages/core/tests/core/test_tools.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 98cf2d0709a..59aaf472803 100644 --- a/python/packages/core/agent_framework/_tools.py +++ b/python/packages/core/agent_framework/_tools.py @@ -47,6 +47,7 @@ from opentelemetry import trace from opentelemetry.metrics import Histogram, NoOpHistogram +from opentelemetry.util.types import AttributeValue from pydantic import BaseModel, Field, TypeAdapter, ValidationError, create_model from pydantic_core import to_jsonable_python @@ -1232,10 +1233,10 @@ async def invoke( span_attributes = get_function_span_attributes(self, tool_call_id=tool_call_id) # Keep metric dimensions explicit so span-only attributes cannot flow into the histogram. - metric_attributes: dict[str, str] = { - OtelAttr.OPERATION: span_attributes[OtelAttr.OPERATION], - OtelAttr.TOOL_NAME: span_attributes[OtelAttr.TOOL_NAME], - OtelAttr.TOOL_TYPE: span_attributes[OtelAttr.TOOL_TYPE], + metric_attributes: dict[str, AttributeValue] = { + OtelAttr.OPERATION: OtelAttr.TOOL_EXECUTION_OPERATION, + OtelAttr.TOOL_NAME: self.name, + OtelAttr.TOOL_TYPE: "function", OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME: self.name, } # Filter out framework kwargs that are not JSON serializable. diff --git a/python/packages/core/tests/core/test_tools.py b/python/packages/core/tests/core/test_tools.py index 0e6fa2e2d1b..d7db2db4971 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -1043,7 +1043,6 @@ def telemetry_test_tool(expression: str) -> str: OtelAttr.TOOL_TYPE: "function", OtelAttr.MEASUREMENT_FUNCTION_TAG_NAME: "telemetry_test_tool", } - assert expression not in attributes.values() @pytest.mark.parametrize("enable_sensitive_data", [False], indirect=True)