From 96243d5710b28cbe2be4d081cf3849c0ea27db0c Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 27 Aug 2026 11:22:06 -0400 Subject: [PATCH 1/6] 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 f0ebe0afc14d..0ef3169fb5da 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -87,16 +87,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( @@ -106,29 +147,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( @@ -138,33 +219,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( @@ -174,14 +279,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 16fc522ac5588c9a605a9af06e115225ea203240 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 27 Aug 2026 13:11:19 -0400 Subject: [PATCH 2/6] 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 0ef3169fb5da..9eaf5920e1f8 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -294,3 +294,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 7f37d3aecaaa167e409aacd24a2aa278ba7cb1cf Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 28 Aug 2026 09:15:47 -0400 Subject: [PATCH 3/6] 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 9eaf5920e1f8..425392dba0d0 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 @@ -367,3 +368,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 3a8426164fea33df17aa5eafb6f8129323dced72 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 28 Aug 2026 09:56:13 -0400 Subject: [PATCH 4/6] 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 425392dba0d0..eccd7c88b37e 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -298,6 +298,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) @@ -335,6 +336,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) @@ -402,6 +404,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) @@ -476,6 +479,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) From 9d8d54285d6fd6811269822d2146a5aaa22128ba Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 31 Aug 2026 09:34:18 -0400 Subject: [PATCH 5/6] feat(api-core): add OpenTelemetry channel wrapper and async interceptor helpers --- .../google/api_core/_observability.py | 74 ++--- .../tests/unit/test_observability.py | 291 ++---------------- 2 files changed, 61 insertions(+), 304 deletions(-) diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index 32193b5b2eca..5aa18afd7b60 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -16,16 +16,22 @@ """OpenTelemetry helpers for resolving and instantiating interceptors.""" -from typing import Any, Callable, Optional, Union +import functools +from typing import TYPE_CHECKING, Any, Callable, Optional, Union from google.api_core import _feature_gating_helpers from google.api_core.client_options import ClientOptions +if TYPE_CHECKING: + from google.api_core.grpc_helpers import ChannelWrapperCallable +else: + ChannelWrapperCallable = Callable[[Any], Any] + _TRACER_PROVIDER = "tracer_provider" def is_otel_capabilities_enabled( - client_options: Optional[ClientOptions | dict[str, Any]] = None, + client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, env_var: str = "GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", ) -> bool: """Checks if OTel capabilities are enabled and installed. @@ -81,68 +87,42 @@ def _get_otel_interceptor( return otel_grpc.client_interceptor(tracer_provider=tracer_provider) -def create_channel_with_otel( - channel_factory: Callable[..., Any], - *channel_args: Any, +def get_otel_channel_wrapper( 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. +) -> Optional[ChannelWrapperCallable]: + """Returns a channel wrapper callable that wraps a sync gRPC channel with OpenTelemetry tracing. 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_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. + Optional[ChannelWrapperCallable]: A channel-wrapping callable if OpenTelemetry + tracing is enabled and installed, None otherwise. """ - 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] + if not is_otel_capabilities_enabled(client_options): + return None - interceptor = _get_otel_interceptor(client_options, is_async=False) - return otel_grpc.intercept_channel(raw_channel, interceptor) - return raw_channel + import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] + interceptor = _get_otel_interceptor(client_options, is_async=False) + return functools.partial(otel_grpc.intercept_channel, interceptor=interceptor) -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: - """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. +def get_otel_async_interceptor( + client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, +) -> Optional[Any]: + """Returns an async gRPC client interceptor for OpenTelemetry tracing. 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_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 instantiated async gRPC channel. + Optional[Any]: An instantiated OpenTelemetry async client interceptor + if tracing is enabled and installed, None otherwise. """ - if is_otel_capabilities_enabled(client_options): - async_interceptor = _get_otel_interceptor(client_options, is_async=True) - interceptors = list(channel_kwargs.pop("interceptors", None) or []) - interceptors.append(async_interceptor) - channel_kwargs["interceptors"] = interceptors + if not is_otel_capabilities_enabled(client_options): + return None - return channel_factory(*channel_args, **channel_kwargs) + return _get_otel_interceptor(client_options, is_async=True) diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index eccd7c88b37e..4005aa14fc8e 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import functools import sys from unittest import mock @@ -179,35 +178,28 @@ def test_get_otel_interceptor_async(monkeypatch): ) -def test_create_channel_with_otel_disabled(monkeypatch): +def test_get_otel_channel_wrapper_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) + assert _observability.get_otel_channel_wrapper() is None - 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_get_otel_channel_wrapper_otel_missing(monkeypatch): + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", None) + assert _observability.get_otel_channel_wrapper() is None -def test_create_channel_with_otel_enabled(monkeypatch): +def test_get_otel_channel_wrapper_enabled(monkeypatch): monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") 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_interceptor = mock.Mock(name="otel_interceptor") mock_otel_grpc.client_interceptor.return_value = mock_interceptor mock_otel_grpc.intercept_channel.return_value = mock_wrapped_channel @@ -220,201 +212,35 @@ def test_create_channel_with_otel_enabled(monkeypatch): sys.modules, "opentelemetry.instrumentation.grpc", mock_otel_grpc ) - result = _observability.create_channel_with_otel( - mock_channel_factory, - client_options=options, - target="example.com:443", - credentials="mock_creds", - ) + wrapper = _observability.get_otel_channel_wrapper(client_options=options) + assert callable(wrapper) - 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_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_create_async_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_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 - ) - - result = _observability.create_async_channel_with_otel( - mock_channel_factory, - client_options=options, - target="example.com:443", - 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( - target="example.com:443", - interceptors=[user_interceptor, mock_async_interceptor], - ) - - -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) - - 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): - monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") - 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], - ) - - -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" + result = wrapper(mock_raw_channel) + assert result is mock_wrapped_channel + mock_otel_grpc.intercept_channel.assert_called_once_with( + mock_raw_channel, interceptor=mock_interceptor ) -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. +def test_get_otel_channel_wrapper_with_apply_channel_wrappers(monkeypatch): + """Proves that get_otel_channel_wrapper integrates seamlessly into apply_channel_wrappers.""" + pytest.importorskip("grpc") + from google.api_core import grpc_helpers - 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. - """ monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") 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_interceptor = mock.Mock(name="otel_interceptor") mock_otel_grpc.client_interceptor.return_value = mock_interceptor mock_otel_grpc.intercept_channel.return_value = mock_wrapped_channel @@ -427,65 +253,34 @@ def test_create_channel_with_otel_partial_application(monkeypatch): 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, - ) + otel_wrapper = _observability.get_otel_channel_wrapper(client_options=options) + assert callable(otel_wrapper) - # 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"], + result = grpc_helpers.apply_channel_wrappers( + mock_raw_channel, wrappers=[otel_wrapper] ) - 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 + mock_raw_channel, interceptor=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) +def test_get_otel_async_interceptor_disabled(monkeypatch): + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") + assert _observability.get_otel_async_interceptor() is None - 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_get_otel_async_interceptor_otel_missing(monkeypatch): + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", None) + assert _observability.get_otel_async_interceptor() is None -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']. - """ +def test_get_otel_async_interceptor_enabled(monkeypatch): monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") 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() @@ -500,26 +295,8 @@ def test_create_async_channel_with_otel_partial_application(monkeypatch): 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 + result = _observability.get_otel_async_interceptor(client_options=options) + assert result is mock_async_interceptor 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 d751865610b7ab3995411d13486b93e3aee0be2f Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Mon, 31 Aug 2026 09:47:29 -0400 Subject: [PATCH 6/6] refactor(api-core): use PEP 604 union syntax in _observability.py --- .../google/api_core/_observability.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index 5aa18afd7b60..701829796827 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -17,7 +17,7 @@ """OpenTelemetry helpers for resolving and instantiating interceptors.""" import functools -from typing import TYPE_CHECKING, Any, Callable, Optional, Union +from typing import TYPE_CHECKING, Any, Callable from google.api_core import _feature_gating_helpers from google.api_core.client_options import ClientOptions @@ -31,7 +31,7 @@ def is_otel_capabilities_enabled( - client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, + client_options: ClientOptions | dict[str, Any] | None = None, env_var: str = "GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", ) -> bool: """Checks if OTel capabilities are enabled and installed. @@ -61,7 +61,7 @@ def is_otel_capabilities_enabled( def _get_otel_interceptor( - client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, + client_options: ClientOptions | dict[str, Any] | None = None, is_async: bool = False, ) -> Any: """Instantiates a sync or async OpenTelemetry gRPC client interceptor. @@ -88,8 +88,8 @@ def _get_otel_interceptor( def get_otel_channel_wrapper( - client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, -) -> Optional[ChannelWrapperCallable]: + client_options: ClientOptions | dict[str, Any] | None = None, +) -> ChannelWrapperCallable | None: """Returns a channel wrapper callable that wraps a sync gRPC channel with OpenTelemetry tracing. Args: @@ -110,8 +110,8 @@ def get_otel_channel_wrapper( def get_otel_async_interceptor( - client_options: Optional[Union[ClientOptions, dict[str, Any]]] = None, -) -> Optional[Any]: + client_options: ClientOptions | dict[str, Any] | None = None, +) -> Any | None: """Returns an async gRPC client interceptor for OpenTelemetry tracing. Args: