Skip to content

Commit ef7a205

Browse files
feat(gemini): migrate google-generativeai to latest OTel GenAI semantic conventions (#3840)
Co-authored-by: Oz Ben Simhon <oz@traceloop.com>
1 parent 0a25803 commit ef7a205

10 files changed

Lines changed: 1848 additions & 603 deletions

File tree

packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
emit_message_events,
1616
)
1717
from opentelemetry.instrumentation.google_generativeai.span_utils import (
18+
_collect_finish_reasons_from_response,
19+
set_input_attributes,
1820
set_input_attributes_sync,
1921
set_model_request_attributes,
2022
set_model_response_attributes,
@@ -32,40 +34,37 @@
3234
)
3335
from opentelemetry.semconv_ai import (
3436
SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
35-
LLMRequestTypeValues,
36-
SpanAttributes,
37-
Meters
37+
Meters,
3838
)
3939
from opentelemetry.metrics import Meter, get_meter
4040
from opentelemetry.trace import SpanKind, get_tracer, StatusCode
4141
from wrapt import wrap_function_wrapper
4242

43+
_GCP_GEN_AI = GenAIAttributes.GenAiProviderNameValues.GCP_GEN_AI.value
44+
_GEN_CONTENT = GenAIAttributes.GenAiOperationNameValues.GENERATE_CONTENT.value
45+
4346
logger = logging.getLogger(__name__)
4447

4548
WRAPPED_METHODS = [
4649
{
4750
"package": "google.genai.models",
4851
"object": "Models",
4952
"method": "generate_content",
50-
"span_name": "gemini.generate_content",
5153
},
5254
{
5355
"package": "google.genai.models",
5456
"object": "AsyncModels",
5557
"method": "generate_content",
56-
"span_name": "gemini.generate_content",
5758
},
5859
{
5960
"package": "google.genai.models",
6061
"object": "Models",
6162
"method": "generate_content_stream",
62-
"span_name": "gemini.generate_content",
6363
},
6464
{
6565
"package": "google.genai.models",
6666
"object": "AsyncModels",
6767
"method": "generate_content_stream",
68-
"span_name": "gemini.generate_content",
6968
},
7069
]
7170

@@ -85,43 +84,79 @@ def _build_from_streaming_response(
8584
event_logger,
8685
token_histogram,
8786
):
88-
complete_response = ""
87+
emit_events = should_emit_events() and event_logger
88+
text_parts = []
8989
last_chunk = None
9090
for item in response:
9191
item_to_yield = item
9292
last_chunk = item
93-
complete_response += str(item.text)
93+
if not emit_events:
94+
t = getattr(item, "text", None)
95+
if isinstance(t, str):
96+
text_parts.append(t)
9497

9598
yield item_to_yield
9699

97-
if should_emit_events() and event_logger:
100+
complete_response = "".join(text_parts)
101+
102+
if emit_events:
98103
emit_choice_events(response, event_logger)
99104
else:
100-
set_response_attributes(span, complete_response, llm_model)
105+
if last_chunk is not None and getattr(last_chunk, "candidates", None):
106+
set_response_attributes(span, last_chunk, llm_model)
107+
else:
108+
set_response_attributes(
109+
span, complete_response, llm_model, stream_last_chunk=last_chunk
110+
)
111+
112+
# Finish reasons from the final chunk — Gemini SDK aggregates candidates per chunk,
113+
# so the last chunk reflects all candidates without deduplication artifacts.
114+
stream_reasons = _collect_finish_reasons_from_response(last_chunk) if last_chunk else None
101115
set_model_response_attributes(
102-
span, last_chunk or response, llm_model, token_histogram
116+
span,
117+
last_chunk or response,
118+
llm_model,
119+
token_histogram,
120+
stream_finish_reasons=stream_reasons or None,
103121
)
104122
span.end()
105123

106124

107125
async def _abuild_from_streaming_response(
108126
span, response: GenerateContentResponse, llm_model, event_logger, token_histogram
109127
):
110-
complete_response = ""
128+
emit_events = should_emit_events() and event_logger
129+
text_parts = []
111130
last_chunk = None
112131
async for item in response:
113132
item_to_yield = item
114133
last_chunk = item
115-
complete_response += str(item.text)
134+
if not emit_events:
135+
t = getattr(item, "text", None)
136+
if isinstance(t, str):
137+
text_parts.append(t)
116138

117139
yield item_to_yield
118140

