Skip to content

Commit 071a399

Browse files
committed
Improve attribute handling and align with 'execute_tool' span spec.
1 parent 6ecea10 commit 071a399

File tree

3 files changed

+190
-55
lines changed

3 files changed

+190
-55
lines changed

instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import functools
1616
import inspect
17+
import json
1718
from typing import Any, Callable, Optional, Union
1819

1920
from google.genai.types import (
@@ -33,10 +34,18 @@
3334
ToolFunction = Callable[..., Any]
3435

3536

37+
def _is_primitive(value):
38+
primitive_types = [str, int, bool, float]
39+
for ptype in primitive_types:
40+
if isinstance(value, ptype):
41+
return True
42+
return False
43+
44+
3645
def _to_otel_value(python_value):
3746
"""Coerces parameters to something representable with Open Telemetry."""
38-
if python_value is None:
39-
return None
47+
if python_value is None or _is_primitive(python_value):
48+
return python_value
4049
if isinstance(python_value, list):
4150
return [_to_otel_value(x) for x in python_value]
4251
if isinstance(python_value, dict):
@@ -50,10 +59,31 @@ def _to_otel_value(python_value):
5059
return repr(python_value)
5160

5261

62+
def _is_homogenous_primitive_list(value):
63+
if not isinstance(value, list):
64+
return False
65+
if not value:
66+
return True
67+
if not _is_primitive(value[0]):
68+
return False
69+
first_type = type(value[0])
70+
for entry in value[1:]:
71+
if not isinstance(entry, first_type):
72+
return False
73+
return True
74+
75+
76+
def _to_otel_attribute(python_value):
77+
otel_value = _to_otel_value(python_value)
78+
if _is_primitive(otel_value) or _is_homogenous_primitive_list(otel_value):
79+
return otel_value
80+
return json.dumps(otel_value)
81+
82+
5383
def _create_function_span_name(wrapped_function):
5484
"""Constructs the span name for a given local function tool call."""
5585
function_name = wrapped_function.__name__
56-
return f"tool_call {function_name}"
86+
return f"execute_tool {function_name}"
5787

5888

5989
def _create_function_span_attributes(
@@ -63,6 +93,10 @@ def _create_function_span_attributes(
6393
result = {}
6494
if extra_span_attributes:
6595
result.update(extra_span_attributes)
96+
result["gen_ai.operation.name"] = "execute_tool"
97+
result["gen_ai.tool.name"] = wrapped_function.__name__
98+
if wrapped_function.__doc__:
99+
result["gen_ai.tool.description"] = wrapped_function.__doc__
66100
result[code_attributes.CODE_FUNCTION_NAME] = wrapped_function.__name__
67101
result["code.module"] = wrapped_function.__module__
68102
result["code.args.positional.count"] = len(function_args)
@@ -78,7 +112,7 @@ def _record_function_call_argument(
78112
span.set_attribute(type_attribute, type(param_value).__name__)
79113
if include_values:
80114
value_attribute = f"{attribute_prefix}.value"
81-
span.set_attribute(value_attribute, _to_otel_value(param_value))
115+
span.set_attribute(value_attribute, _to_otel_attribute(param_value))
82116

83117

84118
def _record_function_call_arguments(
@@ -105,7 +139,7 @@ def _record_function_call_result(otel_wrapper, wrapped_function, result):
105139
span.set_attribute("code.function.return.type", type(result).__name__)
106140
if include_values:
107141
span.set_attribute(
108-
"code.function.return.value", _to_otel_value(result)
142+
"code.function.return.value", _to_otel_attribute(result)
109143
)
110144

111145

instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import os
15+
from unittest.mock import patch
1616

1717
import google.genai.types as genai_types
1818

@@ -43,10 +43,10 @@ def somefunction(somearg):
4343
tools = config.tools
4444
wrapped_somefunction = tools[0]
4545

46-
self.assertIsNone(self.otel.get_span_named("tool_call somefunction"))
46+
self.assertIsNone(self.otel.get_span_named("execute_tool somefunction"))
4747
wrapped_somefunction(somearg="someparam")
48-
self.otel.assert_has_span_named("tool_call somefunction")
49-
generated_span = self.otel.get_span_named("tool_call somefunction")
48+
self.otel.assert_has_span_named("execute_tool somefunction")
49+
generated_span = self.otel.get_span_named("execute_tool somefunction")
5050
self.assertEqual(
5151
generated_span.attributes["code.function.name"], "somefunction"
5252
)
@@ -74,18 +74,16 @@ def somefunction(somearg):
7474
tools = config.tools
7575
wrapped_somefunction = tools[0]
7676

77-
self.assertIsNone(self.otel.get_span_named("tool_call somefunction"))
77+
self.assertIsNone(self.otel.get_span_named("execute_tool somefunction"))
7878
wrapped_somefunction(somearg="someparam")
79-
self.otel.assert_has_span_named("tool_call somefunction")
80-
generated_span = self.otel.get_span_named("tool_call somefunction")
79+
self.otel.assert_has_span_named("execute_tool somefunction")
80+
generated_span = self.otel.get_span_named("execute_tool somefunction")
8181
self.assertEqual(
8282
generated_span.attributes["code.function.name"], "somefunction"
8383
)
8484

85+
@patch.dict("os.environ", {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true"})
8586
def test_tool_calls_record_parameter_values_on_span_if_enabled(self):
86-
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = (
87-
"true"
88-
)
8987
calls = []
9088

9189
def handle(*args, **kwargs):
@@ -108,8 +106,8 @@ def somefunction(someparam, otherparam=2):
108106
tools = config.tools
109107
wrapped_somefunction = tools[0]
110108
wrapped_somefunction(123, otherparam="abc")
111-
self.otel.assert_has_span_named("tool_call somefunction")
112-
generated_span = self.otel.get_span_named("tool_call somefunction")
109+
self.otel.assert_has_span_named("execute_tool somefunction")
110+
generated_span = self.otel.get_span_named("execute_tool somefunction")
113111
self.assertEqual(
114112
generated_span.attributes[
115113
"code.function.parameters.someparam.type"
@@ -126,19 +124,17 @@ def somefunction(someparam, otherparam=2):
126124
generated_span.attributes[
127125
"code.function.parameters.someparam.value"
128126
],
129-
"123",
127+
123,
130128
)
131129
self.assertEqual(
132130
generated_span.attributes[
133131
"code.function.parameters.otherparam.value"
134132
],
135-
"'abc'",
133+
"abc",
136134
)
137135

136+
@patch.dict("os.environ", {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "false"})
138137
def test_tool_calls_do_not_record_parameter_values_if_not_enabled(self):
139-
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = (
140-
"false"
141-
)
142138
calls = []
143139

144140
def handle(*args, **kwargs):
@@ -161,8 +157,8 @@ def somefunction(someparam, otherparam=2):
161157
tools = config.tools
162158
wrapped_somefunction = tools[0]
163159
wrapped_somefunction(123, otherparam="abc")
164-
self.otel.assert_has_span_named("tool_call somefunction")
165-
generated_span = self.otel.get_span_named("tool_call somefunction")
160+
self.otel.assert_has_span_named("execute_tool somefunction")
161+
generated_span = self.otel.get_span_named("execute_tool somefunction")
166162
self.assertEqual(
167163
generated_span.attributes[
168164
"code.function.parameters.someparam.type"
@@ -184,10 +180,8 @@ def somefunction(someparam, otherparam=2):
184180
generated_span.attributes,
185181
)
186182

183+
@patch.dict("os.environ", {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "true"})
187184
def test_tool_calls_record_return_values_on_span_if_enabled(self):
188-
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = (
189-
"true"
190-
)
191185
calls = []
192186

193187
def handle(*args, **kwargs):
@@ -210,19 +204,17 @@ def somefunction(x, y=2):
210204
tools = config.tools
211205
wrapped_somefunction = tools[0]
212206
wrapped_somefunction(123)
213-
self.otel.assert_has_span_named("tool_call somefunction")
214-
generated_span = self.otel.get_span_named("tool_call somefunction")
207+
self.otel.assert_has_span_named("execute_tool somefunction")
208+
generated_span = self.otel.get_span_named("execute_tool somefunction")
215209
self.assertEqual(
216210
generated_span.attributes["code.function.return.type"], "int"
217211
)
218212
self.assertEqual(
219-
generated_span.attributes["code.function.return.value"], "125"
213+
generated_span.attributes["code.function.return.value"], 125
220214
)
221215

216+
@patch.dict("os.environ", {"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "false"})
222217
def test_tool_calls_do_not_record_return_values_if_not_enabled(self):
223-
os.environ["OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"] = (
224-
"false"
225-
)
226218
calls = []
227219

228220
def handle(*args, **kwargs):
@@ -245,8 +237,8 @@ def somefunction(x, y=2):
245237
tools = config.tools
246238
wrapped_somefunction = tools[0]
247239
wrapped_somefunction(123)
248-
self.otel.assert_has_span_named("tool_call somefunction")
249-
generated_span = self.otel.get_span_named("tool_call somefunction")
240+
self.otel.assert_has_span_named("execute_tool somefunction")
241+
generated_span = self.otel.get_span_named("execute_tool somefunction")
250242
self.assertEqual(
251243
generated_span.attributes["code.function.return.type"], "int"
252244
)

0 commit comments

Comments
 (0)