Skip to content

Commit b3a6efb

Browse files
committed
test: add tool call instrumentation tests and nonstreaming recording in spans test.
1 parent 47d5728 commit b3a6efb

File tree

2 files changed

+167
-2
lines changed

2 files changed

+167
-2
lines changed

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import unittest
1717
from unittest.mock import patch
1818

19+
from google.genai.types import GenerateContentConfig
1920
from opentelemetry.instrumentation._semconv import (
2021
_OpenTelemetrySemanticConventionStability,
2122
_OpenTelemetryStabilitySignalType,
@@ -188,7 +189,7 @@ def test_does_not_record_response_as_log_if_disabled_by_env(self):
188189
self.assertEqual(event_record.attributes["gen_ai.system"], "gemini")
189190
self.assertEqual(event_record.body["content"], "<elided>")
190191

191-
def test_new_semconv_record_response_as_log(self):
192+
def test_new_semconv_record_completion_as_log(self):
192193
for mode in ContentCapturingMode:
193194
patched_environ = patch.dict(
194195
"os.environ",
@@ -218,7 +219,42 @@ def test_new_semconv_record_response_as_log(self):
218219
self.otel.assert_has_event_named("gen_ai.client.inference.operation.details")
219220

220221
self.tearDown()
221-
222+
223+
def test_new_semconv_record_completion_in_span(self):
224+
for mode in ContentCapturingMode:
225+
patched_environ = patch.dict(
226+
"os.environ",
227+
{
228+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": mode.name,
229+
"OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental",
230+
},
231+
)
232+
patched_otel_mapping = patch.dict(
233+
_OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING,
234+
{
235+
_OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL
236+
},
237+
)
238+
with self.subTest(f'mode: {mode}', patched_environ=patched_environ):
239+
self.setUp()
240+
with patched_environ, patched_otel_mapping:
241+
self.configure_valid_response(text="Some response content")
242+
self.generate_content(model="gemini-2.0-flash", contents="Some input", config=GenerateContentConfig(system_instruction="System instruction"))
243+
span = self.otel.get_span_named("generate_content gemini-2.0-flash")
244+
if mode in [
245+
ContentCapturingMode.SPAN_ONLY,
246+
ContentCapturingMode.SPAN_AND_EVENT,
247+
]:
248+
self.assertEqual(span.attributes["gen_ai.input.messages"], '[{"role": "user", "parts": [{"content": "Some input", "type": "text"}]}]')
249+
self.assertEqual(span.attributes["gen_ai.output.messages"], '[{"role": "assistant", "parts": [{"content": "Some response content", "type": "text"}], "finish_reason": ""}]')
250+
self.assertEqual(span.attributes["gen_ai.system_instructions"], '[{"content": "System instruction", "type": "text"}]')
251+
else:
252+
self.assertNotIn("gen_ai.input.messages", span.attributes)
253+
self.assertNotIn("gen_ai.output.messages", span.attributes)
254+
self.assertNotIn("gen_ai.system_instructions", span.attributes)
255+
256+
self.tearDown()
257+
222258
def test_records_metrics_data(self):
223259
self.configure_valid_response()
224260
self.generate_content(model="gemini-2.0-flash", contents="Some input")

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

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
from unittest.mock import patch
1616

1717
import google.genai.types as genai_types
18+
from opentelemetry.instrumentation._semconv import (
19+
_OpenTelemetrySemanticConventionStability,
20+
_OpenTelemetryStabilitySignalType,
21+
_StabilityMode,
22+
)
23+
from opentelemetry.util.genai.types import ContentCapturingMode
1824

1925
from .base import TestCase
2026

@@ -275,3 +281,126 @@ def somefunction(x, y=2):
275281
self.assertNotIn(
276282
"code.function.return.value", generated_span.attributes
277283
)
284+
285+
def test_new_semconv_tool_calls_record_parameter_values(self):
286+
for mode in ContentCapturingMode:
287+
patched_environ = patch.dict(
288+
"os.environ",
289+
{
290+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": mode.name,
291+
"OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental",
292+
},
293+
)
294+
patched_otel_mapping = patch.dict(
295+
_OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING,
296+
{
297+
_OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL
298+
},
299+
)
300+
with self.subTest(f'mode: {mode}', patched_environ=patched_environ):
301+
self.setUp()
302+
with patched_environ, patched_otel_mapping:
303+
calls = []
304+
305+
def handle(*args, **kwargs):
306+
calls.append((args, kwargs))
307+
return "some result"
308+
309+
def somefunction(someparam, otherparam=2):
310+
print("someparam=%s, otherparam=%s", someparam, otherparam)
311+
312+
self.mock_generate_content.side_effect = handle
313+
self.client.models.generate_content(
314+
model="some-model-name",
315+
contents="Some content",
316+
config={
317+
"tools": [somefunction],
318+
},
319+
)
320+
self.assertEqual(len(calls), 1)
321+
config = calls[0][1]["config"]
322+
tools = config.tools
323+
wrapped_somefunction = tools[0]
324+
wrapped_somefunction(123, otherparam="abc")
325+
self.otel.assert_has_span_named("execute_tool somefunction")
326+
generated_span = self.otel.get_span_named("execute_tool somefunction")
327+
self.assertEqual(
328+
generated_span.attributes[
329+
"code.function.parameters.someparam.type"
330+
],
331+
"int",
332+
)
333+
self.assertEqual(
334+
generated_span.attributes[
335+
"code.function.parameters.otherparam.type"
336+
],
337+
"str",
338+
)
339+
if mode in [
340+
ContentCapturingMode.SPAN_ONLY,
341+
ContentCapturingMode.SPAN_AND_EVENT,
342+
]:
343+
self.assertEqual(generated_span.attributes["code.function.parameters.someparam.value"], 123)
344+
self.assertEqual(generated_span.attributes["code.function.parameters.otherparam.value"], "abc")
345+
else:
346+
self.assertNotIn("code.function.parameters.someparam.value", generated_span.attributes)
347+
self.assertNotIn("code.function.parameters.otherparam.value", generated_span.attributes)
348+
self.tearDown()
349+
350+
def test_new_semconv_tool_calls_record_return_values(self):
351+
for mode in ContentCapturingMode:
352+
patched_environ = patch.dict(
353+
"os.environ",
354+
{
355+
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": mode.name,
356+
"OTEL_SEMCONV_STABILITY_OPT_IN": "gen_ai_latest_experimental",
357+
},
358+
)
359+
patched_otel_mapping = patch.dict(
360+
_OpenTelemetrySemanticConventionStability._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING,
361+
{
362+
_OpenTelemetryStabilitySignalType.GEN_AI: _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL
363+
},
364+
)
365+
with self.subTest(f'mode: {mode}', patched_environ=patched_environ):
366+
self.setUp()
367+
with patched_environ, patched_otel_mapping:
368+
calls = []
369+
370+
def handle(*args, **kwargs):
371+
calls.append((args, kwargs))
372+
return "some result"
373+
374+
def somefunction(x, y=2):
375+
return x + y
376+
377+
self.mock_generate_content.side_effect = handle
378+
self.client.models.generate_content(
379+
model="some-model-name",
380+
contents="Some content",
381+
config={
382+
"tools": [somefunction],
383+
},
384+
)
385+
self.assertEqual(len(calls), 1)
386+
config = calls[0][1]["config"]
387+
tools = config.tools
388+
wrapped_somefunction = tools[0]
389+
wrapped_somefunction(123)
390+
self.otel.assert_has_span_named("execute_tool somefunction")
391+
generated_span = self.otel.get_span_named("execute_tool somefunction")
392+
self.assertEqual(
393+
generated_span.attributes["code.function.return.type"], "int"
394+
)
395+
if mode in [
396+
ContentCapturingMode.SPAN_ONLY,
397+
ContentCapturingMode.SPAN_AND_EVENT,
398+
]:
399+
self.assertIn(
400+
"code.function.return.value", generated_span.attributes
401+
)
402+
else:
403+
self.assertNotIn(
404+
"code.function.return.value", generated_span.attributes
405+
)
406+
self.tearDown()

0 commit comments

Comments
 (0)