119-
if should_emit_events() and event_logger:
141+
complete_response = "".join(text_parts)
142+
143+
if emit_events:
120144
emit_choice_events(response, event_logger)
121145
else:
122-
set_response_attributes(span, complete_response, llm_model)
146+
if last_chunk is not None and getattr(last_chunk, "candidates", None):
147+
set_response_attributes(span, last_chunk, llm_model)
148+
else:
149+
set_response_attributes(
150+
span, complete_response, llm_model, stream_last_chunk=last_chunk
151+
)
152+
153+
stream_reasons = _collect_finish_reasons_from_response(last_chunk) if last_chunk else None
123154
set_model_response_attributes(
124-
span, last_chunk if last_chunk else response, llm_model, token_histogram
155+
span,
156+
last_chunk if last_chunk else response,
157+
llm_model,
158+
token_histogram,
159+
stream_finish_reasons=stream_reasons or None,
125160
)
126161
span.end()
127162

@@ -136,6 +171,16 @@ def _handle_request(span, args, kwargs, llm_model, event_logger):
136171
set_model_request_attributes(span, kwargs, llm_model)
137172

138173

174+
@dont_throw
175+
async def _handle_request_async(span, args, kwargs, llm_model, event_logger):
176+
if should_emit_events() and event_logger:
177+
emit_message_events(args, kwargs, event_logger)
178+
else:
179+
await set_input_attributes(span, args, kwargs, llm_model)
180+
181+
set_model_request_attributes(span, kwargs, llm_model)
182+
183+
139184
@dont_throw
140185
def _handle_response(span, response, llm_model, event_logger, token_histogram):
141186
if should_emit_events() and event_logger:
@@ -200,17 +245,17 @@ async def _awrap(
200245
if "model" in kwargs:
201246
llm_model = kwargs["model"].replace("models/", "")
202247

203-
name = to_wrap.get("span_name")
204248
span = tracer.start_span(
205-
name,
249+
f"{_GEN_CONTENT} {llm_model}",
206250
kind=SpanKind.CLIENT,
207251
attributes={
208-
GenAIAttributes.GEN_AI_SYSTEM: "Google",
209-
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
252+
GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
253+
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT,
254+
GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model,
210255
},
211256
)
212257
start_time = time.perf_counter()
213-
_handle_request(span, args, kwargs, llm_model, event_logger)
258+
await _handle_request_async(span, args, kwargs, llm_model, event_logger)
214259
try:
215260
response = await wrapped(*args, **kwargs)
216261
except Exception as e:
@@ -224,7 +269,9 @@ async def _awrap(
224269
duration_histogram.record(
225270
duration,
226271
attributes={
227-
GenAIAttributes.GEN_AI_PROVIDER_NAME: "Google",
272+
GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
273+
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT,
274+
GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model,
228275
GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model,
229276
},
230277
)
@@ -276,13 +323,13 @@ def _wrap(
276323
if "model" in kwargs:
277324
llm_model = kwargs["model"].replace("models/", "")
278325

279-
name = to_wrap.get("span_name")
280326
span = tracer.start_span(
281-
name,
327+
f"{_GEN_CONTENT} {llm_model}",
282328
kind=SpanKind.CLIENT,
283329
attributes={
284-
GenAIAttributes.GEN_AI_SYSTEM: "Google",
285-
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
330+
GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
331+
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT,
332+
GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model,
286333
},
287334
)
288335

@@ -301,7 +348,9 @@ def _wrap(
301348
duration_histogram.record(
302349
duration,
303350
attributes={
304-
GenAIAttributes.GEN_AI_PROVIDER_NAME: "Google",
351+
GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI,
352+
GenAIAttributes.GEN_AI_OPERATION_NAME: _GEN_CONTENT,
353+
GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model,
305354
GenAIAttributes.GEN_AI_RESPONSE_MODEL: llm_model,
306355
},
307356
)

packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/event_emitter.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
from opentelemetry.semconv._incubating.attributes import (
1717
gen_ai_attributes as GenAIAttributes,
1818
)
19+
from opentelemetry.instrumentation.google_generativeai.span_utils import (
20+
_map_gemini_finish_reason,
21+
)
22+
23+
_GCP_GEN_AI = GenAIAttributes.GenAiProviderNameValues.GCP_GEN_AI.value
1924

2025

2126
class Roles(Enum):
@@ -28,7 +33,7 @@ class Roles(Enum):
2833
VALID_MESSAGE_ROLES = {role.value for role in Roles}
2934
"""The valid roles for naming the message event."""
3035

31-
EVENT_ATTRIBUTES = {GenAIAttributes.GEN_AI_SYSTEM: "gemini"}
36+
EVENT_ATTRIBUTES = {GenAIAttributes.GEN_AI_PROVIDER_NAME: _GCP_GEN_AI}
3237
"""The attributes to be used for the event."""
3338

3439

@@ -65,7 +70,7 @@ def emit_choice_events(
6570
"content": [part_to_dict(i) for i in candidate.content.parts],
6671
"role": candidate.content.role,
6772
},
68-
finish_reason=candidate.finish_reason.name,
73+
finish_reason=_map_gemini_finish_reason(candidate.finish_reason),
6974
),
7075
event_logger,
7176
)

0 commit comments

Comments
 (0)