Skip to content

Commit d8cc094

Browse files
committed
feat add ability to add custom attributes to google genai generate_content {model.name} spans
1 parent 96a9d0f commit d8cc094

File tree

3 files changed

+42
-1
lines changed

3 files changed

+42
-1
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
import logging
2121
import os
2222
import time
23-
from typing import Any, AsyncIterator, Awaitable, Iterator, Optional, Union
23+
import contextvars
24+
from typing import Any, AsyncIterator, Awaitable, Iterator, Optional, Union, Mapping
2425

2526
from google.genai.models import AsyncModels, Models
2627
from google.genai.models import t as transformers
@@ -57,6 +58,9 @@
5758
MessagePart,
5859
OutputMessage,
5960
)
61+
from opentelemetry.util.types import (
62+
AttributeValue,
63+
)
6064
from opentelemetry.util.genai.utils import gen_ai_json_dumps
6165

6266
from .allowlist_util import AllowList
@@ -80,6 +84,7 @@
8084
# Constant used for the value of 'gen_ai.operation.name".
8185
_GENERATE_CONTENT_OP_NAME = "generate_content"
8286

87+
GENERATE_CONTENT_EXTRA_ATTRIBUTES = contextvars.ContextVar[Mapping[str, AttributeValue]]("GENERATE_CONTENT_EXTRA_ATTRIBUTES", default={})
8388

8489
class _MethodsSnapshot:
8590
def __init__(self):
@@ -728,6 +733,7 @@ def instrumented_generate_content(
728733
with helper.start_span_as_current_span(
729734
model, "google.genai.Models.generate_content"
730735
) as span:
736+
span.set_attributes(GENERATE_CONTENT_EXTRA_ATTRIBUTES.get())
731737
span.set_attributes(request_attributes)
732738
if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
733739
helper.process_request(contents, config, span)
@@ -803,6 +809,7 @@ def instrumented_generate_content_stream(
803809
with helper.start_span_as_current_span(
804810
model, "google.genai.Models.generate_content_stream"
805811
) as span:
812+
span.set_attributes(GENERATE_CONTENT_EXTRA_ATTRIBUTES.get())
806813
span.set_attributes(request_attributes)
807814
if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
808815
helper.process_request(contents, config, span)
@@ -878,6 +885,7 @@ async def instrumented_generate_content(
878885
with helper.start_span_as_current_span(
879886
model, "google.genai.AsyncModels.generate_content"
880887
) as span:
888+
span.set_attributes(GENERATE_CONTENT_EXTRA_ATTRIBUTES.get())
881889
span.set_attributes(request_attributes)
882890
if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
883891
helper.process_request(contents, config, span)
@@ -954,6 +962,7 @@ async def instrumented_generate_content_stream(
954962
"google.genai.AsyncModels.generate_content_stream",
955963
end_on_exit=False,
956964
) as span:
965+
span.set_attributes(GENERATE_CONTENT_EXTRA_ATTRIBUTES.get())
957966
span.set_attributes(request_attributes)
958967
if not is_experimental_mode:
959968
helper.process_request(contents, config, span)

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from opentelemetry.util.genai.types import ContentCapturingMode
3333

3434
from .base import TestCase
35+
from opentelemetry.instrumentation.google_genai.generate_content import GENERATE_CONTENT_EXTRA_ATTRIBUTES
3536

3637
# pylint: disable=too-many-public-methods
3738

@@ -100,6 +101,21 @@ def test_generated_span_has_minimal_genai_attributes(self):
100101
span.attributes["gen_ai.operation.name"], "generate_content"
101102
)
102103

104+
def test_generated_span_has_extra_genai_attributes(self):
105+
self.configure_valid_response(text="Yep, it works!")
106+
tok = GENERATE_CONTENT_EXTRA_ATTRIBUTES.set({"extra_attribute_key": "extra_attribute_value"})
107+
try:
108+
self.generate_content(
109+
model="gemini-2.0-flash", contents="Does this work?"
110+
)
111+
self.otel.assert_has_span_named("generate_content gemini-2.0-flash")
112+
span = self.otel.get_span_named("generate_content gemini-2.0-flash")
113+
self.assertEqual(span.attributes["extra_attribute_key"], "extra_attribute_value")
114+
except:
115+
raise
116+
finally:
117+
GENERATE_CONTENT_EXTRA_ATTRIBUTES.reset(tok)
118+
103119
def test_span_and_event_still_written_when_response_is_exception(self):
104120
self.configure_exception(ValueError("Uh oh!"))
105121
patched_environ = patch.dict(

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import unittest
1616

1717
from .base import TestCase
18+
from opentelemetry.instrumentation.google_genai.generate_content import GENERATE_CONTENT_EXTRA_ATTRIBUTES
1819

1920

2021
class StreamingTestCase(TestCase):
@@ -42,6 +43,21 @@ def test_instrumentation_does_not_break_core_functionality(self):
4243
response = responses[0]
4344
self.assertEqual(response.text, "Yep, it works!")
4445

46+
def test_generated_span_has_extra_genai_attributes(self):
47+
self.configure_valid_response(text="Yep, it works!")
48+
tok = GENERATE_CONTENT_EXTRA_ATTRIBUTES.set({"extra_attrbiute_key": "extra_attribute_value"})
49+
try:
50+
self.generate_content(
51+
model="gemini-2.0-flash", contents="Does this work?"
52+
)
53+
self.otel.assert_has_span_named("generate_content gemini-2.0-flash")
54+
span = self.otel.get_span_named("generate_content gemini-2.0-flash")
55+
self.assertEqual(span.attributes["extra_attrbiute_key"], "extra_attribute_value")
56+
except:
57+
raise
58+
finally:
59+
GENERATE_CONTENT_EXTRA_ATTRIBUTES.reset(tok)
60+
4561
def test_handles_multiple_ressponses(self):
4662
self.configure_valid_response(text="First response")
4763
self.configure_valid_response(text="Second response")

0 commit comments

Comments
 (0)