Skip to content

Commit 3a3981a

Browse files
RKestaabmass
andauthored
feat: add extension point to add custom attributes to generate_content spans in google genai instrumentor. (#3961)
* feat add ability to add custom attributes to google genai `generate_content {model.name}` spans * Switch from contextvars to OTel context_api * Update CHANGELOG.md --------- Co-authored-by: Aaron Abbott <aaronabbott@google.com>
1 parent 0ab0c5f commit 3a3981a

File tree

5 files changed

+57
-3
lines changed

5 files changed

+57
-3
lines changed

instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Enable the addition of custom attributes to the `generate_content {model.name}` span via the Context API. ([#3961](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3961)).
11+
1012
## Version 0.5b0 (2025-12-11)
1113

1214
- Ensure log event is written and completion hook is called even when model call results in exception. Put new
@@ -39,4 +41,4 @@ span attribute `gen_ai.response.finish_reasons` is empty ([#3417](https://github
3941
([#3298](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3298))
4042

4143
Create an initial version of Open Telemetry instrumentation for github.com/googleapis/python-genai.
42-
([#3256](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3256))
44+
([#3256](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3256))

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"""
4343

4444
from .instrumentor import GoogleGenAiSdkInstrumentor
45+
from .generate_content import GENERATE_CONTENT_EXTRA_ATTRIBUTES_CONTEXT_KEY
4546
from .version import __version__
4647

47-
__all__ = ["GoogleGenAiSdkInstrumentor", "__version__"]
48+
__all__ = ["GoogleGenAiSdkInstrumentor", "GENERATE_CONTENT_EXTRA_ATTRIBUTES_CONTEXT_KEY", "__version__"]

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

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

2525
from google.genai.models import AsyncModels, Models
2626
from google.genai.models import t as transformers
@@ -57,6 +57,9 @@
5757
MessagePart,
5858
OutputMessage,
5959
)
60+
from opentelemetry.util.types import (
61+
AttributeValue,
62+
)
6063
from opentelemetry.util.genai.utils import gen_ai_json_dumps
6164

6265
from .allowlist_util import AllowList
@@ -70,6 +73,7 @@
7073
)
7174
from .otel_wrapper import OTelWrapper
7275
from .tool_call_wrapper import wrapped as wrapped_tool
76+
from opentelemetry import context as context_api
7377

7478
_logger = logging.getLogger(__name__)
7579

@@ -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_CONTEXT_KEY = context_api.create_key("generate_content_extra_attributes_context_key")
8388

8489
class _MethodsSnapshot:
8590
def __init__(self):
@@ -294,6 +299,10 @@ def _create_completion_details_attributes(
294299
return attributes
295300

296301

302+
def _get_extra_generate_content_attributes() -> Optional[Mapping[str, AttributeValue]]:
303+
return context_api.get_value(GENERATE_CONTENT_EXTRA_ATTRIBUTES_CONTEXT_KEY)
304+
305+
297306
class _GenerateContentInstrumentationHelper:
298307
def __init__(
299308
self,
@@ -720,6 +729,8 @@ def instrumented_generate_content(
720729
with helper.start_span_as_current_span(
721730
model, "google.genai.Models.generate_content"
722731
) as span:
732+
if extra_attributes := _get_extra_generate_content_attributes():
733+
span.set_attributes(extra_attributes)
723734
span.set_attributes(request_attributes)
724735
if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
725736
helper.process_request(contents, config, span)
@@ -793,6 +804,8 @@ def instrumented_generate_content_stream(
793804
with helper.start_span_as_current_span(
794805
model, "google.genai.Models.generate_content_stream"
795806
) as span:
807+
if extra_attributes := _get_extra_generate_content_attributes():
808+
span.set_attributes(extra_attributes)
796809
span.set_attributes(request_attributes)
797810
if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
798811
helper.process_request(contents, config, span)
@@ -866,6 +879,8 @@ async def instrumented_generate_content(
866879
with helper.start_span_as_current_span(
867880
model, "google.genai.AsyncModels.generate_content"
868881
) as span:
882+
if extra_attributes := _get_extra_generate_content_attributes():
883+
span.set_attributes(extra_attributes)
869884
span.set_attributes(request_attributes)
870885
if helper.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
871886
helper.process_request(contents, config, span)
@@ -940,6 +955,8 @@ async def instrumented_generate_content_stream(
940955
"google.genai.AsyncModels.generate_content_stream",
941956
end_on_exit=False,
942957
) as span:
958+
if extra_attributes := _get_extra_generate_content_attributes():
959+
span.set_attributes(extra_attributes)
943960
span.set_attributes(request_attributes)
944961
if (
945962
not helper.sem_conv_opt_in_mode

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
from opentelemetry.util.genai.types import ContentCapturingMode
3232

3333
from .base import TestCase
34+
from opentelemetry import context as context_api
35+
from opentelemetry.instrumentation.google_genai import GENERATE_CONTENT_EXTRA_ATTRIBUTES_CONTEXT_KEY
3436

3537
# pylint: disable=too-many-public-methods
3638

@@ -99,6 +101,21 @@ def test_generated_span_has_minimal_genai_attributes(self):
99101
span.attributes["gen_ai.operation.name"], "generate_content"
100102
)
101103

104+
def test_generated_span_has_extra_genai_attributes(self):
105+
self.configure_valid_response(text="Yep, it works!")
106+
tok = context_api.attach(context_api.set_value(GENERATE_CONTENT_EXTRA_ATTRIBUTES_CONTEXT_KEY, {"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+
context_api.detach(tok)
118+
102119
def test_span_and_event_still_written_when_response_is_exception(self):
103120
self.configure_exception(ValueError("Uh oh!"))
104121
patched_environ = patch.dict(

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

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

1717
from .base import TestCase
18+
from opentelemetry import context as context_api
19+
from opentelemetry.instrumentation.google_genai import GENERATE_CONTENT_EXTRA_ATTRIBUTES_CONTEXT_KEY
1820

1921

2022
class StreamingTestCase(TestCase):
@@ -42,6 +44,21 @@ def test_instrumentation_does_not_break_core_functionality(self):
4244
response = responses[0]
4345
self.assertEqual(response.text, "Yep, it works!")
4446

47+
def test_generated_span_has_extra_genai_attributes(self):
48+
self.configure_valid_response(text="Yep, it works!")
49+
tok = context_api.attach(context_api.set_value(GENERATE_CONTENT_EXTRA_ATTRIBUTES_CONTEXT_KEY, {"extra_attribute_key": "extra_attribute_value"}))
50+
try:
51+
self.generate_content(
52+
model="gemini-2.0-flash", contents="Does this work?"
53+
)
54+
self.otel.assert_has_span_named("generate_content gemini-2.0-flash")
55+
span = self.otel.get_span_named("generate_content gemini-2.0-flash")
56+
self.assertEqual(span.attributes["extra_attribute_key"], "extra_attribute_value")
57+
except:
58+
raise
59+
finally:
60+
context_api.detach(tok)
61+
4562
def test_handles_multiple_ressponses(self):
4663
self.configure_valid_response(text="First response")
4764
self.configure_valid_response(text="Second response")

0 commit comments

Comments
 (0)