Skip to content

Commit 84a1367

Browse files
committed
toolinvocation metrics and tests
1 parent 34bfc28 commit 84a1367

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

util/opentelemetry-util-genai/src/opentelemetry/util/genai/_tool_invocation.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from opentelemetry.semconv._incubating.attributes import (
2121
gen_ai_attributes as GenAI,
2222
)
23+
from opentelemetry.semconv.attributes import server_attributes
2324
from opentelemetry.trace import Tracer
2425
from opentelemetry.util.genai._invocation import Error, GenAIInvocation
2526
from opentelemetry.util.genai.metrics import InvocationMetricsRecorder
@@ -57,6 +58,9 @@ def __init__(
5758
tool_type: str | None = None,
5859
tool_description: str | None = None,
5960
tool_result: Any = None,
61+
provider: str | None = None,
62+
server_address: str | None = None,
63+
server_port: int | None = None,
6064
attributes: dict[str, Any] | None = None,
6165
metric_attributes: dict[str, Any] | None = None,
6266
) -> None:
@@ -77,8 +81,24 @@ def __init__(
7781
self.tool_type = tool_type
7882
self.tool_description = tool_description
7983
self.tool_result = tool_result
84+
self.provider = provider
85+
self.server_address = server_address
86+
self.server_port = server_port
8087
self._start()
8188

89+
def _get_metric_attributes(self) -> dict[str, Any]:
90+
optional_attrs = (
91+
(GenAI.GEN_AI_PROVIDER_NAME, self.provider),
92+
(server_attributes.SERVER_ADDRESS, self.server_address),
93+
(server_attributes.SERVER_PORT, self.server_port),
94+
)
95+
attrs: dict[str, Any] = {
96+
GenAI.GEN_AI_OPERATION_NAME: self._operation_name,
97+
**{k: v for k, v in optional_attrs if v is not None},
98+
}
99+
attrs.update(self.metric_attributes)
100+
return attrs
101+
82102
def _apply_finish(self, error: Error | None = None) -> None:
83103
if error is not None:
84104
self._apply_error_attributes(error)
@@ -94,3 +114,4 @@ def _apply_finish(self, error: Error | None = None) -> None:
94114
}
95115
attributes.update(self.attributes)
96116
self.span.set_attributes(attributes)
117+
self._metrics_recorder.record(self)

util/opentelemetry-util-genai/src/opentelemetry/util/genai/handler.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ def start_tool(
171171
tool_call_id: str | None = None,
172172
tool_type: str | None = None,
173173
tool_description: str | None = None,
174+
provider: str | None = None,
175+
server_address: str | None = None,
176+
server_port: int | None = None,
174177
) -> ToolInvocation:
175178
"""Create and start a tool invocation.
176179
@@ -186,6 +189,9 @@ def start_tool(
186189
tool_call_id=tool_call_id,
187190
tool_type=tool_type,
188191
tool_description=tool_description,
192+
provider=provider,
193+
server_address=server_address,
194+
server_port=server_port,
189195
)
190196

191197
def start_workflow(
@@ -282,6 +288,9 @@ def tool(
282288
tool_call_id: str | None = None,
283289
tool_type: str | None = None,
284290
tool_description: str | None = None,
291+
provider: str | None = None,
292+
server_address: str | None = None,
293+
server_port: int | None = None,
285294
) -> AbstractContextManager[ToolInvocation]:
286295
"""Context manager for Tool invocations.
287296
@@ -297,6 +306,9 @@ def tool(
297306
tool_call_id=tool_call_id,
298307
tool_type=tool_type,
299308
tool_description=tool_description,
309+
provider=provider,
310+
server_address=server_address,
311+
server_port=server_port,
300312
)._managed()
301313

302314
def workflow(

util/opentelemetry-util-genai/tests/test_handler_metrics.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,84 @@ def _assert_metric_scope_schema_urls(
181181
self.assertEqual(
182182
scope_metric.scope.schema_url, expected_schema_url
183183
)
184+
185+
186+
class TelemetryHandlerToolMetricsTest(TestBase):
187+
def _harvest_metrics(self) -> Dict[str, List[Any]]:
188+
metrics = self.get_sorted_metrics()
189+
metrics_by_name: Dict[str, List[Any]] = {}
190+
for metric in metrics or []:
191+
points = metric.data.data_points or []
192+
metrics_by_name.setdefault(metric.name, []).extend(points)
193+
return metrics_by_name
194+
195+
def test_stop_tool_records_duration(self) -> None:
196+
handler = TelemetryHandler(
197+
tracer_provider=self.tracer_provider,
198+
meter_provider=self.meter_provider,
199+
)
200+
with patch("timeit.default_timer", return_value=1000.0):
201+
invocation = handler.start_tool(
202+
"get_weather",
203+
provider="test-provider",
204+
server_address="api.example.com",
205+
server_port=443,
206+
)
207+
invocation.metric_attributes = {"custom.key": "custom_value"}
208+
209+
with patch("timeit.default_timer", return_value=1002.5):
210+
invocation.stop()
211+
212+
metrics = self._harvest_metrics()
213+
self.assertIn("gen_ai.client.operation.duration", metrics)
214+
duration_points = metrics["gen_ai.client.operation.duration"]
215+
self.assertEqual(len(duration_points), 1)
216+
duration_point = duration_points[0]
217+
218+
self.assertEqual(
219+
duration_point.attributes[GenAI.GEN_AI_OPERATION_NAME],
220+
"execute_tool",
221+
)
222+
self.assertEqual(
223+
duration_point.attributes[GenAI.GEN_AI_PROVIDER_NAME],
224+
"test-provider",
225+
)
226+
self.assertEqual(
227+
duration_point.attributes["server.address"], "api.example.com"
228+
)
229+
self.assertEqual(duration_point.attributes["server.port"], 443)
230+
self.assertEqual(
231+
duration_point.attributes["custom.key"], "custom_value"
232+
)
233+
self.assertAlmostEqual(duration_point.sum, 2.5, places=3)
234+
self.assertNotIn("gen_ai.client.token.usage", metrics)
235+
236+
def test_fail_tool_records_duration_with_error(self) -> None:
237+
handler = TelemetryHandler(
238+
tracer_provider=self.tracer_provider,
239+
meter_provider=self.meter_provider,
240+
)
241+
with patch("timeit.default_timer", return_value=500.0):
242+
invocation = handler.start_tool(
243+
"failing_tool", provider="err-provider"
244+
)
245+
246+
error = Error(message="Tool execution failed", type=RuntimeError)
247+
with patch("timeit.default_timer", return_value=501.5):
248+
invocation.fail(error)
249+
250+
metrics = self._harvest_metrics()
251+
self.assertIn("gen_ai.client.operation.duration", metrics)
252+
duration_points = metrics["gen_ai.client.operation.duration"]
253+
self.assertEqual(len(duration_points), 1)
254+
duration_point = duration_points[0]
255+
256+
self.assertEqual(
257+
duration_point.attributes["error.type"], "RuntimeError"
258+
)
259+
self.assertEqual(
260+
duration_point.attributes[GenAI.GEN_AI_OPERATION_NAME],
261+
"execute_tool",
262+
)
263+
self.assertAlmostEqual(duration_point.sum, 1.5, places=3)
264+
self.assertNotIn("gen_ai.client.token.usage", metrics)

0 commit comments

Comments
 (0)