diff --git a/python/packages/core/agent_framework/_tools.py b/python/packages/core/agent_framework/_tools.py index 068830040ff..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 @@ -1231,8 +1232,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, 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. 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..d7db2db4971 100644 --- a/python/packages/core/tests/core/test_tools.py +++ b/python/packages/core/tests/core/test_tools.py @@ -1037,11 +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 expression not in attributes.values() + 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", [False], indirect=True) @@ -1085,8 +1086,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 +1233,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 +1277,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():