feat(api-core): add ClientInterceptor and apply_interceptors helper (A) - #18236
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces the apply_interceptors helper function to sequentially apply a list of client interceptors to a gRPC channel, along with comprehensive unit tests verifying its behavior. The reviewer feedback correctly points out that applying interceptors sequentially in a loop introduces unnecessary nesting overhead and reverses the standard gRPC execution order. To resolve this, the reviewer suggests unpacking the interceptors directly into a single grpc.intercept_channel call and updating the corresponding execution order test assertion.
…terceptor unit tests
…HON_TRACING_ENABLED - Set is_otel_capabilities_enabled default env_var to GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED - Activate fail-fast FeatureGatingError experimental path when tracer_provider is set without env var - Update unit tests to verify experimental gating behavior
394451b to
8b2dc1f
Compare
| from re import match | ||
|
|
||
| import pytest | ||
|
|
There was a problem hiding this comment.
Note
Blank line(s) introduced by ruff and "import order" sorting process.
| assert channel.close() is None | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("falsy_wrappers", [None, [], ()]) |
There was a problem hiding this comment.
As noted previously. Happy to revisit these in a fast-follow PR to apply fixtures, reusable functions, and/or parametrizations to reduce the size/complexity of the test suite.
Given a preference, would like to ensure this gets merged before coming back to invest heavily in what might otherwise be premature optimization.
This pull request introduces OpenTelemetry helper functions in `google.api_core._observability` to produce channel wrappers for synchronous gRPC channels and interceptors for asynchronous gRPC channels. ### Problem Generated client libraries need a consistent and maintainable way to instrument gRPC channels with OpenTelemetry tracing when enabled via environment variables or client options. Because synchronous gRPC channels can be wrapped post-creation while asynchronous gRPC channels require interceptors at channel creation time, client transports need helpers that return the appropriate channel wrapper or async interceptors without duplicating OpenTelemetry resolution logic across client libraries. ### Solution This pull request introduces the following helper functions in `google.api_core._observability`: 1. `get_otel_channel_wrapper(client_options)`: * Returns a channel-wrapping function (`Callable[[Channel], Channel]`) for synchronous gRPC channels when OpenTelemetry tracing is enabled and installed. * Integrates with `grpc_helpers.apply_channel_wrappers` to wrap raw channels using OpenTelemetry's `intercept_channel`. 2. `get_otel_async_interceptor(client_options)`: * Returns a list of OpenTelemetry asynchronous client interceptors (`aio_client_interceptors`) for use when constructing `grpc.aio` channels. 3. `_get_otel_interceptor(client_options, is_async)`: * Internal helper that extracts `tracer_provider` from `ClientOptions` and creates the appropriate OpenTelemetry sync or async interceptors. ### Testing * Added unit tests in `tests/unit/test_observability.py` covering: * Sync and async interceptor extraction and `tracer_provider` configuration. * `get_otel_channel_wrapper` behavior when tracing is disabled, when OpenTelemetry is not installed, and when tracing is enabled. * Integration between `get_otel_channel_wrapper` and `grpc_helpers.apply_channel_wrappers`. * `get_otel_async_interceptor` behavior across disabled, missing, and enabled states. ### Notes for Reviewers * This PR builds upon PR #18236 (`ChannelWrapper` and `apply_channel_wrappers`). * `get_otel_channel_wrapper` returns a callable rather than modifying the channel immediately, allowing transport layers to combine OpenTelemetry wrapping with user-supplied custom channel wrappers.
daniel-sanche
left a comment
There was a problem hiding this comment.
LGTM, but if it's possible to improve the types before merging, that would be great
| Raises: | ||
| ImportError: If OpenTelemetry packages are not installed and this function | ||
| is called directly (bypassing the precondition). | ||
| Any: The instantiated OpenTelemetry client interceptor. |
There was a problem hiding this comment.
Is it possible to use a concrete type here?
There was a problem hiding this comment.
I will update this will a follow-on PR.
…cer provider (#18263) ## Summary Refactors `google.api_core._observability` to simplify OpenTelemetry interceptor instantiation and use concrete return types: - Extracts `_get_tracer_provider(client_options)` to centralize tracer provider lookup across `ClientOptions` objects and dictionaries. - Removes the private `_get_otel_interceptor(is_async: bool)` helper, allowing `get_otel_interceptor` and `get_otel_async_interceptor` to directly invoke synchronous and asynchronous OpenTelemetry APIs with concrete return types. - Adds dedicated unit tests for `_get_tracer_provider` and maintains symmetrical tests for sync and async interceptor getters. Addresses review feedback on #18236.
Problem
Generated client libraries and transports need a centralized, maintainable way in
google-api-coreto apply gRPC channel wrappers (standard client interceptors as well as custom channel-wrapping callables) in a clean, order-preserving pipeline. Without a shared helper, downstream packages must duplicate wrapping loops or risk wrapper ordering issues.Because synchronous gRPC channels can be wrapped post-creation while asynchronous gRPC channels require interceptors at channel creation time, client transports need helpers that return the appropriate channel wrapper or async interceptors without duplicating OpenTelemetry resolution logic across client libraries.
Additionally, OpenTelemetry tracing support in client initialization defaults to the EXPERIMENTAL token for experimental feature gating to prevent premature invocation of in-development capabilities.
Solution
This PR introduces the following foundational utilities to
google-api-core:gRPC Channel Wrapper Utilities (
google.api_core.grpc_helpers):ClientInterceptor: Type alias representing any client-side gRPC interceptor (UnaryUnaryClientInterceptor,UnaryStreamClientInterceptor,StreamUnaryClientInterceptor,StreamStreamClientInterceptor).ChannelWrapperCallable: Type alias representing a channel-wrapping callable (Callable[[grpc.Channel], grpc.Channel]).ChannelWrapper: Generic union type alias representing any channel wrapper (Union[ClientInterceptor, ChannelWrapperCallable]).apply_channel_wrappers: Applies an optional sequence of channel wrappers to agrpc.Channelin reverse order so the first item in the sequence becomes the outermost layer on outbound requests. Returns the original channel unmodified ifwrappersisNoneor empty.get_otel_channel_wrapper(client_options): Returns a channel-wrapping function (Callable[[Channel], Channel]) for synchronous gRPC channels when OpenTelemetry tracing is enabled and installed.get_otel_async_interceptor(client_options): Returns a list of OpenTelemetry asynchronous client interceptors (aio_client_interceptors) for use when constructinggrpc.aiochannels.grpc_helpers.apply_channel_wrappersto wrap raw channels using OpenTelemetry'sintercept_channel._get_otel_interceptor(client_options, is_async): Internal helper that extractstracer_providerfromClientOptionsand creates the appropriate OpenTelemetry sync or async interceptors.Experimental Feature Gating for Tracing (
google.api_core._observability):is_otel_capabilities_enabledtoGOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED.ClientOptions.tracer_providerwithout setting the experimental environment variable raisesFeatureGatingError.Testing
test_grpc_helpers.pycovering passthrough behavior, pureClientInterceptorsequences, pure callable sequences, interspersed[interceptor, callable]sequences, andTypeErrorvalidation on invalid types.test_observability.pyvalidating experimental feature gating (FeatureGatingErrorfail-fast validation and successful enablement).tests/unit/test_observability.pycovering:tracer_providerconfiguration.get_otel_channel_wrapperbehavior when tracing is disabled, when OpenTelemetry is not installed, and when tracing is enabled.get_otel_channel_wrapperandgrpc_helpers.apply_channel_wrappers.get_otel_async_interceptorbehavior across disabled, missing, and enabled states.Notes for Reviewers
get_otel_channel_wrapperreturns a callable rather than modifying the channel immediately, allowing transport layers to combine OpenTelemetry wrapping with user-supplied custom channel wrappers.