From d6f95e1b9e9938f2dcbb4830a2498aceb68fb106 Mon Sep 17 00:00:00 2001 From: Max Ind Date: Fri, 5 Dec 2025 17:27:44 +0000 Subject: [PATCH 1/2] feat: support `supress_instrumentation` in `opentelemetry-instrumentation-google-genai` --- .../google_genai/generate_content.py | 22 +++++++++++++ .../google_genai/tool_call_wrapper.py | 7 ++++ .../generate_content/nonstreaming_base.py | 12 +++++++ .../tests/generate_content/streaming_base.py | 15 +++++++++ .../test_tool_call_instrumentation.py | 33 +++++++++++++++++++ 5 files changed, 89 insertions(+) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py index 6d6e02ca5e..4598af0906 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py @@ -44,6 +44,7 @@ _OpenTelemetryStabilitySignalType, _StabilityMode, ) +from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.semconv._incubating.attributes import ( code_attributes, gen_ai_attributes, @@ -705,6 +706,11 @@ def instrumented_generate_content( config: Optional[GenerateContentConfigOrDict] = None, **kwargs: Any, ) -> GenerateContentResponse: + if not is_instrumentation_enabled(): + return wrapped_func( + self, model=model, contents=contents, config=config, **kwargs + ) + candidates = [] helper = _GenerateContentInstrumentationHelper( self, @@ -778,6 +784,13 @@ def instrumented_generate_content_stream( config: Optional[GenerateContentConfigOrDict] = None, **kwargs: Any, ) -> Iterator[GenerateContentResponse]: + if not is_instrumentation_enabled(): + for resp in wrapped_func( + self, model=model, contents=contents, config=config, **kwargs + ): + yield resp + return + candidates: list[Candidate] = [] helper = _GenerateContentInstrumentationHelper( self, @@ -851,6 +864,10 @@ async def instrumented_generate_content( config: Optional[GenerateContentConfigOrDict] = None, **kwargs: Any, ) -> GenerateContentResponse: + if not is_instrumentation_enabled(): + return await wrapped_func( + self, model=model, contents=contents, config=config, **kwargs + ) helper = _GenerateContentInstrumentationHelper( self, otel_wrapper, @@ -924,6 +941,11 @@ async def instrumented_generate_content_stream( config: Optional[GenerateContentConfigOrDict] = None, **kwargs: Any, ) -> Awaitable[AsyncIterator[GenerateContentResponse]]: # type: ignore + if not is_instrumentation_enabled(): + return await wrapped_func( + self, model=model, contents=contents, config=config, **kwargs + ) + helper = _GenerateContentInstrumentationHelper( self, otel_wrapper, diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py index f4303306e3..dfffbec1d7 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/tool_call_wrapper.py @@ -29,6 +29,7 @@ _OpenTelemetryStabilitySignalType, _StabilityMode, ) +from opentelemetry.instrumentation.utils import is_instrumentation_enabled from opentelemetry.semconv._incubating.attributes import ( code_attributes, ) @@ -168,6 +169,9 @@ def _wrap_sync_tool_function( ): @functools.wraps(tool_function) def wrapped_function(*args, **kwargs): + if not is_instrumentation_enabled(): + return tool_function(*args, **kwargs) + span_name = _create_function_span_name(tool_function) attributes = _create_function_span_attributes( tool_function, args, kwargs, extra_span_attributes @@ -193,6 +197,9 @@ def _wrap_async_tool_function( ): @functools.wraps(tool_function) async def wrapped_function(*args, **kwargs): + if not is_instrumentation_enabled(): + return await tool_function(*args, **kwargs) + span_name = _create_function_span_name(tool_function) attributes = _create_function_span_attributes( tool_function, args, kwargs, extra_span_attributes diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/nonstreaming_base.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/nonstreaming_base.py index c669f99dd3..4c0fdfc4a3 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/nonstreaming_base.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/nonstreaming_base.py @@ -20,6 +20,7 @@ from google.genai.types import GenerateContentConfig, Part from pydantic import BaseModel, Field +from opentelemetry.instrumentation import utils from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, @@ -499,3 +500,14 @@ def test_records_metrics_data(self): self.otel.assert_has_metrics_data_named( "gen_ai.client.operation.duration" ) + + def test_suppress_instrumentation(self): + self.configure_valid_response(text="Yep, it works!") + with utils.suppress_instrumentation(): + response = self.generate_content( + model="gemini-2.0-flash", contents="Does this work?" + ) + self.assertEqual(response.text, "Yep, it works!") + self.otel.assert_does_not_have_span_named( + "generate_content gemini-2.0-flash" + ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/streaming_base.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/streaming_base.py index e5bceb7c79..64cdeb0578 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/streaming_base.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/streaming_base.py @@ -14,6 +14,8 @@ import unittest +from opentelemetry.instrumentation import utils + from .base import TestCase @@ -70,3 +72,16 @@ def test_includes_token_counts_in_span_aggregated_from_responses(self): span = self.otel.get_span_named("generate_content gemini-2.0-flash") self.assertEqual(span.attributes["gen_ai.usage.input_tokens"], 9) self.assertEqual(span.attributes["gen_ai.usage.output_tokens"], 12) + + def test_suppress_instrumentation(self): + self.configure_valid_response(text="Yep, it works!") + with utils.suppress_instrumentation(): + responses = self.generate_content( + model="gemini-2.0-flash", contents="Does this work?" + ) + self.assertEqual(len(responses), 1) + response = responses[0] + self.assertEqual(response.text, "Yep, it works!") + self.otel.assert_does_not_have_span_named( + "generate_content gemini-2.0-flash" + ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py index 2dc0a3d633..f89b0d46a8 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/tests/generate_content/test_tool_call_instrumentation.py @@ -16,6 +16,7 @@ import google.genai.types as genai_types +from opentelemetry.instrumentation import utils from opentelemetry.instrumentation._semconv import ( _OpenTelemetrySemanticConventionStability, _OpenTelemetryStabilitySignalType, @@ -440,3 +441,35 @@ def somefunction(x, y=2): generated_span.attributes, ) self.tearDown() + + def test_suppress_tool_call_instrumentation(self): + calls = [] + + def handle(*args, **kwargs): + calls.append((args, kwargs)) + return "some result" + + def somefunction(somearg): + print("somearg=%s", somearg) + + self.mock_generate_content.side_effect = handle + self.client.models.generate_content( + model="some-model-name", + contents="Some content", + config={ + "tools": [somefunction], + }, + ) + self.assertEqual(len(calls), 1) + config = calls[0][1]["config"] + tools = config.tools + wrapped_somefunction = tools[0] + + self.assertIsNone( + self.otel.get_span_named("execute_tool somefunction") + ) + + with utils.suppress_instrumentation(): + wrapped_somefunction("someparam") + + self.otel.assert_does_not_have_span_named("execute_tool somefunction") From c11adfa98be921f16f334fc7d1bc47a99193d1b6 Mon Sep 17 00:00:00 2001 From: Max Ind Date: Mon, 15 Dec 2025 18:17:06 +0000 Subject: [PATCH 2/2] Update CHANGELOG.md --- .../opentelemetry-instrumentation-google-genai/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md index 5da3595dee..c2ffbdc4e0 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md +++ b/instrumentation-genai/opentelemetry-instrumentation-google-genai/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Support `suppress_instrumentation` contextmanager from `opentelemetry.instrumentation.utils`. + ## Version 0.5b0 (2025-12-11) - 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 ([#3298](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3298)) Create an initial version of Open Telemetry instrumentation for github.com/googleapis/python-genai. -([#3256](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3256)) \ No newline at end of file +([#3256](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3256))