From f2ecbc0dd6af41dcda63c8978b7a16773248ae12 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 11 Aug 2026 13:22:20 -0400 Subject: [PATCH 1/9] feat(api_core): Add tracer_provider to ClientOptions --- packages/google-api-core/google/api_core/client_options.py | 4 ++++ packages/google-api-core/tests/unit/test_client_options.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/google-api-core/google/api_core/client_options.py b/packages/google-api-core/google/api_core/client_options.py index 68c4644245ce..724a720c82b0 100644 --- a/packages/google-api-core/google/api_core/client_options.py +++ b/packages/google-api-core/google/api_core/client_options.py @@ -98,6 +98,8 @@ class ClientOptions(object): `googleapis.com`. If both `api_endpoint` and `universe_domain` are set, then `api_endpoint` is used as the service endpoint. If `api_endpoint` is not specified, the format will be `{service}.{universe_domain}`. + tracer_provider (Optional[object]): The OpenTelemetry TracerProvider to use + for tracing. If not set, the global tracer provider is used. Raises: ValueError: If both ``client_cert_source`` and ``client_encrypted_cert_source`` @@ -117,6 +119,7 @@ def __init__( api_key: Optional[str] = None, api_audience: Optional[str] = None, universe_domain: Optional[str] = None, + tracer_provider: Optional[object] = None, ): if credentials_file is not None: warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning) @@ -136,6 +139,7 @@ def __init__( self.api_key = api_key self.api_audience = api_audience self.universe_domain = universe_domain + self.tracer_provider = tracer_provider def __repr__(self) -> str: return "ClientOptions: " + repr(self.__dict__) diff --git a/packages/google-api-core/tests/unit/test_client_options.py b/packages/google-api-core/tests/unit/test_client_options.py index 5d68232219f1..632fd0740772 100644 --- a/packages/google-api-core/tests/unit/test_client_options.py +++ b/packages/google-api-core/tests/unit/test_client_options.py @@ -15,7 +15,6 @@ from re import match import pytest - from google.api_core import client_options from ..helpers import warn_deprecated_credentials_file @@ -162,6 +161,7 @@ def test_repr(): "scopes", "api_key", "api_audience", + "tracer_provider", ] ) options = client_options.ClientOptions(api_endpoint="foo.googleapis.com") From f8e5f4cb4d94c53e972f2bbb1a05bfbec45e153d Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 11 Aug 2026 13:22:24 -0400 Subject: [PATCH 2/9] feat(api_core): Plumb tracer_provider in grpc_helpers.create_channel --- .../google/api_core/grpc_helpers.py | 31 +++++++++++++++++-- packages/google-api-core/pyproject.toml | 3 ++ .../testing/constraints-3.10.txt | 1 + .../testing/constraints-async-rest-3.10.txt | 1 + 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/google/api_core/grpc_helpers.py b/packages/google-api-core/google/api_core/grpc_helpers.py index 263079e7d1f7..67b73d031418 100644 --- a/packages/google-api-core/google/api_core/grpc_helpers.py +++ b/packages/google-api-core/google/api_core/grpc_helpers.py @@ -25,8 +25,7 @@ import google.auth.transport.requests import google.protobuf import grpc - -from google.api_core import exceptions, general_helpers +from google.api_core import _feature_gating_helpers, exceptions, general_helpers # The list of gRPC Callable interfaces that return iterators. _STREAM_WRAP_CLASSES = (grpc.UnaryStreamMultiCallable, grpc.StreamStreamMultiCallable) @@ -384,10 +383,36 @@ def create_channel( if attempt_direct_path: target = _modify_target_for_direct_path(target) - return grpc.secure_channel( + configuration = kwargs.pop("configuration", None) + + channel = grpc.secure_channel( target, composite_credentials, compression=compression, **kwargs ) + is_tracing_enabled = _feature_gating_helpers.resolve_feature_flags( + env_var="GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", + feature_key="tracer_provider", + configuration=configuration, + ) + + if is_tracing_enabled: + try: + import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] + tracer_provider = None + if configuration is not None: + if isinstance(configuration, dict): + tracer_provider = configuration.get("tracer_provider") + else: + tracer_provider = getattr(configuration, "tracer_provider", None) + + interceptor = otel_grpc.client_interceptor(tracer_provider=tracer_provider) + channel = otel_grpc.intercept_channel(channel, interceptor) + except ImportError: + # If OpenTelemetry gRPC instrumentation is missing, this should simply NOOP and fail open rather than failing import. + pass + + return channel + def _modify_target_for_direct_path(target: str) -> str: """ diff --git a/packages/google-api-core/pyproject.toml b/packages/google-api-core/pyproject.toml index 18113cb4de74..8c0a9558c28f 100644 --- a/packages/google-api-core/pyproject.toml +++ b/packages/google-api-core/pyproject.toml @@ -48,6 +48,7 @@ dependencies = [ "proto-plus >= 1.26.1, < 2.0.0", "google-auth >= 2.14.1, < 3.0.0", "requests >= 2.33.0, < 3.0.0", + "opentelemetry-api >= 1.27.0, < 2.0.0", ] dynamic = ["version"] @@ -91,4 +92,6 @@ filterwarnings = [ "ignore:.*custom tp_new.*in Python 3.14:DeprecationWarning", # Remove once https://github.com/grpc/grpc/issues/35086 is fixed (and version newer than 1.60.0 is published) "ignore:There is no current event loop:DeprecationWarning", + # Ignore external OpenTelemetry/importlib.metadata SelectableGroups warning + "ignore:.*SelectableGroups dict interface is deprecated:DeprecationWarning", ] diff --git a/packages/google-api-core/testing/constraints-3.10.txt b/packages/google-api-core/testing/constraints-3.10.txt index 5fb51afb6c56..4cb9760152c6 100644 --- a/packages/google-api-core/testing/constraints-3.10.txt +++ b/packages/google-api-core/testing/constraints-3.10.txt @@ -12,3 +12,4 @@ requests==2.33.0 grpcio==1.59.0 grpcio-status==1.59.0 proto-plus==1.26.1 +opentelemetry-api==1.27.0 diff --git a/packages/google-api-core/testing/constraints-async-rest-3.10.txt b/packages/google-api-core/testing/constraints-async-rest-3.10.txt index d94635253d59..bd2beec5f247 100644 --- a/packages/google-api-core/testing/constraints-async-rest-3.10.txt +++ b/packages/google-api-core/testing/constraints-async-rest-3.10.txt @@ -13,3 +13,4 @@ grpcio==1.59.0 grpcio-status==1.59.0 proto-plus==1.26.1 aiohttp==3.13.4 +opentelemetry-api==1.27.0 From 49a88ff925f30ef6db9d081085fb179da57fa30c Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 11 Aug 2026 13:22:27 -0400 Subject: [PATCH 3/9] fix(api_core): Prevent TypeError in grpc_helpers_async.create_channel --- .../google/api_core/grpc_helpers_async.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/google-api-core/google/api_core/grpc_helpers_async.py b/packages/google-api-core/google/api_core/grpc_helpers_async.py index d1f897901e7a..0a6c9a96814c 100644 --- a/packages/google-api-core/google/api_core/grpc_helpers_async.py +++ b/packages/google-api-core/google/api_core/grpc_helpers_async.py @@ -24,9 +24,8 @@ from typing import AsyncGenerator, Generic, Iterator, Optional, TypeVar import grpc -from grpc import aio - from google.api_core import exceptions, general_helpers, grpc_helpers +from grpc import aio # denotes the proto response type for grpc calls P = TypeVar("P") @@ -303,6 +302,15 @@ def create_channel( if attempt_direct_path: target = grpc_helpers._modify_target_for_direct_path(target) + # NOTE: 'configuration' is popped to prevent a TypeError. + # Generated async transports (like those in secretmanager) pass 'configuration' + # down to this helper via **kwargs to support tracing in sync transports. + # However, 'aio.secure_channel' does not recognize this parameter and will + # crash if it is passed through. + # Async gRPC tracing is deferred to a future phase/PR, so we simply discard + # this parameter for now to ensure generated async code doesn't fail at runtime. + kwargs.pop("configuration", None) + return aio.secure_channel( target, composite_credentials, compression=compression, **kwargs ) From 73f91fe527f510e65018a743fa1789d72dc4752d Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 11 Aug 2026 13:22:30 -0400 Subject: [PATCH 4/9] test(api_core): Add OTel gRPC helpers tests --- .../tests/unit/test_grpc_helpers_otel.py | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 packages/google-api-core/tests/unit/test_grpc_helpers_otel.py diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py new file mode 100644 index 000000000000..577b08dce48e --- /dev/null +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -0,0 +1,159 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for OpenTelemetry gRPC interceptor integration in google-api-core.""" + +import sys +import types +from unittest import mock + +import pytest + +try: + from google.api_core import grpc_helpers + + HAS_GRPC_HELPERS = True +except ImportError: + HAS_GRPC_HELPERS = False + + +@pytest.fixture +def mock_otel_grpc(monkeypatch): + """Fixture to mock OpenTelemetry gRPC hierarchy.""" + mock_otel = mock.Mock() + mock_otel_grpc = mock_otel.instrumentation.grpc + mock_interceptor = mock.Mock() + mock_otel_grpc.client_interceptor.return_value = mock_interceptor + mock_otel_grpc.intercept_channel.side_effect = lambda ch, inc: f"wrapped_{ch}" + + modules = { + "opentelemetry": mock_otel, + "opentelemetry.instrumentation": mock_otel.instrumentation, + "opentelemetry.instrumentation.grpc": mock_otel_grpc, + } + + for name, mod in modules.items(): + monkeypatch.setitem(sys.modules, name, mod) + + return mock_otel_grpc + + +@pytest.mark.skipif(not HAS_GRPC_HELPERS, reason="Requires google-api-core[grpc]") +def test_create_channel_otel_installed_and_enabled(monkeypatch, mock_otel_grpc): + """Verify that create_channel wraps the channel with OTel interceptor when installed and enabled.""" + + # Enable tracing + monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "true") + + # Mock grpc.secure_channel + mock_channel = "raw_channel" + with mock.patch( + "grpc.secure_channel", return_value=mock_channel + ) as mock_secure_channel: + # We need to mock credentials setup to avoid external calls + with mock.patch( + "google.api_core.grpc_helpers._create_composite_credentials", + return_value=mock.Mock(), + ): + channel = grpc_helpers.create_channel("localhost:1234") + + # Verify raw channel was created + mock_secure_channel.assert_called_once() + + # Verify OTel interceptor was fetched and channel was wrapped + mock_otel_grpc.client_interceptor.assert_called_once() + mock_otel_grpc.intercept_channel.assert_called_once_with( + mock_channel, mock_otel_grpc.client_interceptor.return_value + ) + + # Verify returned channel is the wrapped one + assert channel == f"wrapped_{mock_channel}" + + +@pytest.mark.skipif(not HAS_GRPC_HELPERS, reason="Requires google-api-core[grpc]") +def test_create_channel_otel_installed_but_disabled(monkeypatch, mock_otel_grpc): + """Verify that create_channel does NOT wrap the channel if tracing is disabled.""" + + # Disable tracing (or leave unset, default should be false/disabled) + monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "false") + + mock_channel = "raw_channel" + with mock.patch( + "grpc.secure_channel", return_value=mock_channel + ) as mock_secure_channel: + with mock.patch( + "google.api_core.grpc_helpers._create_composite_credentials", + return_value=mock.Mock(), + ): + channel = grpc_helpers.create_channel("localhost:1234") + + # Verify raw channel was created + mock_secure_channel.assert_called_once() + + # Verify OTel was NOT used + mock_otel_grpc.intercept_channel.assert_not_called() + + # Verify returned channel is the raw one + assert channel == mock_channel + + +@pytest.mark.skipif(not HAS_GRPC_HELPERS, reason="Requires google-api-core[grpc]") +def test_create_channel_otel_not_installed_fails_open(monkeypatch): + """Verify that create_channel fails open if OTel is not installed, even if enabled.""" + + # Simulate missing module + monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", None) + + # Enable tracing + monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "true") + + mock_channel = "raw_channel" + with mock.patch( + "grpc.secure_channel", return_value=mock_channel + ) as mock_secure_channel: + with mock.patch( + "google.api_core.grpc_helpers._create_composite_credentials", + return_value=mock.Mock(), + ): + # This should NOT raise ImportError + channel = grpc_helpers.create_channel("localhost:1234") + + # Verify raw channel was created + mock_secure_channel.assert_called_once() + + # Verify returned channel is the raw one + assert channel == mock_channel + + +@pytest.mark.parametrize( + "config_factory", + [ + lambda tp: {"tracer_provider": tp}, + lambda tp: types.SimpleNamespace(tracer_provider=tp), + ], + ids=["dict", "object"], +) +@pytest.mark.skipif(not HAS_GRPC_HELPERS, reason="Requires google-api-core[grpc]") +def test_create_channel_with_custom_tracer_provider(monkeypatch, mock_otel_grpc, config_factory): + """Verify that create_channel passes custom tracer_provider to OTel interceptor.""" + + mock_tracer_provider = mock.Mock() + config = config_factory(mock_tracer_provider) + + mock_channel = "raw_channel" + with mock.patch("grpc.secure_channel", return_value=mock_channel): + with mock.patch("google.api_core.grpc_helpers._create_composite_credentials", return_value=mock.Mock()): + grpc_helpers.create_channel("localhost:1234", configuration=config) + + mock_otel_grpc.client_interceptor.assert_called_once_with(tracer_provider=mock_tracer_provider) From ca1559bbf344f3b2f41088c1342d190d7ed08ee0 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 11 Aug 2026 13:22:35 -0400 Subject: [PATCH 5/9] chore(api_core): Clean up imports in tests --- .../google-api-core/tests/asyncio/test_grpc_helpers_async.py | 1 - packages/google-api-core/tests/unit/test_grpc_helpers.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/google-api-core/tests/asyncio/test_grpc_helpers_async.py b/packages/google-api-core/tests/asyncio/test_grpc_helpers_async.py index dcb09f18fea2..bdc8b80ba8c8 100644 --- a/packages/google-api-core/tests/asyncio/test_grpc_helpers_async.py +++ b/packages/google-api-core/tests/asyncio/test_grpc_helpers_async.py @@ -33,7 +33,6 @@ import google.auth.credentials - from google.api_core import exceptions, grpc_helpers_async diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers.py b/packages/google-api-core/tests/unit/test_grpc_helpers.py index 69281d58109b..39b533261c71 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers.py @@ -24,9 +24,8 @@ pytest.skip("No GRPC", allow_module_level=True) import google.auth.credentials -from google.longrunning import operations_pb2 - from google.api_core import exceptions, grpc_helpers +from google.longrunning import operations_pb2 def test__patch_callable_name(): From 6c3cca81a16f564a8df3dc643e0fa523d5a5a021 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 11 Aug 2026 13:53:57 -0400 Subject: [PATCH 6/9] refactor(api_core): Parametrize OTel installation and enablement tests --- .../tests/unit/test_grpc_helpers_otel.py | 99 ++++++------------- 1 file changed, 30 insertions(+), 69 deletions(-) diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index 577b08dce48e..df95a4edc6a0 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -49,74 +49,28 @@ def mock_otel_grpc(monkeypatch): return mock_otel_grpc +@pytest.mark.parametrize( + "is_otel_installed, tracing_env_var_value, expect_otel_interceptor", + [ + pytest.param(True, "true", True, id="installed_and_enabled"), + pytest.param(True, "false", False, id="installed_but_disabled"), + pytest.param(False, "true", False, id="not_installed_fails_open"), + ], +) @pytest.mark.skipif(not HAS_GRPC_HELPERS, reason="Requires google-api-core[grpc]") -def test_create_channel_otel_installed_and_enabled(monkeypatch, mock_otel_grpc): - """Verify that create_channel wraps the channel with OTel interceptor when installed and enabled.""" - - # Enable tracing - monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "true") - - # Mock grpc.secure_channel - mock_channel = "raw_channel" - with mock.patch( - "grpc.secure_channel", return_value=mock_channel - ) as mock_secure_channel: - # We need to mock credentials setup to avoid external calls - with mock.patch( - "google.api_core.grpc_helpers._create_composite_credentials", - return_value=mock.Mock(), - ): - channel = grpc_helpers.create_channel("localhost:1234") - - # Verify raw channel was created - mock_secure_channel.assert_called_once() - - # Verify OTel interceptor was fetched and channel was wrapped - mock_otel_grpc.client_interceptor.assert_called_once() - mock_otel_grpc.intercept_channel.assert_called_once_with( - mock_channel, mock_otel_grpc.client_interceptor.return_value - ) - - # Verify returned channel is the wrapped one - assert channel == f"wrapped_{mock_channel}" - - -@pytest.mark.skipif(not HAS_GRPC_HELPERS, reason="Requires google-api-core[grpc]") -def test_create_channel_otel_installed_but_disabled(monkeypatch, mock_otel_grpc): - """Verify that create_channel does NOT wrap the channel if tracing is disabled.""" - - # Disable tracing (or leave unset, default should be false/disabled) - monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "false") - - mock_channel = "raw_channel" - with mock.patch( - "grpc.secure_channel", return_value=mock_channel - ) as mock_secure_channel: - with mock.patch( - "google.api_core.grpc_helpers._create_composite_credentials", - return_value=mock.Mock(), - ): - channel = grpc_helpers.create_channel("localhost:1234") - - # Verify raw channel was created - mock_secure_channel.assert_called_once() - - # Verify OTel was NOT used - mock_otel_grpc.intercept_channel.assert_not_called() - - # Verify returned channel is the raw one - assert channel == mock_channel - - -@pytest.mark.skipif(not HAS_GRPC_HELPERS, reason="Requires google-api-core[grpc]") -def test_create_channel_otel_not_installed_fails_open(monkeypatch): - """Verify that create_channel fails open if OTel is not installed, even if enabled.""" +def test_create_channel_otel_combos( + monkeypatch, + mock_otel_grpc, + is_otel_installed, + tracing_env_var_value, + expect_otel_interceptor, +): + """Verify create_channel behavior with various OTel installation and enablement states.""" - # Simulate missing module - monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", None) + monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", tracing_env_var_value) - # Enable tracing - monkeypatch.setenv("GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", "true") + if not is_otel_installed: + monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", None) mock_channel = "raw_channel" with mock.patch( @@ -126,14 +80,21 @@ def test_create_channel_otel_not_installed_fails_open(monkeypatch): "google.api_core.grpc_helpers._create_composite_credentials", return_value=mock.Mock(), ): - # This should NOT raise ImportError channel = grpc_helpers.create_channel("localhost:1234") - # Verify raw channel was created + # Always expect raw channel creation mock_secure_channel.assert_called_once() - # Verify returned channel is the raw one - assert channel == mock_channel + if expect_otel_interceptor: + mock_otel_grpc.client_interceptor.assert_called_once() + mock_otel_grpc.intercept_channel.assert_called_once_with( + mock_channel, mock_otel_grpc.client_interceptor.return_value + ) + assert channel == f"wrapped_{mock_channel}" + else: + # OTel should NOT have been called + mock_otel_grpc.intercept_channel.assert_not_called() + assert channel == mock_channel @pytest.mark.parametrize( From 8297044fc0599502263491458aaecfa71e232f4d Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Tue, 11 Aug 2026 13:57:20 -0400 Subject: [PATCH 7/9] Update packages/google-api-core/google/api_core/grpc_helpers_async.py --- packages/google-api-core/google/api_core/grpc_helpers_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-api-core/google/api_core/grpc_helpers_async.py b/packages/google-api-core/google/api_core/grpc_helpers_async.py index 0a6c9a96814c..9055fa5d81a9 100644 --- a/packages/google-api-core/google/api_core/grpc_helpers_async.py +++ b/packages/google-api-core/google/api_core/grpc_helpers_async.py @@ -303,7 +303,7 @@ def create_channel( target = grpc_helpers._modify_target_for_direct_path(target) # NOTE: 'configuration' is popped to prevent a TypeError. - # Generated async transports (like those in secretmanager) pass 'configuration' + # Generated async transports (like those in google-cloud-* libs) pass 'configuration' # down to this helper via **kwargs to support tracing in sync transports. # However, 'aio.secure_channel' does not recognize this parameter and will # crash if it is passed through. From d019dd329e7713f57f710faddfa2e9b9b73f9413 Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Tue, 11 Aug 2026 13:57:28 -0400 Subject: [PATCH 8/9] Update packages/google-api-core/google/api_core/grpc_helpers_async.py --- packages/google-api-core/google/api_core/grpc_helpers_async.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/google-api-core/google/api_core/grpc_helpers_async.py b/packages/google-api-core/google/api_core/grpc_helpers_async.py index 9055fa5d81a9..c002f6713786 100644 --- a/packages/google-api-core/google/api_core/grpc_helpers_async.py +++ b/packages/google-api-core/google/api_core/grpc_helpers_async.py @@ -305,7 +305,7 @@ def create_channel( # NOTE: 'configuration' is popped to prevent a TypeError. # Generated async transports (like those in google-cloud-* libs) pass 'configuration' # down to this helper via **kwargs to support tracing in sync transports. - # However, 'aio.secure_channel' does not recognize this parameter and will + # However, 'aio.secure_channel' does not recognize this parameter yet and will # crash if it is passed through. # Async gRPC tracing is deferred to a future phase/PR, so we simply discard # this parameter for now to ensure generated async code doesn't fail at runtime. From 182da981d392ed12709b772e818f4dfdb79770d0 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 11 Aug 2026 14:28:16 -0400 Subject: [PATCH 9/9] fix(api_core): Use grpc.intercept_channel instead of invalid otel_grpc.intercept_channel --- .../google-api-core/google/api_core/grpc_helpers.py | 2 +- .../tests/unit/test_grpc_helpers_otel.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/google-api-core/google/api_core/grpc_helpers.py b/packages/google-api-core/google/api_core/grpc_helpers.py index 67b73d031418..97574b91a1d4 100644 --- a/packages/google-api-core/google/api_core/grpc_helpers.py +++ b/packages/google-api-core/google/api_core/grpc_helpers.py @@ -406,7 +406,7 @@ def create_channel( tracer_provider = getattr(configuration, "tracer_provider", None) interceptor = otel_grpc.client_interceptor(tracer_provider=tracer_provider) - channel = otel_grpc.intercept_channel(channel, interceptor) + channel = grpc.intercept_channel(channel, interceptor) except ImportError: # If OpenTelemetry gRPC instrumentation is missing, this should simply NOOP and fail open rather than failing import. pass diff --git a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py index df95a4edc6a0..0739e7fdef6f 100644 --- a/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py +++ b/packages/google-api-core/tests/unit/test_grpc_helpers_otel.py @@ -35,7 +35,6 @@ def mock_otel_grpc(monkeypatch): mock_otel_grpc = mock_otel.instrumentation.grpc mock_interceptor = mock.Mock() mock_otel_grpc.client_interceptor.return_value = mock_interceptor - mock_otel_grpc.intercept_channel.side_effect = lambda ch, inc: f"wrapped_{ch}" modules = { "opentelemetry": mock_otel, @@ -75,7 +74,9 @@ def test_create_channel_otel_combos( mock_channel = "raw_channel" with mock.patch( "grpc.secure_channel", return_value=mock_channel - ) as mock_secure_channel: + ) as mock_secure_channel, mock.patch( + "grpc.intercept_channel", side_effect=lambda ch, inc: f"wrapped_{ch}" + ) as mock_intercept_channel: with mock.patch( "google.api_core.grpc_helpers._create_composite_credentials", return_value=mock.Mock(), @@ -87,13 +88,13 @@ def test_create_channel_otel_combos( if expect_otel_interceptor: mock_otel_grpc.client_interceptor.assert_called_once() - mock_otel_grpc.intercept_channel.assert_called_once_with( + mock_intercept_channel.assert_called_once_with( mock_channel, mock_otel_grpc.client_interceptor.return_value ) assert channel == f"wrapped_{mock_channel}" else: # OTel should NOT have been called - mock_otel_grpc.intercept_channel.assert_not_called() + mock_intercept_channel.assert_not_called() assert channel == mock_channel @@ -113,7 +114,9 @@ def test_create_channel_with_custom_tracer_provider(monkeypatch, mock_otel_grpc, config = config_factory(mock_tracer_provider) mock_channel = "raw_channel" - with mock.patch("grpc.secure_channel", return_value=mock_channel): + with mock.patch("grpc.secure_channel", return_value=mock_channel), mock.patch( + "grpc.intercept_channel", side_effect=lambda ch, inc: f"wrapped_{ch}" + ): with mock.patch("google.api_core.grpc_helpers._create_composite_credentials", return_value=mock.Mock()): grpc_helpers.create_channel("localhost:1234", configuration=config)