Skip to content

Commit 88b7e45

Browse files
committed
Improve the recording of span request attributes.
1 parent 969c003 commit 88b7e45

File tree

3 files changed

+99
-2
lines changed

3 files changed

+99
-2
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ def _flatten_value(
7171
rename_keys: Dict[str, str],
7272
flatten_functions: Dict[str, Callable],
7373
_from_json=False) -> FlattenedDict:
74+
if value is None:
75+
return {}
7476
key_names = set([key])
7577
renamed_key = rename_keys.get(key)
7678
if renamed_key is not None:
@@ -132,7 +134,6 @@ def _flatten_list(
132134
return result
133135

134136

135-
136137
def flatten_dict(
137138
d: Dict[str, Any],
138139
key_prefix: Optional[str] = None,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ def _add_request_options_to_span(span, config: Optional[GenerateContentConfigOrD
165165
"gen_ai.gcp.request.candidate_count": "gen_ai.request.choice.count",
166166
"gen_ai.gcp.request.max_output_tokens": "gen_ai.request.max_tokens",
167167
"gen_ai.gcp.request.stop_sequences": "gen_ai.request.stop_sequences",
168+
"gen_ai.gcp.request.frequency_penalty": "gen_ai.request.frequency_penalty",
168169
"gen_ai.gcp.request.presence_penalty": "gen_ai.request.presence_penalty",
169170
"gen_ai.gcp.request.seed": "gen_ai.request.seed",
170171
}
@@ -302,7 +303,7 @@ def _maybe_log_system_instruction(
302303
system_instruction = None
303304
if config is not None:
304305
if isinstance(config, dict):
305-
system_instruction = config["system_instruction"]
306+
system_instruction = config.get("system_instruction")
306307
else:
307308
system_instruction = config.system_instruction
308309
if not system_instruction:

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import unittest
1818

1919
from .base import TestCase
20+
from google.genai.types import GenerateContentConfig
2021

2122

2223
class NonStreamingTestCase(TestCase):
@@ -35,6 +36,14 @@ def generate_content(self, *args, **kwargs):
3536
def expected_function_name(self):
3637
raise NotImplementedError("Must implement 'expected_function_name'.")
3738

39+
def generate_and_get_span(self, config):
40+
self.generate_content(
41+
model="gemini-2.0-flash",
42+
contents="Some input prompt",
43+
config=config)
44+
self.otel.assert_has_span_named("generate_content gemini-2.0-flash")
45+
return self.otel.get_span_named("generate_content gemini-2.0-flash")
46+
3847
def test_instrumentation_does_not_break_core_functionality(self):
3948
self.configure_valid_response(text="Yep, it works!")
4049
response = self.generate_content(
@@ -94,6 +103,92 @@ def test_generated_span_has_vertex_ai_system_when_configured(self):
94103
span.attributes["gen_ai.operation.name"], "generate_content"
95104
)
96105

106+
def test_option_reflected_to_span_attribute_choice_count_config_dict(self):
107+
self.configure_valid_response(text='Some response')
108+
span = self.generate_and_get_span(config={"candidate_count": 2})
109+
self.assertEqual(span.attributes["gen_ai.request.choice.count"], 2)
110+
111+
def test_option_reflected_to_span_attribute_choice_count_config_obj(self):
112+
self.configure_valid_response(text='Some response')
113+
span = self.generate_and_get_span(config=GenerateContentConfig(candidate_count = 2))
114+
self.assertEqual(span.attributes["gen_ai.request.choice.count"], 2)
115+
116+
def test_option_reflected_to_span_attribute_seed_config_dict(self):
117+
self.configure_valid_response(text='Some response')
118+
span = self.generate_and_get_span(config={"seed": 12345})
119+
self.assertEqual(span.attributes["gen_ai.request.seed"], 12345)
120+
121+
def test_option_reflected_to_span_attribute_seed_config_obj(self):
122+
self.configure_valid_response(text='Some response')
123+
span = self.generate_and_get_span(config=GenerateContentConfig(seed = 12345))
124+
self.assertEqual(span.attributes["gen_ai.request.seed"], 12345)
125+
126+
def test_option_reflected_to_span_attribute_frequency_penalty(self):
127+
self.configure_valid_response(text='Some response')
128+
span = self.generate_and_get_span(config={"frequency_penalty": 1.0})
129+
self.assertEqual(span.attributes["gen_ai.request.frequency_penalty"], 1.0)
130+
131+
def test_option_reflected_to_span_attribute_max_tokens(self):
132+
self.configure_valid_response(text='Some response')
133+
span = self.generate_and_get_span(config=GenerateContentConfig(max_output_tokens=5000))
134+
self.assertEqual(span.attributes["gen_ai.request.max_tokens"], 5000)
135+
136+
def test_option_reflected_to_span_attribute_presence_penalty(self):
137+
self.configure_valid_response(text='Some response')
138+
span = self.generate_and_get_span(config=GenerateContentConfig(presence_penalty=0.5))
139+
self.assertEqual(span.attributes["gen_ai.request.presence_penalty"], 0.5)
140+
141+
def test_option_reflected_to_span_attribute_stop_sequences(self):
142+
self.configure_valid_response(text='Some response')
143+
span = self.generate_and_get_span(config={"stop_sequences": ["foo", "bar"]})
144+
stop_sequences = span.attributes["gen_ai.request.stop_sequences"]
145+
self.assertEqual(len(stop_sequences), 2)
146+
self.assertEqual(stop_sequences[0], "foo")
147+
self.assertEqual(stop_sequences[1], "bar")
148+
149+
def test_option_reflected_to_span_attribute_top_k(self):
150+
self.configure_valid_response(text='Some response')
151+
span = self.generate_and_get_span(config=GenerateContentConfig(top_k=20))
152+
self.assertEqual(span.attributes["gen_ai.request.top_k"], 20)
153+
154+
def test_option_reflected_to_span_attribute_top_p(self):
155+
self.configure_valid_response(text='Some response')
156+
span = self.generate_and_get_span(config={"top_p": 10})
157+
self.assertEqual(span.attributes["gen_ai.request.top_p"], 10)
158+
159+
def test_option_not_reflected_to_span_attribute_system_instruction(self):
160+
self.configure_valid_response(text='Some response')
161+
span = self.generate_and_get_span(config={"system_instruction": "Yadda yadda yadda"})
162+
self.assertNotIn("gen_ai.gcp.request.system_instruction", span.attributes)
163+
self.assertNotIn("gen_ai.request.system_instruction", span.attributes)
164+
for key in span.attributes:
165+
value = span.attributes[key]
166+
if isinstance(value, str):
167+
self.assertNotIn("Yadda yadda yadda", value)
168+
169+
def test_option_not_reflected_to_span_attribute_http_headers(self):
170+
self.configure_valid_response(text='Some response')
171+
span = self.generate_and_get_span(config={"http_options": {
172+
"base_url": "my.backend.override",
173+
"headers": {
174+
"sensitive": 12345,
175+
}
176+
}})
177+
self.assertEqual(
178+
span.attributes["gen_ai.gcp.request.http_options.base_url"], "my.backend.override")
179+
self.assertNotIn(
180+
"gen_ai.gcp.request.http_options.headers.sensitive",
181+
span.attributes
182+
)
183+
184+
def test_option_reflected_to_span_attribute_automatic_func_calling(self):
185+
self.configure_valid_response(text='Some response')
186+
span = self.generate_and_get_span(config={"automatic_function_calling": {
187+
"ignore_call_history": True,
188+
}})
189+
self.assertTrue(
190+
span.attributes["gen_ai.gcp.request.automatic_function_calling.ignore_call_history"])
191+
97192
def test_generated_span_counts_tokens(self):
98193
self.configure_valid_response(input_tokens=123, output_tokens=456)
99194
self.generate_content(model="gemini-2.0-flash", contents="Some input")

0 commit comments

Comments
 (0)