From a1fb4c64ad38e5218ca4e812cb1c22e596971f9a Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 27 Aug 2026 11:22:06 -0400 Subject: [PATCH 1/4] feat(api-core): add eager channel orchestration for OpenTelemetry - Implement create_channel_with_otel and create_async_channel_with_otel helpers - Deduplicate interceptor instantiation via internal _get_otel_interceptor - Add unit tests in test_observability.py --- .../google/api_core/_observability.py | 87 +++++++-- .../tests/unit/test_observability.py | 175 ++++++++++++++---- 2 files changed, 211 insertions(+), 51 deletions(-) diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index b1b71b056658..858451db6ac4 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -16,7 +16,7 @@ """OpenTelemetry helpers for resolving and instantiating interceptors.""" -from typing import Any, Optional +from typing import Any, Callable, Optional, Union from google.api_core import _feature_gating_helpers from google.api_core.client_options import ClientOptions @@ -54,26 +54,19 @@ def is_otel_capabilities_enabled( return False -def apply_otel_capabilities_to_channel( - channel: Any, - client_options: Optional[ClientOptions | dict[str, Any]] = None, +def _get_otel_interceptor( + client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, + is_async: bool = False, ) -> Any: - """Applies OTel capabilities (like tracing) to the channel. - - Precondition: This function assumes `is_otel_capabilities_enabled` has already - been called and returned `True`, i.e. in the Client. At this time - this function is not intended to be standalone. + """Instantiates a sync or async OpenTelemetry gRPC client interceptor. Args: - channel: The raw gRPC channel to wrap. client_options: The client options object or dictionary. + is_async: If True, returns an async interceptor (`aio_client_interceptor`), + otherwise returns a sync interceptor (`client_interceptor`). Returns: - Any: The intercepted channel. - - Raises: - ImportError: If OpenTelemetry packages are not installed and this function - is called directly (bypassing the precondition). + Any: The instantiated OpenTelemetry client interceptor. """ import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] @@ -83,7 +76,65 @@ def apply_otel_capabilities_to_channel( elif client_options is not None: tracer_provider = getattr(client_options, _TRACER_PROVIDER, None) - interceptor = otel_grpc.client_interceptor(tracer_provider=tracer_provider) + if is_async: + return otel_grpc.aio_client_interceptor(tracer_provider=tracer_provider) + return otel_grpc.client_interceptor(tracer_provider=tracer_provider) + + +def create_channel_with_otel( + channel_factory: Callable[..., Any], + client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, + **channel_kwargs: Any, +) -> Any: + """Creates a gRPC channel using the provided factory and applies OTel capabilities if enabled. + + If OpenTelemetry capabilities are enabled (via environment variable or client_options), + the created raw channel is intercepted with an OpenTelemetry client interceptor. + Otherwise, the raw channel is returned unmodified. + + Args: + channel_factory: A callable (such as a Transport's `create_channel` classmethod) + that instantiates and returns a raw gRPC channel. + client_options: The client options object or dictionary used for feature gating + and extracting the tracer provider. + **channel_kwargs: Keyword arguments forwarded directly to `channel_factory`. + + Returns: + Any: The intercepted or raw gRPC channel. + """ + raw_channel = channel_factory(**channel_kwargs) + if is_otel_capabilities_enabled(client_options): + import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] + + interceptor = _get_otel_interceptor(client_options, is_async=False) + return otel_grpc.intercept_channel(raw_channel, interceptor) + return raw_channel + + +def create_async_channel_with_otel( + channel_factory: Callable[..., Any], + client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, + **channel_kwargs: Any, +) -> Any: + """Creates an async gRPC channel using the provided factory with OTel interceptors injected if enabled. + + Because `grpc.aio` channels are immutable after creation, any OpenTelemetry interceptor + must be passed into `channel_factory` during instantiation via the `interceptors` keyword argument. + + Args: + channel_factory: A callable (such as an Async Transport's `create_channel` classmethod) + that instantiates and returns an async gRPC channel. + client_options: The client options object or dictionary used for feature gating + and extracting the tracer provider. + **channel_kwargs: Keyword arguments forwarded directly to `channel_factory`. + + Returns: + Any: The instantiated async gRPC channel. + """ + if is_otel_capabilities_enabled(client_options): + async_interceptor = _get_otel_interceptor(client_options, is_async=True) + interceptors = list(channel_kwargs.pop("interceptors", []) or []) + interceptors.append(async_interceptor) + channel_kwargs["interceptors"] = interceptors - # We use OTel's own compatible applier to avoid standard gRPC TypeError. - return otel_grpc.intercept_channel(channel, interceptor) + return channel_factory(**channel_kwargs) diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index d39edb806040..094d201b35f1 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -86,16 +86,57 @@ def test_is_otel_capabilities_enabled_experimental_enabled_with_config(monkeypat assert _observability.is_otel_capabilities_enabled(options) -def test_apply_otel_capabilities_to_channel_enabled_otel_installed(monkeypatch): - mock_channel = mock.Mock() - mock_intercepted_channel = mock.Mock() +def test_get_otel_interceptor_sync_default(monkeypatch): + mock_otel = mock.Mock() + mock_otel_grpc = mock_otel.instrumentation.grpc + mock_interceptor = mock.Mock() + mock_otel_grpc.client_interceptor.return_value = mock_interceptor + + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation + ) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc + ) + + result = _observability._get_otel_interceptor() + assert result is mock_interceptor + mock_otel_grpc.client_interceptor.assert_called_once_with(tracer_provider=None) + + +def test_get_otel_interceptor_sync_config(monkeypatch): + mock_tracer_provider = object() + options = ClientOptions(tracer_provider=mock_tracer_provider) mock_otel = mock.Mock() mock_otel_grpc = mock_otel.instrumentation.grpc mock_interceptor = mock.Mock() + mock_otel_grpc.client_interceptor.return_value = mock_interceptor + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation + ) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc + ) + + result = _observability._get_otel_interceptor(client_options=options) + assert result is mock_interceptor + mock_otel_grpc.client_interceptor.assert_called_once_with( + tracer_provider=mock_tracer_provider + ) + + +def test_get_otel_interceptor_sync_dict_config(monkeypatch): + mock_tracer_provider = object() + options = {"tracer_provider": mock_tracer_provider} + + 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.return_value = mock_intercepted_channel monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) monkeypatch.setitem( @@ -105,29 +146,69 @@ def test_apply_otel_capabilities_to_channel_enabled_otel_installed(monkeypatch): sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc ) - result = _observability.apply_otel_capabilities_to_channel(mock_channel) + result = _observability._get_otel_interceptor(client_options=options) + assert result is mock_interceptor + mock_otel_grpc.client_interceptor.assert_called_once_with( + tracer_provider=mock_tracer_provider + ) + + +def test_get_otel_interceptor_async(monkeypatch): + mock_tracer_provider = object() + options = ClientOptions(tracer_provider=mock_tracer_provider) + + mock_otel = mock.Mock() + mock_otel_grpc = mock_otel.instrumentation.grpc + mock_async_interceptor = mock.Mock() + mock_otel_grpc.aio_client_interceptor.return_value = mock_async_interceptor + + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation + ) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc + ) + + result = _observability._get_otel_interceptor(client_options=options, is_async=True) + assert result is mock_async_interceptor + mock_otel_grpc.aio_client_interceptor.assert_called_once_with( + tracer_provider=mock_tracer_provider + ) - assert result is mock_intercepted_channel - mock_otel_grpc.client_interceptor.assert_called_once_with(tracer_provider=None) - mock_otel_grpc.intercept_channel.assert_called_once_with( - mock_channel, mock_interceptor + +def test_create_channel_with_otel_disabled(monkeypatch): + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") + mock_raw_channel = mock.Mock(name="raw_channel") + mock_channel_factory = mock.Mock(return_value=mock_raw_channel) + + result = _observability.create_channel_with_otel( + mock_channel_factory, + target="example.com:443", + credentials="mock_creds", ) + assert result is mock_raw_channel + mock_channel_factory.assert_called_once_with( + target="example.com:443", credentials="mock_creds" + ) -def test_apply_otel_capabilities_to_channel_enabled_via_config(monkeypatch): - # Tracing enabled via config (tracer_provider is set) + +def test_create_channel_with_otel_enabled(monkeypatch): + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") mock_tracer_provider = object() options = ClientOptions(tracer_provider=mock_tracer_provider) - mock_channel = mock.Mock() - mock_intercepted_channel = mock.Mock() + mock_raw_channel = mock.Mock(name="raw_channel") + mock_wrapped_channel = mock.Mock(name="wrapped_channel") + mock_channel_factory = mock.Mock(return_value=mock_raw_channel) 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.return_value = mock_intercepted_channel + mock_otel_grpc.intercept_channel.return_value = mock_wrapped_channel monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) monkeypatch.setitem( @@ -137,33 +218,57 @@ def test_apply_otel_capabilities_to_channel_enabled_via_config(monkeypatch): sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc ) - result = _observability.apply_otel_capabilities_to_channel( - mock_channel, client_options=options + result = _observability.create_channel_with_otel( + mock_channel_factory, + client_options=options, + target="example.com:443", + credentials="mock_creds", ) - assert result is mock_intercepted_channel + assert result is mock_wrapped_channel + mock_channel_factory.assert_called_once_with( + target="example.com:443", credentials="mock_creds" + ) mock_otel_grpc.client_interceptor.assert_called_once_with( tracer_provider=mock_tracer_provider ) mock_otel_grpc.intercept_channel.assert_called_once_with( - mock_channel, mock_interceptor + mock_raw_channel, mock_interceptor + ) + + +def test_create_async_channel_with_otel_disabled(monkeypatch): + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") + mock_async_channel = mock.Mock(name="async_channel") + mock_channel_factory = mock.Mock(return_value=mock_async_channel) + user_interceptor = mock.Mock(name="user_interceptor") + + result = _observability.create_async_channel_with_otel( + mock_channel_factory, + target="example.com:443", + interceptors=[user_interceptor], + ) + + assert result is mock_async_channel + mock_channel_factory.assert_called_once_with( + target="example.com:443", + interceptors=[user_interceptor], ) -def test_apply_otel_capabilities_to_channel_enabled_via_dict_config(monkeypatch): - # Tracing enabled via dict config +def test_create_async_channel_with_otel_enabled(monkeypatch): + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") mock_tracer_provider = object() - options = {"tracer_provider": mock_tracer_provider} + options = ClientOptions(tracer_provider=mock_tracer_provider) - mock_channel = mock.Mock() - mock_intercepted_channel = mock.Mock() + mock_async_channel = mock.Mock(name="async_channel") + mock_channel_factory = mock.Mock(return_value=mock_async_channel) + user_interceptor = mock.Mock(name="user_interceptor") + mock_async_interceptor = mock.Mock(name="otel_async_interceptor") 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.return_value = mock_intercepted_channel + mock_otel_grpc.aio_client_interceptor.return_value = mock_async_interceptor monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) monkeypatch.setitem( @@ -173,14 +278,18 @@ def test_apply_otel_capabilities_to_channel_enabled_via_dict_config(monkeypatch) sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc ) - result = _observability.apply_otel_capabilities_to_channel( - mock_channel, client_options=options + result = _observability.create_async_channel_with_otel( + mock_channel_factory, + client_options=options, + target="example.com:443", + interceptors=[user_interceptor], ) - assert result is mock_intercepted_channel - mock_otel_grpc.client_interceptor.assert_called_once_with( + assert result is mock_async_channel + mock_otel_grpc.aio_client_interceptor.assert_called_once_with( tracer_provider=mock_tracer_provider ) - mock_otel_grpc.intercept_channel.assert_called_once_with( - mock_channel, mock_interceptor + mock_channel_factory.assert_called_once_with( + target="example.com:443", + interceptors=[user_interceptor, mock_async_interceptor], ) From c3b23a96ef0a139959c4cbeeec492176d9ecacc3 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 27 Aug 2026 13:11:19 -0400 Subject: [PATCH 2/4] refactor(api-core): simplify async interceptors extraction and expand tests - Use list(channel_kwargs.pop('interceptors', None) or []) in create_async_channel_with_otel - Add unit tests for None and omitted interceptors arguments --- .../google/api_core/_observability.py | 2 +- .../tests/unit/test_observability.py | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index 858451db6ac4..d8881d15c601 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -133,7 +133,7 @@ def create_async_channel_with_otel( """ if is_otel_capabilities_enabled(client_options): async_interceptor = _get_otel_interceptor(client_options, is_async=True) - interceptors = list(channel_kwargs.pop("interceptors", []) or []) + interceptors = list(channel_kwargs.pop("interceptors", None) or []) interceptors.append(async_interceptor) channel_kwargs["interceptors"] = interceptors diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index 094d201b35f1..edae8c70699d 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -293,3 +293,76 @@ def test_create_async_channel_with_otel_enabled(monkeypatch): target="example.com:443", interceptors=[user_interceptor, mock_async_interceptor], ) + + +def test_create_async_channel_with_otel_none_interceptors(monkeypatch): + mock_tracer_provider = object() + options = ClientOptions(tracer_provider=mock_tracer_provider) + + mock_async_channel = mock.Mock(name="async_channel") + mock_channel_factory = mock.Mock(return_value=mock_async_channel) + mock_async_interceptor = mock.Mock(name="otel_async_interceptor") + + mock_otel = mock.Mock() + mock_otel_grpc = mock_otel.instrumentation.grpc + mock_otel_grpc.aio_client_interceptor.return_value = mock_async_interceptor + + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation + ) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc + ) + + result = _observability.create_async_channel_with_otel( + mock_channel_factory, + client_options=options, + target="example.com:443", + interceptors=None, + ) + + assert result is mock_async_channel + mock_otel_grpc.aio_client_interceptor.assert_called_once_with( + tracer_provider=mock_tracer_provider + ) + mock_channel_factory.assert_called_once_with( + target="example.com:443", + interceptors=[mock_async_interceptor], + ) + + +def test_create_async_channel_with_otel_omitted_interceptors(monkeypatch): + mock_tracer_provider = object() + options = ClientOptions(tracer_provider=mock_tracer_provider) + + mock_async_channel = mock.Mock(name="async_channel") + mock_channel_factory = mock.Mock(return_value=mock_async_channel) + mock_async_interceptor = mock.Mock(name="otel_async_interceptor") + + mock_otel = mock.Mock() + mock_otel_grpc = mock_otel.instrumentation.grpc + mock_otel_grpc.aio_client_interceptor.return_value = mock_async_interceptor + + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation + ) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc + ) + + result = _observability.create_async_channel_with_otel( + mock_channel_factory, + client_options=options, + target="example.com:443", + ) + + assert result is mock_async_channel + mock_otel_grpc.aio_client_interceptor.assert_called_once_with( + tracer_provider=mock_tracer_provider + ) + mock_channel_factory.assert_called_once_with( + target="example.com:443", + interceptors=[mock_async_interceptor], + ) From d1f4ccd0f5e37195efd367adf92d5b3dd85920aa Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 28 Aug 2026 09:15:47 -0400 Subject: [PATCH 3/4] feat(api-core): support positional *channel_args and partial application in channel factories - Add *channel_args to create_channel_with_otel and create_async_channel_with_otel - Make client_options keyword-only to prevent argument collision with functools.partial - Add TDD unit tests with detailed docstrings for positional forwarding and partial binding --- .../google/api_core/_observability.py | 12 +- .../tests/unit/test_observability.py | 152 ++++++++++++++++++ 2 files changed, 162 insertions(+), 2 deletions(-) diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index d8881d15c601..32193b5b2eca 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -83,6 +83,7 @@ def _get_otel_interceptor( def create_channel_with_otel( channel_factory: Callable[..., Any], + *channel_args: Any, client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, **channel_kwargs: Any, ) -> Any: @@ -97,12 +98,15 @@ def create_channel_with_otel( that instantiates and returns a raw gRPC channel. client_options: The client options object or dictionary used for feature gating and extracting the tracer provider. + *channel_args: Positional arguments forwarded directly to `channel_factory` (e.g. `host`). + Supporting positional arguments allows this function to be easily bound via + `functools.partial` in Client initialization. **channel_kwargs: Keyword arguments forwarded directly to `channel_factory`. Returns: Any: The intercepted or raw gRPC channel. """ - raw_channel = channel_factory(**channel_kwargs) + raw_channel = channel_factory(*channel_args, **channel_kwargs) if is_otel_capabilities_enabled(client_options): import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] @@ -113,6 +117,7 @@ def create_channel_with_otel( def create_async_channel_with_otel( channel_factory: Callable[..., Any], + *channel_args: Any, client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, **channel_kwargs: Any, ) -> Any: @@ -126,6 +131,9 @@ def create_async_channel_with_otel( that instantiates and returns an async gRPC channel. client_options: The client options object or dictionary used for feature gating and extracting the tracer provider. + *channel_args: Positional arguments forwarded directly to `channel_factory` (e.g. `host`). + Supporting positional arguments allows this function to be easily bound via + `functools.partial` in Client initialization. **channel_kwargs: Keyword arguments forwarded directly to `channel_factory`. Returns: @@ -137,4 +145,4 @@ def create_async_channel_with_otel( interceptors.append(async_interceptor) channel_kwargs["interceptors"] = interceptors - return channel_factory(**channel_kwargs) + return channel_factory(*channel_args, **channel_kwargs) diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index edae8c70699d..1c1dc937a0b0 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import functools import sys from unittest import mock @@ -366,3 +367,154 @@ def test_create_async_channel_with_otel_omitted_interceptors(monkeypatch): target="example.com:443", interceptors=[mock_async_interceptor], ) + + +def test_create_channel_with_otel_positional_args(monkeypatch): + """Proves that create_channel_with_otel forwards positional arguments (*channel_args) + to the underlying channel_factory callable. + + Why this matters: Transports pass host as a positional argument + (e.g., channel_init(self._host, credentials=...)), so the helper must pass + positional arguments through without argument-binding errors. + """ + mock_raw_channel = mock.Mock(name="raw_channel") + mock_channel_factory = mock.Mock(return_value=mock_raw_channel) + + result = _observability.create_channel_with_otel( + mock_channel_factory, + "example.com:443", # positional host argument + credentials="mock_creds", + ) + + assert result is mock_raw_channel + mock_channel_factory.assert_called_once_with( + "example.com:443", credentials="mock_creds" + ) + + +def test_create_channel_with_otel_partial_application(monkeypatch): + """Proves that create_channel_with_otel can be bound with functools.partial + (e.g. functools.partial(create_channel_with_otel, channel_factory, client_options=options)) + and subsequently called by a Transport with positional (*channel_args) and keyword (**channel_kwargs) args. + + Why this matters: This allows Client.__init__ to pass a lazy factory to + Transport(channel=partial(...)) without needing to eagerly extract and duplicate + credentials, scopes, and quota_project_id. + """ + mock_tracer_provider = object() + options = ClientOptions(tracer_provider=mock_tracer_provider) + + mock_raw_channel = mock.Mock(name="raw_channel") + mock_wrapped_channel = mock.Mock(name="wrapped_channel") + mock_channel_factory = mock.Mock(return_value=mock_raw_channel) + + 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.return_value = mock_wrapped_channel + + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation + ) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc + ) + + # 1. Client creates lazy factory using functools.partial + lazy_factory = functools.partial( + _observability.create_channel_with_otel, + mock_channel_factory, + client_options=options, + ) + + # 2. Transport invokes the factory passing host positionally and credentials by keyword + result = lazy_factory( + "secretmanager.googleapis.com:443", + credentials="mock_credentials", + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + + assert result is mock_wrapped_channel + mock_channel_factory.assert_called_once_with( + "secretmanager.googleapis.com:443", + credentials="mock_credentials", + scopes=["https://www.googleapis.com/auth/cloud-platform"], + ) + mock_otel_grpc.client_interceptor.assert_called_once_with( + tracer_provider=mock_tracer_provider + ) + mock_otel_grpc.intercept_channel.assert_called_once_with( + mock_raw_channel, mock_interceptor + ) + + +def test_create_async_channel_with_otel_positional_args(monkeypatch): + """Proves that create_async_channel_with_otel forwards positional arguments (*channel_args) + to the underlying async channel_factory callable. + """ + mock_async_channel = mock.Mock(name="async_channel") + mock_channel_factory = mock.Mock(return_value=mock_async_channel) + + result = _observability.create_async_channel_with_otel( + mock_channel_factory, + "example.com:443", # positional host argument + credentials="mock_creds", + ) + + assert result is mock_async_channel + mock_channel_factory.assert_called_once_with( + "example.com:443", credentials="mock_creds" + ) + + +def test_create_async_channel_with_otel_partial_application(monkeypatch): + """Proves that create_async_channel_with_otel can be bound with functools.partial + and called by an Async Transport with positional host and keyword arguments, + injecting the async OTel interceptor seamlessly into kwargs['interceptors']. + """ + mock_tracer_provider = object() + options = ClientOptions(tracer_provider=mock_tracer_provider) + + mock_async_channel = mock.Mock(name="async_channel") + mock_channel_factory = mock.Mock(return_value=mock_async_channel) + user_interceptor = mock.Mock(name="user_interceptor") + mock_async_interceptor = mock.Mock(name="otel_async_interceptor") + + mock_otel = mock.Mock() + mock_otel_grpc = mock_otel.instrumentation.grpc + mock_otel_grpc.aio_client_interceptor.return_value = mock_async_interceptor + + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation + ) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc + ) + + # 1. Client creates lazy factory using functools.partial + lazy_factory = functools.partial( + _observability.create_async_channel_with_otel, + mock_channel_factory, + client_options=options, + ) + + # 2. Transport invokes the factory passing host positionally and interceptors by keyword + result = lazy_factory( + "secretmanager.googleapis.com:443", + credentials="mock_credentials", + interceptors=[user_interceptor], + ) + + assert result is mock_async_channel + mock_otel_grpc.aio_client_interceptor.assert_called_once_with( + tracer_provider=mock_tracer_provider + ) + mock_channel_factory.assert_called_once_with( + "secretmanager.googleapis.com:443", + credentials="mock_credentials", + interceptors=[user_interceptor, mock_async_interceptor], + ) From cf9552348e253738da5fb579437cbcc256be771c Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 28 Aug 2026 09:56:13 -0400 Subject: [PATCH 4/4] test(api-core): update eager channel tests to set experimental env var --- packages/google-api-core/tests/unit/test_observability.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index 1c1dc937a0b0..7b4b95341b63 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -297,6 +297,7 @@ def test_create_async_channel_with_otel_enabled(monkeypatch): def test_create_async_channel_with_otel_none_interceptors(monkeypatch): + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") mock_tracer_provider = object() options = ClientOptions(tracer_provider=mock_tracer_provider) @@ -334,6 +335,7 @@ def test_create_async_channel_with_otel_none_interceptors(monkeypatch): def test_create_async_channel_with_otel_omitted_interceptors(monkeypatch): + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") mock_tracer_provider = object() options = ClientOptions(tracer_provider=mock_tracer_provider) @@ -401,6 +403,7 @@ def test_create_channel_with_otel_partial_application(monkeypatch): Transport(channel=partial(...)) without needing to eagerly extract and duplicate credentials, scopes, and quota_project_id. """ + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") mock_tracer_provider = object() options = ClientOptions(tracer_provider=mock_tracer_provider) @@ -475,6 +478,7 @@ def test_create_async_channel_with_otel_partial_application(monkeypatch): and called by an Async Transport with positional host and keyword arguments, injecting the async OTel interceptor seamlessly into kwargs['interceptors']. """ + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") mock_tracer_provider = object() options = ClientOptions(tracer_provider=mock_tracer_provider)