From 6b941c8c01aa13f29b961a6344b54fdb137cb063 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 15 Oct 2026 13:47:38 -0400 Subject: [PATCH 01/11] feat(observability): suppress redundant gRPC auto-instrumentation spans --- .../%service/transports/grpc_asyncio.py.j2 | 18 +- .../%name_%version/%sub/test_%service.py.j2 | 14 +- .../asset_service/transports/grpc_asyncio.py | 18 +- .../unit/gapic/asset_v1/test_asset_service.py | 14 +- .../transports/grpc_asyncio.py | 18 +- .../credentials_v1/test_iam_credentials.py | 14 +- .../eventarc/transports/grpc_asyncio.py | 18 +- .../unit/gapic/eventarc_v1/test_eventarc.py | 14 +- .../transports/grpc_asyncio.py | 18 +- .../transports/grpc_asyncio.py | 18 +- .../transports/grpc_asyncio.py | 18 +- .../logging_v2/test_config_service_v2.py | 14 +- .../logging_v2/test_logging_service_v2.py | 14 +- .../logging_v2/test_metrics_service_v2.py | 14 +- .../transports/grpc_asyncio.py | 18 +- .../transports/grpc_asyncio.py | 18 +- .../transports/grpc_asyncio.py | 18 +- .../logging_v2/test_config_service_v2.py | 14 +- .../logging_v2/test_logging_service_v2.py | 14 +- .../logging_v2/test_metrics_service_v2.py | 14 +- .../cloud_redis/transports/grpc_asyncio.py | 18 +- .../unit/gapic/redis_v1/test_cloud_redis.py | 14 +- .../cloud_redis/transports/grpc_asyncio.py | 18 +- .../unit/gapic/redis_v1/test_cloud_redis.py | 14 +- .../services/echo/transports/grpc_asyncio.py | 18 +- .../identity/transports/grpc_asyncio.py | 18 +- .../messaging/transports/grpc_asyncio.py | 18 +- .../transports/grpc_asyncio.py | 18 +- .../transports/grpc_asyncio.py | 18 +- .../testing/transports/grpc_asyncio.py | 18 +- .../unit/gapic/showcase_v1beta1/test_echo.py | 14 +- .../gapic/showcase_v1beta1/test_identity.py | 14 +- .../gapic/showcase_v1beta1/test_messaging.py | 14 +- .../test_resumable_upload_service.py | 14 +- .../showcase_v1beta1/test_sequence_service.py | 14 +- .../gapic/showcase_v1beta1/test_testing.py | 14 +- .../transports/grpc_asyncio.py | 18 +- .../test_storage_batch_operations.py | 14 +- .../tests/system/test_tracing.py | 135 +++++++++- .../google/api_core/_observability.py | 233 +++++++++++++++++- .../tests/asyncio/test_observability_async.py | 179 ++++++++++++++ packages/google-api-core/tests/conftest.py | 19 ++ .../tests/unit/test_observability.py | 215 +++++++++------- 43 files changed, 1033 insertions(+), 356 deletions(-) create mode 100644 packages/google-api-core/tests/asyncio/test_observability_async.py diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 894fdd9de946..fe19734252ec 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -333,21 +333,27 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 5b3c39c061ec..c6d61c83359a 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1045,20 +1045,19 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_channel_intercepto @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_{{ service.name|snake_case }}_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.{{ service.grpc_asyncio_transport_name }}, "create_channel", @@ -1071,9 +1070,8 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_otel_channel_inter client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py index 7a8f6ff641cc..044360293d13 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py @@ -319,21 +319,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index b19ad4ebf79f..9bddb6bf4b41 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -900,20 +900,19 @@ def test_asset_service_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_asset_service_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.AssetServiceGrpcAsyncIOTransport, "create_channel", @@ -926,9 +925,8 @@ def test_asset_service_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py index 19603cc28836..e582999d3ba4 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py @@ -324,21 +324,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index 4b13be93cc7c..3b186bf7a53d 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -890,20 +890,19 @@ def test_iam_credentials_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_iam_credentials_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.IAMCredentialsGrpcAsyncIOTransport, "create_channel", @@ -916,9 +915,8 @@ def test_iam_credentials_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py index d350d3d2d3c0..a8824568ce4a 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py @@ -333,21 +333,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index 52b232686c64..9dd4a4da6b9a 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -921,20 +921,19 @@ def test_eventarc_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_eventarc_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.EventarcGrpcAsyncIOTransport, "create_channel", @@ -947,9 +946,8 @@ def test_eventarc_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index fa63ababe2ae..b4f164a93970 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -319,21 +319,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 4c2ccce63e6d..873f2060b30f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -317,21 +317,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index 00d65df94ad2..bf0936a88140 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -317,21 +317,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index afb0002126cf..e0a039bb5329 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -876,20 +876,19 @@ def test_config_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.ConfigServiceV2GrpcAsyncIOTransport, "create_channel", @@ -902,9 +901,8 @@ def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 4f3443ffa649..b65230f51039 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -877,20 +877,19 @@ def test_logging_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.LoggingServiceV2GrpcAsyncIOTransport, "create_channel", @@ -903,9 +902,8 @@ def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index a54c2753e0e5..dfc28a660b34 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -875,20 +875,19 @@ def test_metrics_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.MetricsServiceV2GrpcAsyncIOTransport, "create_channel", @@ -901,9 +900,8 @@ def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index fa63ababe2ae..b4f164a93970 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -319,21 +319,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 4c2ccce63e6d..873f2060b30f 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -317,21 +317,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index 00d65df94ad2..bf0936a88140 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -317,21 +317,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index bc4c58a995f5..e8fb09f043d7 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -876,20 +876,19 @@ def test_config_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.ConfigServiceV2GrpcAsyncIOTransport, "create_channel", @@ -902,9 +901,8 @@ def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index 4f3443ffa649..b65230f51039 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -877,20 +877,19 @@ def test_logging_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.LoggingServiceV2GrpcAsyncIOTransport, "create_channel", @@ -903,9 +902,8 @@ def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 590d022a4056..5df6538826bd 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -875,20 +875,19 @@ def test_metrics_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.MetricsServiceV2GrpcAsyncIOTransport, "create_channel", @@ -901,9 +900,8 @@ def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index f9045ac7e06b..3d0b8dc934e2 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -339,21 +339,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index 2dcd23f856aa..73548cf6736e 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -908,20 +908,19 @@ def test_cloud_redis_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.CloudRedisGrpcAsyncIOTransport, "create_channel", @@ -934,9 +933,8 @@ def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index db914f4b2471..a9c6999c92ec 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -339,21 +339,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index 7237130575ff..000b37cd1ca4 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -908,20 +908,19 @@ def test_cloud_redis_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.CloudRedisGrpcAsyncIOTransport, "create_channel", @@ -934,9 +933,8 @@ def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py index c6d94f56b067..89b24a5834b7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py @@ -328,21 +328,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py index 299e009966a7..8b20752e4f01 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py @@ -320,21 +320,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py index dc90eb327bd9..7f43c36442bf 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py @@ -325,21 +325,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index d43839f84e68..cc8f770a2fde 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -333,21 +333,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py index 12f399d36d9b..00ddd26563b0 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py @@ -323,21 +323,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py index b966c83fa9ff..8fdbbb9075ad 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py @@ -326,21 +326,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py index ab41a74e8afa..4ee73ecb2765 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py @@ -1197,20 +1197,19 @@ def test_echo_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_echo_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.EchoGrpcAsyncIOTransport, "create_channel", @@ -1223,9 +1222,8 @@ def test_echo_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py index 1e96a08dcbdc..7981c131bd63 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py @@ -898,20 +898,19 @@ def test_identity_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_identity_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.IdentityGrpcAsyncIOTransport, "create_channel", @@ -924,9 +923,8 @@ def test_identity_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py index 4d366542f410..2e3eff92eb11 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py @@ -902,20 +902,19 @@ def test_messaging_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_messaging_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.MessagingGrpcAsyncIOTransport, "create_channel", @@ -928,9 +927,8 @@ def test_messaging_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index cdf9f108e7bb..a125ac846820 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -903,20 +903,19 @@ def test_resumable_upload_service_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_resumable_upload_service_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.ResumableUploadServiceGrpcAsyncIOTransport, "create_channel", @@ -929,9 +928,8 @@ def test_resumable_upload_service_grpc_asyncio_transport_otel_channel_intercepto client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py index 78383ff9ec55..fa3bb02ec40e 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py @@ -899,20 +899,19 @@ def test_sequence_service_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_sequence_service_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.SequenceServiceGrpcAsyncIOTransport, "create_channel", @@ -925,9 +924,8 @@ def test_sequence_service_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py index 336faed128c4..00851721c522 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py @@ -896,20 +896,19 @@ def test_testing_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_testing_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.TestingGrpcAsyncIOTransport, "create_channel", @@ -922,9 +921,8 @@ def test_testing_grpc_asyncio_transport_otel_channel_interceptor(): client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py index 875d1d1dfb3d..5f9d70d3382c 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py @@ -325,21 +325,27 @@ def __init__(self, *, ], ) + # 1. User & Logging Interceptors (appended to channel) channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) + self._grpc_channel = apply_async_channel_interceptors( + self._grpc_channel, channel_interceptors + ) + # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) + # Must be placed at the front of the channel's pipeline so that our SDK spans + # wrap outside and downstream generic auto-instrumentation is suppressed. if ( _observability is not None - and (otel_interceptors := _observability.get_otel_async_interceptor(self._client_options)) is not None + and hasattr(_observability, "apply_otel_async_channel_interceptors") ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # Lifecycle: Can be lifted once lowest constraints require google-api-core >= 2.36.0. - otel_list = otel_interceptors if isinstance(otel_interceptors, (list, tuple)) else [otel_interceptors] # pragma: NO COVER - channel_interceptors.extend(otel_list) # pragma: NO COVER - - self._grpc_channel = apply_async_channel_interceptors(self._grpc_channel, channel_interceptors) + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + self._grpc_channel = _observability.apply_otel_async_channel_interceptors( + self._grpc_channel, self._client_options + ) # pragma: NO COVER self._logged_channel = self._grpc_channel # Wrap messages. This must be done after self._logged_channel exists diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index ac4ac6794b9f..ac8e81ab0d64 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -900,20 +900,19 @@ def test_storage_batch_operations_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( - _observability is None, - reason="Requires google-api-core with OpenTelemetry support (>= 2.36.0)", + _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), + reason="Requires google-api-core with OpenTelemetry async channel interceptors support", ) def test_storage_batch_operations_grpc_asyncio_transport_otel_channel_interceptor(): - mock_otel_interceptor = mock.Mock() mock_channel = mock.Mock() mock_channel._unary_unary_interceptors = [] with ( mock.patch.object( _observability, - "get_otel_async_interceptor", - return_value=mock_otel_interceptor, - ) as mock_get_async_interceptor, + "apply_otel_async_channel_interceptors", + return_value=mock_channel, + ) as mock_apply_async_interceptors, mock.patch.object( transports.StorageBatchOperationsGrpcAsyncIOTransport, "create_channel", @@ -926,9 +925,8 @@ def test_storage_batch_operations_grpc_asyncio_transport_otel_channel_intercepto client_options=options, ) - mock_get_async_interceptor.assert_called_once_with(options) + mock_apply_async_interceptors.assert_called_once_with(mock_channel, options) assert mock_create_channel.call_count == 1 - assert mock_otel_interceptor in transport.grpc_channel._unary_unary_interceptors assert transport.grpc_channel == mock_channel diff --git a/packages/gapic-generator/tests/system/test_tracing.py b/packages/gapic-generator/tests/system/test_tracing.py index 7f2ca695a60b..97eb8856a657 100644 --- a/packages/gapic-generator/tests/system/test_tracing.py +++ b/packages/gapic-generator/tests/system/test_tracing.py @@ -19,7 +19,6 @@ import pytest try: - from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( @@ -50,7 +49,16 @@ from google.api_core._feature_gating_helpers import FeatureGatingError from google.api_core.client_options import ClientOptions from google.auth import credentials as ga_credentials -from google.showcase import EchoClient +from google.showcase import EchoAsyncClient, EchoClient + +try: + from google.api_core import _observability + + HAS_AUTO_INSTRUMENTATION_SUPPRESSION = _observability is not None and hasattr( + _observability, "_AsyncSuppressingClientInterceptor" + ) +except ImportError: + HAS_AUTO_INSTRUMENTATION_SUPPRESSION = False try: from .conftest import construct_client @@ -233,3 +241,126 @@ def test_env_var_opt_in(otel_echo_client): assert len(spans) == 2 for span in spans: assert span.name == "google.showcase.v1beta1.Echo/Echo" + + +@pytest.mark.skipif( + not HAS_AUTO_INSTRUMENTATION_SUPPRESSION, + reason="Installed google-api-core lacks auto-instrumentation suppression", +) +def test_auto_instrumentation_suppression_sync(span_exporter): + """Verifies that upstream gRPC client auto-instrumentation spans are suppressed in sync calls. + + When users enable global GrpcInstrumentorClient().instrument(), upstream injects a + generic interceptor into channel creation. This test verifies that our downstream + suppression interceptor prevents the duplicate, bare-bones upstream span from being + emitted while preserving the full Google Cloud SDK T4 span. + """ + grpc_instrumentation = pytest.importorskip("opentelemetry.instrumentation.grpc") + + exporter, provider = span_exporter + instrumentor = grpc_instrumentation.GrpcInstrumentorClient() + instrumentor.instrument(tracer_provider=provider) + try: + options = ClientOptions( + api_endpoint="localhost:7469", + tracer_provider=provider, + ) + with mock.patch.dict( + os.environ, {"GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED": "true"} + ): + channel = grpc.insecure_channel("localhost:7469") + transport = EchoClient.get_transport_class("grpc")( + channel=channel, + client_options=options, + credentials=ga_credentials.AnonymousCredentials(), + ) + client = EchoClient(transport=transport, client_options=options) + response = client.echo( + showcase.EchoRequest(content="suppress sync duplicate") + ) + assert response.content == "suppress sync duplicate" + + spans = exporter.get_finished_spans() + assert len(spans) == 2 + + root_spans = [s for s in spans if s.parent is None] + child_spans = [s for s in spans if s.parent is not None] + assert len(root_spans) == 1 + assert len(child_spans) == 1 + + root_span = root_spans[0] + child_span = child_spans[0] + + assert root_span.name == "google.showcase.v1beta1.Echo/Echo" + assert child_span.name == "google.showcase.v1beta1.Echo/Echo" + assert child_span.parent.span_id == root_span.context.span_id + + assert child_span.attributes.get("rpc.system.name") == "grpc" + assert child_span.attributes.get("server.address") == "localhost" + assert child_span.attributes.get("server.port") == 7469 + assert child_span.attributes.get("url.domain") == "googleapis.com" + assert child_span.attributes.get("rpc.response.status_code") == "OK" + finally: + instrumentor.uninstrument() + + +@pytest.mark.asyncio +@pytest.mark.skipif( + not HAS_AUTO_INSTRUMENTATION_SUPPRESSION, + reason="Installed google-api-core lacks auto-instrumentation suppression", +) +async def test_auto_instrumentation_suppression_async(span_exporter): + """Verifies that upstream gRPC client auto-instrumentation spans are suppressed in async calls. + + When users enable global GrpcAioInstrumentorClient().instrument(), upstream injects a + generic interceptor into aio channel creation. This test verifies that our downstream + suppression interceptor prevents the duplicate, bare-bones upstream span from being + emitted while preserving the full Google Cloud SDK T4 span. + """ + grpc_instrumentation = pytest.importorskip("opentelemetry.instrumentation.grpc") + + exporter, provider = span_exporter + instrumentor = grpc_instrumentation.GrpcAioInstrumentorClient() + instrumentor.instrument(tracer_provider=provider) + try: + options = ClientOptions( + api_endpoint="localhost:7469", + tracer_provider=provider, + ) + with mock.patch.dict( + os.environ, {"GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED": "true"} + ): + channel = grpc.aio.insecure_channel("localhost:7469") + transport = EchoAsyncClient.get_transport_class("grpc_asyncio")( + channel=channel, + client_options=options, + credentials=ga_credentials.AnonymousCredentials(), + ) + client = EchoAsyncClient(transport=transport, client_options=options) + response = await client.echo( + showcase.EchoRequest(content="suppress async duplicate") + ) + assert response.content == "suppress async duplicate" + + spans = exporter.get_finished_spans() + assert len(spans) == 2 + + root_spans = [s for s in spans if s.parent is None] + child_spans = [s for s in spans if s.parent is not None] + assert len(root_spans) == 1 + assert len(child_spans) == 1 + + root_span = root_spans[0] + child_span = child_spans[0] + + assert root_span.name == "google.showcase.v1beta1.Echo/Echo" + assert child_span.name == "google.showcase.v1beta1.Echo/Echo" + assert child_span.parent.span_id == root_span.context.span_id + + assert child_span.attributes.get("rpc.system.name") == "grpc" + assert child_span.attributes.get("server.address") == "localhost" + assert child_span.attributes.get("server.port") == 7469 + assert child_span.attributes.get("url.domain") == "googleapis.com" + assert child_span.attributes.get("rpc.response.status_code") == "OK" + finally: + instrumentor.uninstrument() diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index cddc579b99bf..fe9b5d651312 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -18,12 +18,20 @@ from __future__ import annotations +import contextlib import urllib.parse from typing import TYPE_CHECKING, Any, Callable, Sequence from google.api_core import _feature_gating_helpers from google.api_core.client_options import ClientOptions +try: + from opentelemetry.instrumentation.utils import ( + suppress_instrumentation as _suppress_instrumentation, + ) +except ImportError: + _suppress_instrumentation = contextlib.nullcontext + if TYPE_CHECKING: # flake8: grpc, trace, and ClientInterceptor are imported only for static analysis and type annotations # The 'noqa: F401' comment avoids flake8 "imported but not used" errors. @@ -223,6 +231,146 @@ def _get_tracer_provider( return None +try: + # flake8: `grpc` imported conditionally for runtime interceptor base classes + import grpc # noqa: F811 + from grpc import ( + StreamStreamClientInterceptor as _SyncStreamStreamClientInterceptor, + ) + from grpc import ( + StreamUnaryClientInterceptor as _SyncStreamUnaryClientInterceptor, + ) + from grpc import ( + UnaryStreamClientInterceptor as _SyncUnaryStreamClientInterceptor, + ) + from grpc import ( + UnaryUnaryClientInterceptor as _SyncUnaryUnaryClientInterceptor, + ) +except ImportError: # pragma: NO COVER + # mypy: Fallback sentinel when optional grpc is not installed in the environment. + grpc = None # type: ignore[assignment] + + # mypy: Fallback dummy classes when optional grpc is not installed. + # Four distinct empty classes avoid duplicate base class 'object' TypeError at runtime. + class _SyncUnaryUnaryClientInterceptor: # type: ignore[no-redef] + __slots__ = () + + class _SyncUnaryStreamClientInterceptor: # type: ignore[no-redef] + __slots__ = () + + class _SyncStreamUnaryClientInterceptor: # type: ignore[no-redef] + __slots__ = () + + class _SyncStreamStreamClientInterceptor: # type: ignore[no-redef] + __slots__ = () + + +try: + from grpc.aio import ( + StreamStreamClientInterceptor as _AsyncStreamStreamClientInterceptor, + ) + from grpc.aio import ( + StreamUnaryClientInterceptor as _AsyncStreamUnaryClientInterceptor, + ) + from grpc.aio import ( + UnaryStreamClientInterceptor as _AsyncUnaryStreamClientInterceptor, + ) + from grpc.aio import ( + UnaryUnaryClientInterceptor as _AsyncUnaryUnaryClientInterceptor, + ) +except ImportError: # pragma: NO COVER + # mypy: Fallback dummy classes when optional grpc is not installed. + # Four distinct empty classes avoid duplicate base class 'object' TypeError at runtime. + class _AsyncUnaryUnaryClientInterceptor: # type: ignore[no-redef] + __slots__ = () + + class _AsyncUnaryStreamClientInterceptor: # type: ignore[no-redef] + __slots__ = () + + class _AsyncStreamUnaryClientInterceptor: # type: ignore[no-redef] + __slots__ = () + + class _AsyncStreamStreamClientInterceptor: # type: ignore[no-redef] + __slots__ = () + + +class _SuppressingClientInterceptor( + _SyncUnaryUnaryClientInterceptor, + _SyncUnaryStreamClientInterceptor, + _SyncStreamUnaryClientInterceptor, + _SyncStreamStreamClientInterceptor, +): + """Client interceptor that suppresses redundant downstream generic auto-instrumentation spans. + + When users enable global OpenTelemetry gRPC auto-instrumentation (e.g. GrpcInstrumentorClient), + the underlying channel is wrapped with generic interceptors that emit bare-bones spans. + This interceptor wraps downstream calls in OpenTelemetry's native `suppress_instrumentation` + context manager so that our feature-rich Google Cloud SDK T4 span is emitted while + redundant generic downstream spans are bypassed. + """ + + __slots__ = () + + def intercept_unary_unary( + self, continuation: Any, client_call_details: Any, request: Any + ) -> Any: + with _suppress_instrumentation(): + return continuation(client_call_details, request) + + def intercept_unary_stream( + self, continuation: Any, client_call_details: Any, request: Any + ) -> Any: + with _suppress_instrumentation(): + return continuation(client_call_details, request) + + def intercept_stream_unary( + self, continuation: Any, client_call_details: Any, request_iterator: Any + ) -> Any: + with _suppress_instrumentation(): + return continuation(client_call_details, request_iterator) + + def intercept_stream_stream( + self, continuation: Any, client_call_details: Any, request_iterator: Any + ) -> Any: + with _suppress_instrumentation(): + return continuation(client_call_details, request_iterator) + + +class _AsyncSuppressingClientInterceptor( + _AsyncUnaryUnaryClientInterceptor, + _AsyncUnaryStreamClientInterceptor, + _AsyncStreamUnaryClientInterceptor, + _AsyncStreamStreamClientInterceptor, +): + """AsyncIO client interceptor that suppresses redundant downstream generic auto-instrumentation spans.""" + + __slots__ = () + + async def intercept_unary_unary( + self, continuation: Any, client_call_details: Any, request: Any + ) -> Any: + with _suppress_instrumentation(): + return await continuation(client_call_details, request) + + async def intercept_unary_stream( + self, continuation: Any, client_call_details: Any, request: Any + ) -> Any: + with _suppress_instrumentation(): + return await continuation(client_call_details, request) + + async def intercept_stream_unary( + self, continuation: Any, client_call_details: Any, request_iterator: Any + ) -> Any: + with _suppress_instrumentation(): + return await continuation(client_call_details, request_iterator) + + async def intercept_stream_stream( + self, continuation: Any, client_call_details: Any, request_iterator: Any + ) -> Any: + with _suppress_instrumentation(): + return await continuation(client_call_details, request_iterator) + + def get_otel_interceptor( client_options: ClientOptions | dict[str, Any] | None = None, ) -> Callable[[grpc.Channel], grpc.Channel] | None: @@ -249,8 +397,15 @@ def get_otel_interceptor( request_hook=request_hook, response_hook=_grpc_client_response_hook, ) + suppressor = _SuppressingClientInterceptor() def otel_interceptor(channel: grpc.Channel) -> grpc.Channel: + if grpc is not None: + try: + channel = grpc.intercept_channel(channel, suppressor) + except (AttributeError, TypeError): + # Fallback if grpc lacks intercept_channel or custom channel type rejects it. + pass return otel_grpc.intercept_channel(channel, interceptor) otel_interceptor._is_otel_interceptor = True # type: ignore[attr-defined] @@ -279,11 +434,87 @@ def get_otel_async_interceptor( endpoint_attrs = _extract_endpoint_attributes(client_options) request_hook = _make_grpc_client_request_hook(endpoint_attrs) - return otel_grpc.aio_client_interceptors( + raw_interceptors = otel_grpc.aio_client_interceptors( tracer_provider=_get_tracer_provider(client_options), request_hook=request_hook, response_hook=_grpc_client_response_hook, ) + interceptors = ( + list(raw_interceptors) + if isinstance(raw_interceptors, (list, tuple)) + else [raw_interceptors] + ) + interceptors.append(_AsyncSuppressingClientInterceptor()) + + return interceptors + + +_CHANNEL_INTERCEPTOR_MAPPING = ( + ("intercept_unary_unary", "_unary_unary_interceptors"), + ("intercept_unary_stream", "_unary_stream_interceptors"), + ("intercept_stream_unary", "_stream_unary_interceptors"), + ("intercept_stream_stream", "_stream_stream_interceptors"), +) + + +def apply_otel_async_channel_interceptors( + channel: Any, + client_options: ClientOptions | dict[str, Any] | Sequence[Any] | None = None, +) -> Any: + """Creates and attaches OpenTelemetry client interceptors to the front of an async gRPC channel. + + In grpc.aio, channels maintain internal interceptor lists evaluated in index order. + To ensure Google Cloud SDK telemetry wraps outside downstream interceptors and + suppresses ambient generic auto-instrumentation, our interceptors are prepended + to the front of the channel's interceptor lists. + + Args: + channel: The async gRPC channel to intercept. + client_options (Optional[Union[ClientOptions, dict[str, Any], Sequence[Any]]]): The client + options used to resolve feature flags and tracer provider, or a sequence of + pre-resolved interceptors. + + Returns: + The channel with interceptors prepended to its internal lists, + or the original channel if tracing is disabled or not installed. + """ + otel_interceptors: Sequence[Any] | None + if isinstance(client_options, (ClientOptions, dict)) or client_options is None: + otel_interceptors = get_otel_async_interceptor(client_options) + elif isinstance(client_options, Sequence): + otel_interceptors = client_options + else: + otel_interceptors = None + + if not otel_interceptors: + return channel + + interceptor_list = list(otel_interceptors) + + for method_name, attr_name in _CHANNEL_INTERCEPTOR_MAPPING: + target_list = getattr(channel, attr_name, None) + if isinstance(target_list, list): + matching = [ + i + for i in interceptor_list + if hasattr(i, method_name) and i not in target_list + ] + if matching: + target_list[:0] = matching + + # Path 2: Fallback for generic/untyped interceptors. + unary_list = getattr(channel, "_unary_unary_interceptors", None) + if isinstance(unary_list, list): + standard_methods = tuple(method for method, _ in _CHANNEL_INTERCEPTOR_MAPPING) + fallback_items = [ + i + for i in interceptor_list + if not any(hasattr(i, m) for m in standard_methods) and i not in unary_list + ] + if fallback_items: + unary_list[:0] = fallback_items + + return channel _TRACE_CONTEXT_PROPAGATOR: Any = None diff --git a/packages/google-api-core/tests/asyncio/test_observability_async.py b/packages/google-api-core/tests/asyncio/test_observability_async.py new file mode 100644 index 000000000000..391382ba957c --- /dev/null +++ b/packages/google-api-core/tests/asyncio/test_observability_async.py @@ -0,0 +1,179 @@ +# 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. + +from unittest import mock + +import pytest + +from google.api_core import _observability + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "method_name", + [ + "intercept_unary_unary", + "intercept_unary_stream", + "intercept_stream_unary", + "intercept_stream_stream", + ], + ids=["unary_unary", "unary_stream", "stream_unary", "stream_stream"], +) +async def test_async_suppressing_interceptor_invocations(method_name): + """Verifies that _AsyncSuppressingClientInterceptor wraps downstream async calls in suppress_instrumentation. + + When global OpenTelemetry gRPC auto-instrumentation is enabled, it emits + generic, un-enriched attempt spans. We wrap downstream async calls in + suppress_instrumentation so only our enriched Google Cloud SDK T4 span is emitted. + """ + suppressor = _observability._AsyncSuppressingClientInterceptor() + + with mock.patch( + "google.api_core._observability._suppress_instrumentation" + ) as mock_suppress: + mock_continuation = mock.AsyncMock(return_value="expected_response") + interceptor_method = getattr(suppressor, method_name) + result = await interceptor_method( + mock_continuation, "call_details", "request_or_iterator" + ) + + assert result == "expected_response" + mock_continuation.assert_called_once_with("call_details", "request_or_iterator") + mock_suppress.assert_called_once() + + +def test_apply_otel_async_channel_interceptors_none_or_empty(): + """Proves that passing None or empty interceptors returns the channel unchanged.""" + channel = mock.Mock() + assert ( + _observability.apply_otel_async_channel_interceptors(channel, None) is channel + ) + assert _observability.apply_otel_async_channel_interceptors(channel, []) is channel + + +def test_apply_otel_async_channel_interceptors_prepends_in_order(): + """Proves that OpenTelemetry interceptors are prepended to the front of channel interceptor lists in order.""" + existing_interceptor = mock.Mock() + interceptor1 = mock.Mock(spec=["intercept_unary_unary"]) + interceptor2 = mock.Mock(spec=["intercept_unary_unary"]) + + channel = mock.Mock() + channel._unary_unary_interceptors = [existing_interceptor] + + result = _observability.apply_otel_async_channel_interceptors( + channel, [interceptor1, interceptor2] + ) + assert result is channel + assert channel._unary_unary_interceptors == [ + interceptor1, + interceptor2, + existing_interceptor, + ] + + +def test_apply_otel_async_channel_interceptors_prevents_duplicates(): + """Proves that already-registered interceptors are not prepended again.""" + interceptor = mock.Mock(spec=["intercept_unary_unary"]) + + channel = mock.Mock() + channel._unary_unary_interceptors = [interceptor] + + result = _observability.apply_otel_async_channel_interceptors( + channel, [interceptor] + ) + assert result is channel + assert channel._unary_unary_interceptors == [interceptor] + + +def test_apply_otel_async_channel_interceptors_non_list_ignored(): + """Proves that non-list interceptor containers on a channel are safely ignored.""" + channel = mock.Mock() + channel._unary_unary_interceptors = "not-a-list" + interceptor = mock.Mock(spec=["intercept_unary_unary"]) + + result = _observability.apply_otel_async_channel_interceptors( + channel, [interceptor] + ) + assert result is channel + assert channel._unary_unary_interceptors == "not-a-list" + + +def test_apply_otel_async_channel_interceptors_with_client_options_disabled( + monkeypatch, +): + """Proves that apply_otel_async_channel_interceptors returns channel unchanged when tracing is disabled.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") + channel = mock.Mock() + channel._unary_unary_interceptors = [] + options = _observability.ClientOptions() + + result = _observability.apply_otel_async_channel_interceptors(channel, options) + assert result is channel + assert channel._unary_unary_interceptors == [] + + +def test_apply_otel_async_channel_interceptors_with_client_options_enabled(monkeypatch): + """Proves that apply_otel_async_channel_interceptors resolves interceptors and prepends them.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + channel = mock.Mock() + existing = mock.Mock() + channel._unary_unary_interceptors = [existing] + options = _observability.ClientOptions() + mock_interceptor = mock.Mock(spec=["intercept_unary_unary"]) + + with mock.patch.object( + _observability, "get_otel_async_interceptor", return_value=[mock_interceptor] + ) as mock_get: + result = _observability.apply_otel_async_channel_interceptors(channel, options) + assert result is channel + mock_get.assert_called_once_with(options) + assert channel._unary_unary_interceptors == [mock_interceptor, existing] + + +def test_apply_otel_async_channel_interceptors_sequence_tuple(): + """Proves that passing a sequence tuple of interceptors prepends correctly.""" + channel = mock.Mock() + existing = mock.Mock() + channel._unary_unary_interceptors = [existing] + interceptor = mock.Mock(spec=["intercept_unary_unary"]) + + result = _observability.apply_otel_async_channel_interceptors( + channel, (interceptor,) + ) + assert result is channel + assert channel._unary_unary_interceptors == [interceptor, existing] + + +def test_apply_otel_async_channel_interceptors_invalid_client_options(): + """Proves that passing an invalid client_options type returns the channel unchanged.""" + channel = mock.Mock() + channel._unary_unary_interceptors = [] + + result = _observability.apply_otel_async_channel_interceptors(channel, 12345) + assert result is channel + assert channel._unary_unary_interceptors == [] + + +def test_apply_otel_async_channel_interceptors_generic_untyped_fallback(): + """Proves that generic interceptors without standard methods fall back to unary list.""" + channel = mock.Mock() + existing = mock.Mock() + channel._unary_unary_interceptors = [existing] + generic_interceptor = mock.Mock(spec=[]) + + result = _observability.apply_otel_async_channel_interceptors( + channel, [generic_interceptor] + ) + assert result is channel + assert channel._unary_unary_interceptors == [generic_interceptor, existing] diff --git a/packages/google-api-core/tests/conftest.py b/packages/google-api-core/tests/conftest.py index 664ef7f95895..1fa774ded176 100644 --- a/packages/google-api-core/tests/conftest.py +++ b/packages/google-api-core/tests/conftest.py @@ -64,3 +64,22 @@ def mock_otel(monkeypatch): tracer=mock_tracer, span=mock_span, ) + + +@pytest.fixture +def mock_otel_grpc(monkeypatch): + """Provides a mocked opentelemetry.instrumentation.grpc module environment. + + Unlike `mock_otel` (which mocks `opentelemetry.trace` to test span generation + in client methods), this fixture simulates the installation of the gRPC + auto-instrumentation package (`opentelemetry.instrumentation.grpc`) in `sys.modules`. + This allows testing gRPC channel interceptor resolution and capability checks. + """ + mock_otel = mock.Mock() + mock_grpc = mock_otel.instrumentation.grpc + monkeypatch.setitem(sys.modules, "opentelemetry", mock_otel) + monkeypatch.setitem( + sys.modules, "opentelemetry.instrumentation", mock_otel.instrumentation + ) + monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", mock_grpc) + return mock_grpc diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index 90ac4acb3ba3..951cab49f9a6 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -40,23 +40,11 @@ def test_is_otel_capabilities_enabled_otel_missing(monkeypatch): assert not _observability.is_otel_capabilities_enabled() -def test_is_otel_capabilities_enabled_otel_installed(monkeypatch): +def test_is_otel_capabilities_enabled_otel_installed(monkeypatch, mock_otel_grpc): """Proves that is_otel_capabilities_enabled returns True when tracing is enabled and OpenTelemetry gRPC instrumentation is installed. """ monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") - - mock_otel = mock.Mock() - mock_otel_grpc = mock_otel.instrumentation.grpc - - 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 - ) - assert _observability.is_otel_capabilities_enabled() @@ -74,23 +62,13 @@ def test_is_otel_capabilities_enabled_experimental_requires_env_var(monkeypatch) _observability.is_otel_capabilities_enabled(options) -def test_is_otel_capabilities_enabled_experimental_enabled_with_config(monkeypatch): +def test_is_otel_capabilities_enabled_experimental_enabled_with_config( + monkeypatch, mock_otel_grpc +): """Proves that when GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED=true and tracer_provider is supplied via client_options, is_otel_capabilities_enabled returns True. """ monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") - - mock_otel = mock.Mock() - mock_otel_grpc = mock_otel.instrumentation.grpc - - 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 - ) - options = ClientOptions(tracer_provider=mock.Mock()) assert _observability.is_otel_capabilities_enabled(options) @@ -135,22 +113,38 @@ def test_get_tracer_provider_dict_config(): assert _observability._get_tracer_provider(options) is mock_tracer_provider -def test_get_otel_interceptor_disabled(monkeypatch): - """Proves that get_otel_interceptor returns None when tracing is disabled.""" +@pytest.mark.parametrize( + "interceptor_getter", + [ + _observability.get_otel_interceptor, + _observability.get_otel_async_interceptor, + ], + ids=["sync_interceptor", "async_interceptor"], +) +def test_get_otel_interceptor_disabled(monkeypatch, interceptor_getter): + """Proves that interceptor getters return None when tracing is disabled.""" monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") - assert _observability.get_otel_interceptor() is None + assert interceptor_getter() is None -def test_get_otel_interceptor_otel_missing(monkeypatch): - """Proves that get_otel_interceptor returns None when OpenTelemetry gRPC +@pytest.mark.parametrize( + "interceptor_getter", + [ + _observability.get_otel_interceptor, + _observability.get_otel_async_interceptor, + ], + ids=["sync_interceptor", "async_interceptor"], +) +def test_get_otel_interceptor_otel_missing(monkeypatch, interceptor_getter): + """Proves that interceptor getters return None when OpenTelemetry gRPC instrumentation is not installed. """ monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", None) - assert _observability.get_otel_interceptor() is None + assert interceptor_getter() is None -def test_get_otel_interceptor_enabled(monkeypatch): +def test_get_otel_interceptor_enabled(monkeypatch, mock_otel_grpc): """Proves that get_otel_interceptor creates a synchronous OpenTelemetry client interceptor with the resolved tracer provider and returns a channel-intercepting callable. """ @@ -160,22 +154,11 @@ def test_get_otel_interceptor_enabled(monkeypatch): mock_raw_channel = mock.Mock(name="raw_channel") mock_wrapped_channel = mock.Mock(name="wrapped_channel") - - mock_otel = mock.Mock() - mock_otel_grpc = mock_otel.instrumentation.grpc 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 - 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 - ) - interceptor = _observability.get_otel_interceptor(client_options=options) assert callable(interceptor) @@ -192,12 +175,14 @@ def test_get_otel_interceptor_enabled(monkeypatch): result = interceptor(mock_raw_channel) assert result is mock_wrapped_channel - mock_otel_grpc.intercept_channel.assert_called_once_with( - mock_raw_channel, mock_interceptor - ) + assert mock_otel_grpc.intercept_channel.call_count == 1 + chan_arg, interceptor_arg = mock_otel_grpc.intercept_channel.call_args[0] + assert interceptor_arg is mock_interceptor -def test_get_otel_interceptor_with_apply_channel_interceptors(monkeypatch): +def test_get_otel_interceptor_with_apply_channel_interceptors( + monkeypatch, mock_otel_grpc +): """Proves that get_otel_interceptor integrates seamlessly into apply_channel_interceptors.""" pytest.importorskip("grpc") from google.api_core import grpc_helpers @@ -208,22 +193,11 @@ def test_get_otel_interceptor_with_apply_channel_interceptors(monkeypatch): mock_raw_channel = mock.Mock(name="raw_channel") mock_wrapped_channel = mock.Mock(name="wrapped_channel") - - mock_otel = mock.Mock() - mock_otel_grpc = mock_otel.instrumentation.grpc 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 - 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 - ) - otel_interceptor = _observability.get_otel_interceptor(client_options=options) assert callable(otel_interceptor) @@ -231,27 +205,35 @@ def test_get_otel_interceptor_with_apply_channel_interceptors(monkeypatch): mock_raw_channel, interceptors=[otel_interceptor] ) assert result is mock_wrapped_channel - mock_otel_grpc.intercept_channel.assert_called_once_with( - mock_raw_channel, mock_interceptor - ) - - -def test_get_otel_async_interceptor_disabled(monkeypatch): - """Proves that get_otel_async_interceptor returns None when tracing is disabled.""" - monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") - assert _observability.get_otel_async_interceptor() is None + assert mock_otel_grpc.intercept_channel.call_count == 1 + chan_arg, interceptor_arg = mock_otel_grpc.intercept_channel.call_args[0] + assert interceptor_arg is mock_interceptor -def test_get_otel_async_interceptor_otel_missing(monkeypatch): - """Proves that get_otel_async_interceptor returns None when OpenTelemetry gRPC - instrumentation is not installed. - """ +@pytest.mark.skipif(_observability.grpc is None, reason="grpc is not installed") +@pytest.mark.parametrize("error_cls", [AttributeError, TypeError]) +def test_get_otel_interceptor_grpc_intercept_channel_fallback( + monkeypatch, mock_otel_grpc, error_cls +): + """Proves that otel_interceptor falls back gracefully if grpc.intercept_channel raises AttributeError or TypeError.""" 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 + options = ClientOptions() + mock_raw_channel = mock.Mock(name="raw_channel") + mock_wrapped_channel = mock.Mock(name="wrapped_channel") + mock_otel_grpc.client_interceptor.return_value = mock.Mock() + mock_otel_grpc.intercept_channel.return_value = mock_wrapped_channel + otel_interceptor = _observability.get_otel_interceptor(client_options=options) + with mock.patch.object( + _observability.grpc, + "intercept_channel", + side_effect=error_cls("Mock intercept error"), + ): + result = otel_interceptor(mock_raw_channel) + assert result is mock_wrapped_channel -def test_get_otel_async_interceptor_enabled(monkeypatch): + +def test_get_otel_async_interceptor_enabled(monkeypatch, mock_otel_grpc): """Proves that get_otel_async_interceptor instantiates and returns asynchronous OpenTelemetry client interceptors with the resolved tracer provider. """ @@ -260,21 +242,13 @@ def test_get_otel_async_interceptor_enabled(monkeypatch): options = ClientOptions(tracer_provider=mock_tracer_provider) mock_async_interceptors = [mock.Mock(name="otel_async_interceptor")] - - mock_otel = mock.Mock() - mock_otel_grpc = mock_otel.instrumentation.grpc mock_otel_grpc.aio_client_interceptors.return_value = mock_async_interceptors - 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_async_interceptor(client_options=options) - assert result is mock_async_interceptors + assert mock_async_interceptors[0] in result + assert any( + isinstance(i, _observability._AsyncSuppressingClientInterceptor) for i in result + ) mock_otel_grpc.aio_client_interceptors.assert_called_once_with( tracer_provider=mock_tracer_provider, request_hook=mock.ANY, @@ -288,6 +262,32 @@ def test_get_otel_async_interceptor_enabled(monkeypatch): mock_span.set_attribute.assert_any_call("url.domain", "googleapis.com") +def test_get_otel_async_interceptor_single_return(monkeypatch, mock_otel_grpc): + """Verifies that a single interceptor returned by upstream OTel is safely wrapped into a list.""" + # Step 1: Opt-in to experimental SDK tracing via environment variable. + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + + # Step 2: Configure the mock OpenTelemetry gRPC instrumentation fixture. + # While upstream normally returns a sequence (list or tuple), third-party + # instrumentation or older versions may return a lone interceptor object. + # We simulate this edge case by returning a single mock interceptor. + single_interceptor = mock.Mock(name="single_otel_interceptor") + mock_otel_grpc.aio_client_interceptors.return_value = single_interceptor + + # Step 3: Call get_otel_async_interceptor with standard ClientOptions. + options = ClientOptions() + result = _observability.get_otel_async_interceptor(client_options=options) + + # Step 4: Validate list normalization and downstream suppression interceptor injection. + # The result must be a 2-item list: + # [0]: The single upstream interceptor wrapped in a list. + # [1]: Our internal _AsyncSuppressingClientInterceptor appended to prevent redundant wire spans. + assert isinstance(result, list) + assert len(result) == 2 + assert result[0] is single_interceptor + assert isinstance(result[1], _observability._AsyncSuppressingClientInterceptor) + + @pytest.mark.parametrize( "client_options,expected_attrs", [ @@ -1302,3 +1302,44 @@ def test_trace_http_request_initialization_fails_open(monkeypatch): assert isinstance(ctx, _observability._TraceContext) assert ctx._span is None ctx.record_response(mock.Mock()) + + +@pytest.mark.parametrize( + "method_name", + [ + "intercept_unary_unary", + "intercept_unary_stream", + "intercept_stream_unary", + "intercept_stream_stream", + ], + ids=["unary_unary", "unary_stream", "stream_unary", "stream_stream"], +) +def test_sync_suppressing_interceptor_invocations(method_name): + """Verifies that _SuppressingClientInterceptor wraps downstream calls in suppress_instrumentation. + + When global OpenTelemetry gRPC auto-instrumentation is enabled, it emits + generic, un-enriched attempt spans. We wrap downstream calls in + suppress_instrumentation so only our enriched Google Cloud SDK T4 span is emitted. + """ + suppressor = _observability._SuppressingClientInterceptor() + + with mock.patch( + "google.api_core._observability._suppress_instrumentation" + ) as mock_suppress: + mock_continuation = mock.Mock(return_value="expected_response") + interceptor_method = getattr(suppressor, method_name) + result = interceptor_method( + mock_continuation, "call_details", "request_or_iterator" + ) + + assert result == "expected_response" + mock_continuation.assert_called_once_with("call_details", "request_or_iterator") + mock_suppress.assert_called_once() + + +def test_suppressing_interceptors_define_slots(): + """Verifies that suppressing interceptor classes explicitly define __slots__ = ().""" + assert _observability._SuppressingClientInterceptor.__slots__ == () + assert _observability._AsyncSuppressingClientInterceptor.__slots__ == () + assert "__dict__" not in _observability._SuppressingClientInterceptor.__dict__ + assert "__dict__" not in _observability._AsyncSuppressingClientInterceptor.__dict__ From a22fab79b1182b477285b56bee2489ab44d223ea Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 15 Oct 2026 16:33:14 -0400 Subject: [PATCH 02/11] refactor(observability): simplify template comments and parametrize async tests - Update grpc_asyncio.py.j2 comments to concise descriptions and regenerate goldens - Document gRPC onion prepending rationale and label symmetric dispatch sections in _observability.py - Consolidate and parametrize async observability unit tests with explicit parameter IDs - Add mock_async_channel fixture to conftest.py and maintain 100% test coverage --- .../%service/transports/grpc_asyncio.py.j2 | 6 +- .../asset_service/transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../eventarc/transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../cloud_redis/transports/grpc_asyncio.py | 6 +- .../cloud_redis/transports/grpc_asyncio.py | 6 +- .../services/echo/transports/grpc_asyncio.py | 6 +- .../identity/transports/grpc_asyncio.py | 6 +- .../messaging/transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../testing/transports/grpc_asyncio.py | 6 +- .../transports/grpc_asyncio.py | 6 +- .../google/api_core/_observability.py | 9 +- .../tests/asyncio/test_observability_async.py | 193 ++++++++---------- packages/google-api-core/tests/conftest.py | 11 + .../tests/unit/test_observability.py | 15 ++ 23 files changed, 161 insertions(+), 181 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index fe19734252ec..b5baf14f0d9d 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -333,7 +333,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -341,9 +341,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py index 044360293d13..cf2717ef9096 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py @@ -319,7 +319,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -327,9 +327,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py index e582999d3ba4..7f5174e076e6 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py @@ -324,7 +324,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -332,9 +332,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py index a8824568ce4a..f12a3a5871c8 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py @@ -333,7 +333,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -341,9 +341,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index b4f164a93970..8dbec10b8531 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -319,7 +319,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -327,9 +327,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 873f2060b30f..cfb9f532c742 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -317,7 +317,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -325,9 +325,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index bf0936a88140..3c5ae0968108 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -317,7 +317,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -325,9 +325,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index b4f164a93970..8dbec10b8531 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -319,7 +319,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -327,9 +327,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index 873f2060b30f..cfb9f532c742 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -317,7 +317,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -325,9 +325,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index bf0936a88140..3c5ae0968108 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -317,7 +317,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -325,9 +325,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index 3d0b8dc934e2..8f5c3dec6b97 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -339,7 +339,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -347,9 +347,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index a9c6999c92ec..ec2d9a1a7457 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -339,7 +339,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -347,9 +347,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py index 89b24a5834b7..eda679937993 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py @@ -328,7 +328,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -336,9 +336,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py index 8b20752e4f01..ce63afdb575f 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py @@ -320,7 +320,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -328,9 +328,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py index 7f43c36442bf..ee63afa722c0 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py @@ -325,7 +325,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -333,9 +333,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index cc8f770a2fde..0f3ad488eb01 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -333,7 +333,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -341,9 +341,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py index 00ddd26563b0..7d208378e7a9 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py @@ -323,7 +323,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -331,9 +331,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py index 8fdbbb9075ad..7650aa1cf48a 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py @@ -326,7 +326,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -334,9 +334,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py index 5f9d70d3382c..685e85f8430d 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py @@ -325,7 +325,7 @@ def __init__(self, *, ], ) - # 1. User & Logging Interceptors (appended to channel) + # Wrap channel with user-specified and logging interceptors. channel_interceptors = list(interceptors) if interceptors else [] self._interceptor = _LoggingClientAIOInterceptor() channel_interceptors.append(self._interceptor) @@ -333,9 +333,7 @@ def __init__(self, *, self._grpc_channel, channel_interceptors ) - # 2. OpenTelemetry Tracing & Suppression Interceptors (prepended to channel) - # Must be placed at the front of the channel's pipeline so that our SDK spans - # wrap outside and downstream generic auto-instrumentation is suppressed. + # Configure OpenTelemetry tracing and span suppression on the channel. if ( _observability is not None and hasattr(_observability, "apply_otel_async_channel_interceptors") diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index fe9b5d651312..b7f4d7228c91 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -491,6 +491,12 @@ def apply_otel_async_channel_interceptors( interceptor_list = list(otel_interceptors) + # Section 1: Typed interceptors - prepend matching interceptors to each + # corresponding channel list (unary-unary, unary-stream, etc.) in order. + # We prepend (put at the very front) rather than append because gRPC interceptors + # run like layers of an onion from first to last: placing our tracing and + # suppressing interceptors at the very front ensures our Google span wraps + # around the whole call and blocks duplicate background spans before they start. for method_name, attr_name in _CHANNEL_INTERCEPTOR_MAPPING: target_list = getattr(channel, attr_name, None) if isinstance(target_list, list): @@ -502,7 +508,8 @@ def apply_otel_async_channel_interceptors( if matching: target_list[:0] = matching - # Path 2: Fallback for generic/untyped interceptors. + # Section 2: Untyped/generic fallback - prepend any remaining interceptors + # lacking standard method names directly to the unary-unary list. unary_list = getattr(channel, "_unary_unary_interceptors", None) if isinstance(unary_list, list): standard_methods = tuple(method for method, _ in _CHANNEL_INTERCEPTOR_MAPPING) diff --git a/packages/google-api-core/tests/asyncio/test_observability_async.py b/packages/google-api-core/tests/asyncio/test_observability_async.py index 391382ba957c..f3b4fa4b1285 100644 --- a/packages/google-api-core/tests/asyncio/test_observability_async.py +++ b/packages/google-api-core/tests/asyncio/test_observability_async.py @@ -53,127 +53,112 @@ async def test_async_suppressing_interceptor_invocations(method_name): mock_suppress.assert_called_once() -def test_apply_otel_async_channel_interceptors_none_or_empty(): - """Proves that passing None or empty interceptors returns the channel unchanged.""" - channel = mock.Mock() - assert ( - _observability.apply_otel_async_channel_interceptors(channel, None) is channel +@pytest.mark.parametrize( + "options_or_interceptors,tracing_enabled", + [ + (None, False), + ([], False), + (12345, False), + ({}, False), + (_observability.ClientOptions(), False), + ], + ids=[ + "none_options", + "empty_list", + "invalid_type", + "client_options_dict_tracing_disabled", + "client_options_tracing_disabled", + ], +) +def test_apply_otel_async_channel_interceptors_inactive_or_empty( + mock_async_channel, monkeypatch, options_or_interceptors, tracing_enabled +): + """Proves that passing inactive or empty options/interceptors returns channel unchanged.""" + if tracing_enabled: + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + else: + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") + + result = _observability.apply_otel_async_channel_interceptors( + mock_async_channel, options_or_interceptors ) - assert _observability.apply_otel_async_channel_interceptors(channel, []) is channel + assert result is mock_async_channel + assert mock_async_channel._unary_unary_interceptors == [] + assert mock_async_channel._unary_stream_interceptors == [] -def test_apply_otel_async_channel_interceptors_prepends_in_order(): - """Proves that OpenTelemetry interceptors are prepended to the front of channel interceptor lists in order.""" +@pytest.mark.parametrize( + "input_mode", + ["list", "tuple", "client_options"], + ids=["list_interceptors", "tuple_interceptors", "client_options_resolved"], +) +def test_apply_otel_async_channel_interceptors_prepending_forms( + mock_async_channel, monkeypatch, input_mode +): + """Proves that interceptors (via list, tuple, or ClientOptions) are prepended in order.""" existing_interceptor = mock.Mock() + mock_async_channel._unary_unary_interceptors = [existing_interceptor] + interceptor1 = mock.Mock(spec=["intercept_unary_unary"]) interceptor2 = mock.Mock(spec=["intercept_unary_unary"]) - channel = mock.Mock() - channel._unary_unary_interceptors = [existing_interceptor] + if input_mode == "list": + arg = [interceptor1, interceptor2] + elif input_mode == "tuple": + arg = (interceptor1, interceptor2) + else: # client_options + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + arg = _observability.ClientOptions() + monkeypatch.setattr( + _observability, + "get_otel_async_interceptor", + lambda opts: [interceptor1, interceptor2], + ) result = _observability.apply_otel_async_channel_interceptors( - channel, [interceptor1, interceptor2] + mock_async_channel, arg ) - assert result is channel - assert channel._unary_unary_interceptors == [ + assert result is mock_async_channel + assert mock_async_channel._unary_unary_interceptors == [ interceptor1, interceptor2, existing_interceptor, ] -def test_apply_otel_async_channel_interceptors_prevents_duplicates(): - """Proves that already-registered interceptors are not prepended again.""" - interceptor = mock.Mock(spec=["intercept_unary_unary"]) - - channel = mock.Mock() - channel._unary_unary_interceptors = [interceptor] - - result = _observability.apply_otel_async_channel_interceptors( - channel, [interceptor] - ) - assert result is channel - assert channel._unary_unary_interceptors == [interceptor] - - -def test_apply_otel_async_channel_interceptors_non_list_ignored(): - """Proves that non-list interceptor containers on a channel are safely ignored.""" - channel = mock.Mock() - channel._unary_unary_interceptors = "not-a-list" - interceptor = mock.Mock(spec=["intercept_unary_unary"]) - - result = _observability.apply_otel_async_channel_interceptors( - channel, [interceptor] - ) - assert result is channel - assert channel._unary_unary_interceptors == "not-a-list" - - -def test_apply_otel_async_channel_interceptors_with_client_options_disabled( - monkeypatch, +@pytest.mark.parametrize( + "case,setup_channel,interceptor_spec,expected_result", + [ + ( + "prevent_duplicates", + lambda ch, i: setattr(ch, "_unary_unary_interceptors", [i]), + ["intercept_unary_unary"], + lambda ch, i: ch._unary_unary_interceptors == [i], + ), + ( + "non_list_ignored", + lambda ch, i: setattr(ch, "_unary_unary_interceptors", "not-a-list"), + ["intercept_unary_unary"], + lambda ch, i: ch._unary_unary_interceptors == "not-a-list", + ), + ( + "generic_untyped_fallback", + lambda ch, i: setattr(ch, "_unary_unary_interceptors", ["existing"]), + [], + lambda ch, i: ch._unary_unary_interceptors == [i, "existing"], + ), + ], + ids=["prevent_duplicates", "non_list_ignored", "generic_untyped_fallback"], +) +def test_apply_otel_async_channel_interceptors_edge_cases( + mock_async_channel, case, setup_channel, interceptor_spec, expected_result ): - """Proves that apply_otel_async_channel_interceptors returns channel unchanged when tracing is disabled.""" - monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") - channel = mock.Mock() - channel._unary_unary_interceptors = [] - options = _observability.ClientOptions() - - result = _observability.apply_otel_async_channel_interceptors(channel, options) - assert result is channel - assert channel._unary_unary_interceptors == [] - - -def test_apply_otel_async_channel_interceptors_with_client_options_enabled(monkeypatch): - """Proves that apply_otel_async_channel_interceptors resolves interceptors and prepends them.""" - monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") - channel = mock.Mock() - existing = mock.Mock() - channel._unary_unary_interceptors = [existing] - options = _observability.ClientOptions() - mock_interceptor = mock.Mock(spec=["intercept_unary_unary"]) - - with mock.patch.object( - _observability, "get_otel_async_interceptor", return_value=[mock_interceptor] - ) as mock_get: - result = _observability.apply_otel_async_channel_interceptors(channel, options) - assert result is channel - mock_get.assert_called_once_with(options) - assert channel._unary_unary_interceptors == [mock_interceptor, existing] - - -def test_apply_otel_async_channel_interceptors_sequence_tuple(): - """Proves that passing a sequence tuple of interceptors prepends correctly.""" - channel = mock.Mock() - existing = mock.Mock() - channel._unary_unary_interceptors = [existing] - interceptor = mock.Mock(spec=["intercept_unary_unary"]) - - result = _observability.apply_otel_async_channel_interceptors( - channel, (interceptor,) - ) - assert result is channel - assert channel._unary_unary_interceptors == [interceptor, existing] - - -def test_apply_otel_async_channel_interceptors_invalid_client_options(): - """Proves that passing an invalid client_options type returns the channel unchanged.""" - channel = mock.Mock() - channel._unary_unary_interceptors = [] - - result = _observability.apply_otel_async_channel_interceptors(channel, 12345) - assert result is channel - assert channel._unary_unary_interceptors == [] - - -def test_apply_otel_async_channel_interceptors_generic_untyped_fallback(): - """Proves that generic interceptors without standard methods fall back to unary list.""" - channel = mock.Mock() - existing = mock.Mock() - channel._unary_unary_interceptors = [existing] - generic_interceptor = mock.Mock(spec=[]) + """Proves edge case handling: deduplication, non-list attributes, and untyped fallback.""" + interceptor = mock.Mock(spec=interceptor_spec) + setup_channel(mock_async_channel, interceptor) result = _observability.apply_otel_async_channel_interceptors( - channel, [generic_interceptor] + mock_async_channel, [interceptor] ) - assert result is channel - assert channel._unary_unary_interceptors == [generic_interceptor, existing] + assert result is mock_async_channel + assert expected_result(mock_async_channel, interceptor) diff --git a/packages/google-api-core/tests/conftest.py b/packages/google-api-core/tests/conftest.py index 1fa774ded176..84cd3283f047 100644 --- a/packages/google-api-core/tests/conftest.py +++ b/packages/google-api-core/tests/conftest.py @@ -83,3 +83,14 @@ def mock_otel_grpc(monkeypatch): ) monkeypatch.setitem(sys.modules, "opentelemetry.instrumentation.grpc", mock_grpc) return mock_grpc + + +@pytest.fixture +def mock_async_channel(): + """Provides a mock grpc.aio.Channel with standard interceptor list attributes.""" + channel = mock.Mock() + channel._unary_unary_interceptors = [] + channel._unary_stream_interceptors = [] + channel._stream_unary_interceptors = [] + channel._stream_stream_interceptors = [] + return channel diff --git a/packages/google-api-core/tests/unit/test_observability.py b/packages/google-api-core/tests/unit/test_observability.py index 951cab49f9a6..53425f42554e 100644 --- a/packages/google-api-core/tests/unit/test_observability.py +++ b/packages/google-api-core/tests/unit/test_observability.py @@ -233,6 +233,21 @@ def test_get_otel_interceptor_grpc_intercept_channel_fallback( assert result is mock_wrapped_channel +def test_get_otel_interceptor_grpc_none(monkeypatch, mock_otel_grpc): + """Proves that otel_interceptor proceeds cleanly when grpc is None.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + options = ClientOptions() + mock_raw_channel = mock.Mock(name="raw_channel") + mock_wrapped_channel = mock.Mock(name="wrapped_channel") + mock_otel_grpc.client_interceptor.return_value = mock.Mock() + mock_otel_grpc.intercept_channel.return_value = mock_wrapped_channel + + otel_interceptor = _observability.get_otel_interceptor(client_options=options) + with mock.patch.object(_observability, "grpc", None): + result = otel_interceptor(mock_raw_channel) + assert result is mock_wrapped_channel + + def test_get_otel_async_interceptor_enabled(monkeypatch, mock_otel_grpc): """Proves that get_otel_async_interceptor instantiates and returns asynchronous OpenTelemetry client interceptors with the resolved tracer provider. From 7438abcb21f5b1b0525f6cdf57617f09a6d9a8cb Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 15 Oct 2026 16:42:11 -0400 Subject: [PATCH 03/11] test(observability): simplify parametrization for inactive async channel interceptors --- .../tests/asyncio/test_observability_async.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/google-api-core/tests/asyncio/test_observability_async.py b/packages/google-api-core/tests/asyncio/test_observability_async.py index f3b4fa4b1285..2e5235cb01b1 100644 --- a/packages/google-api-core/tests/asyncio/test_observability_async.py +++ b/packages/google-api-core/tests/asyncio/test_observability_async.py @@ -54,30 +54,27 @@ async def test_async_suppressing_interceptor_invocations(method_name): @pytest.mark.parametrize( - "options_or_interceptors,tracing_enabled", + "options_or_interceptors", [ - (None, False), - ([], False), - (12345, False), - ({}, False), - (_observability.ClientOptions(), False), + None, + [], + 12345, + {}, + _observability.ClientOptions(), ], ids=[ "none_options", "empty_list", "invalid_type", - "client_options_dict_tracing_disabled", + "client_options_dict", "client_options_tracing_disabled", ], ) def test_apply_otel_async_channel_interceptors_inactive_or_empty( - mock_async_channel, monkeypatch, options_or_interceptors, tracing_enabled + mock_async_channel, monkeypatch, options_or_interceptors ): """Proves that passing inactive or empty options/interceptors returns channel unchanged.""" - if tracing_enabled: - monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") - else: - monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") result = _observability.apply_otel_async_channel_interceptors( mock_async_channel, options_or_interceptors From c764cf68a0c7ff31fbed837c831dde5a25919aba Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Fri, 16 Oct 2026 04:34:43 -0400 Subject: [PATCH 04/11] Apply suggestion from @chalmerlowe --- .../google-api-core/google/api_core/_observability.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index b7f4d7228c91..42bba69e26f4 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -493,10 +493,10 @@ def apply_otel_async_channel_interceptors( # Section 1: Typed interceptors - prepend matching interceptors to each # corresponding channel list (unary-unary, unary-stream, etc.) in order. - # We prepend (put at the very front) rather than append because gRPC interceptors - # run like layers of an onion from first to last: placing our tracing and - # suppressing interceptors at the very front ensures our Google span wraps - # around the whole call and blocks duplicate background spans before they start. + # We prepend (rather than append) because gRPC interceptors run from outside + # in: placing our tracing and suppressing interceptors at the front of the + # sequence ensures our Google span wraps are outside the entire call + # and block duplicate background spans before they start. for method_name, attr_name in _CHANNEL_INTERCEPTOR_MAPPING: target_list = getattr(channel, attr_name, None) if isinstance(target_list, list): From b07bf58fe1a56c59fc857016ad45e35544aeb412 Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Fri, 16 Oct 2026 04:48:58 -0400 Subject: [PATCH 05/11] update version number --- .../%sub/services/%service/transports/grpc_asyncio.py.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index b5baf14f0d9d..7e35880bfd7e 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -348,7 +348,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER From d89f53410bd06d7b83d0cfb9cf476be79b41335d Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Fri, 16 Oct 2026 04:49:56 -0400 Subject: [PATCH 06/11] update version number --- .../tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index c6d61c83359a..136319c7ced0 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1046,7 +1046,7 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_channel_intercepto @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_{{ service.name|snake_case }}_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() From 845883e841c1b24b8f0a4381785eac6c51661934 Mon Sep 17 00:00:00 2001 From: Chalmer Lowe Date: Fri, 16 Oct 2026 05:08:45 -0400 Subject: [PATCH 07/11] Update comment --- packages/google-api-core/google/api_core/_observability.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index 42bba69e26f4..f0578be02dad 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -493,10 +493,7 @@ def apply_otel_async_channel_interceptors( # Section 1: Typed interceptors - prepend matching interceptors to each # corresponding channel list (unary-unary, unary-stream, etc.) in order. - # We prepend (rather than append) because gRPC interceptors run from outside - # in: placing our tracing and suppressing interceptors at the front of the - # sequence ensures our Google span wraps are outside the entire call - # and block duplicate background spans before they start. + # See note in the docstring about why prepending is important here. for method_name, attr_name in _CHANNEL_INTERCEPTOR_MAPPING: target_list = getattr(channel, attr_name, None) if isinstance(target_list, list): From 79645c5d49a120d0400b0f348de8db9882d4f7f6 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 16 Oct 2026 05:09:18 -0400 Subject: [PATCH 08/11] refactor(observability): consolidate grpc interceptor imports and fallback block --- .../google/api_core/_observability.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/google-api-core/google/api_core/_observability.py b/packages/google-api-core/google/api_core/_observability.py index f0578be02dad..85398158a982 100644 --- a/packages/google-api-core/google/api_core/_observability.py +++ b/packages/google-api-core/google/api_core/_observability.py @@ -246,12 +246,23 @@ def _get_tracer_provider( from grpc import ( UnaryUnaryClientInterceptor as _SyncUnaryUnaryClientInterceptor, ) + from grpc.aio import ( + StreamStreamClientInterceptor as _AsyncStreamStreamClientInterceptor, + ) + from grpc.aio import ( + StreamUnaryClientInterceptor as _AsyncStreamUnaryClientInterceptor, + ) + from grpc.aio import ( + UnaryStreamClientInterceptor as _AsyncUnaryStreamClientInterceptor, + ) + from grpc.aio import ( + UnaryUnaryClientInterceptor as _AsyncUnaryUnaryClientInterceptor, + ) except ImportError: # pragma: NO COVER - # mypy: Fallback sentinel when optional grpc is not installed in the environment. + # mypy: Fallbacks: grpc sentinel when optional grpc is not installed in the environment. + # and distinct empty classes avoid duplicate base class 'object' TypeError at runtime. grpc = None # type: ignore[assignment] - # mypy: Fallback dummy classes when optional grpc is not installed. - # Four distinct empty classes avoid duplicate base class 'object' TypeError at runtime. class _SyncUnaryUnaryClientInterceptor: # type: ignore[no-redef] __slots__ = () @@ -264,23 +275,6 @@ class _SyncStreamUnaryClientInterceptor: # type: ignore[no-redef] class _SyncStreamStreamClientInterceptor: # type: ignore[no-redef] __slots__ = () - -try: - from grpc.aio import ( - StreamStreamClientInterceptor as _AsyncStreamStreamClientInterceptor, - ) - from grpc.aio import ( - StreamUnaryClientInterceptor as _AsyncStreamUnaryClientInterceptor, - ) - from grpc.aio import ( - UnaryStreamClientInterceptor as _AsyncUnaryStreamClientInterceptor, - ) - from grpc.aio import ( - UnaryUnaryClientInterceptor as _AsyncUnaryUnaryClientInterceptor, - ) -except ImportError: # pragma: NO COVER - # mypy: Fallback dummy classes when optional grpc is not installed. - # Four distinct empty classes avoid duplicate base class 'object' TypeError at runtime. class _AsyncUnaryUnaryClientInterceptor: # type: ignore[no-redef] __slots__ = () From a9efd3c5baf124b86d8fe2536f362a300f27f41f Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 16 Oct 2026 05:28:04 -0400 Subject: [PATCH 09/11] test(observability): separate sequence and client_options async tests --- .../tests/asyncio/test_observability_async.py | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/packages/google-api-core/tests/asyncio/test_observability_async.py b/packages/google-api-core/tests/asyncio/test_observability_async.py index 2e5235cb01b1..78fd8b8155bf 100644 --- a/packages/google-api-core/tests/asyncio/test_observability_async.py +++ b/packages/google-api-core/tests/asyncio/test_observability_async.py @@ -85,35 +85,50 @@ def test_apply_otel_async_channel_interceptors_inactive_or_empty( @pytest.mark.parametrize( - "input_mode", - ["list", "tuple", "client_options"], - ids=["list_interceptors", "tuple_interceptors", "client_options_resolved"], + "seq_type", + [list, tuple], + ids=["list", "tuple"], ) -def test_apply_otel_async_channel_interceptors_prepending_forms( - mock_async_channel, monkeypatch, input_mode +def test_apply_otel_async_channel_interceptors_sequence_prepending( + mock_async_channel, seq_type ): - """Proves that interceptors (via list, tuple, or ClientOptions) are prepended in order.""" + """Proves that interceptor sequences (list or tuple) are prepended in order.""" existing_interceptor = mock.Mock() mock_async_channel._unary_unary_interceptors = [existing_interceptor] interceptor1 = mock.Mock(spec=["intercept_unary_unary"]) interceptor2 = mock.Mock(spec=["intercept_unary_unary"]) - if input_mode == "list": - arg = [interceptor1, interceptor2] - elif input_mode == "tuple": - arg = (interceptor1, interceptor2) - else: # client_options - monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") - arg = _observability.ClientOptions() - monkeypatch.setattr( - _observability, - "get_otel_async_interceptor", - lambda opts: [interceptor1, interceptor2], - ) + result = _observability.apply_otel_async_channel_interceptors( + mock_async_channel, seq_type([interceptor1, interceptor2]) + ) + assert result is mock_async_channel + assert mock_async_channel._unary_unary_interceptors == [ + interceptor1, + interceptor2, + existing_interceptor, + ] + + +def test_apply_otel_async_channel_interceptors_client_options_prepending( + mock_async_channel, monkeypatch +): + """Proves that interceptors resolved from ClientOptions are prepended in order.""" + existing_interceptor = mock.Mock() + mock_async_channel._unary_unary_interceptors = [existing_interceptor] + + interceptor1 = mock.Mock(spec=["intercept_unary_unary"]) + interceptor2 = mock.Mock(spec=["intercept_unary_unary"]) + + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true") + monkeypatch.setattr( + _observability, + "get_otel_async_interceptor", + lambda opts: [interceptor1, interceptor2], + ) result = _observability.apply_otel_async_channel_interceptors( - mock_async_channel, arg + mock_async_channel, _observability.ClientOptions() ) assert result is mock_async_channel assert mock_async_channel._unary_unary_interceptors == [ From 6b24cf3c868a906769f2bf6ac3b7666f1a600e0b Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 16 Oct 2026 05:46:00 -0400 Subject: [PATCH 10/11] test(observability): replace edge cases parametrization with explicit tests --- .../tests/asyncio/test_observability_async.py | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/google-api-core/tests/asyncio/test_observability_async.py b/packages/google-api-core/tests/asyncio/test_observability_async.py index 78fd8b8155bf..2a2bfec7dc77 100644 --- a/packages/google-api-core/tests/asyncio/test_observability_async.py +++ b/packages/google-api-core/tests/asyncio/test_observability_async.py @@ -138,39 +138,39 @@ def test_apply_otel_async_channel_interceptors_client_options_prepending( ] -@pytest.mark.parametrize( - "case,setup_channel,interceptor_spec,expected_result", - [ - ( - "prevent_duplicates", - lambda ch, i: setattr(ch, "_unary_unary_interceptors", [i]), - ["intercept_unary_unary"], - lambda ch, i: ch._unary_unary_interceptors == [i], - ), - ( - "non_list_ignored", - lambda ch, i: setattr(ch, "_unary_unary_interceptors", "not-a-list"), - ["intercept_unary_unary"], - lambda ch, i: ch._unary_unary_interceptors == "not-a-list", - ), - ( - "generic_untyped_fallback", - lambda ch, i: setattr(ch, "_unary_unary_interceptors", ["existing"]), - [], - lambda ch, i: ch._unary_unary_interceptors == [i, "existing"], - ), - ], - ids=["prevent_duplicates", "non_list_ignored", "generic_untyped_fallback"], -) -def test_apply_otel_async_channel_interceptors_edge_cases( - mock_async_channel, case, setup_channel, interceptor_spec, expected_result +def test_apply_otel_async_channel_interceptors_prevents_duplicates(mock_async_channel): + """Proves that interceptors already present in the channel list are not duplicated.""" + interceptor = mock.Mock(spec=["intercept_unary_unary"]) + mock_async_channel._unary_unary_interceptors = [interceptor] + + result = _observability.apply_otel_async_channel_interceptors( + mock_async_channel, [interceptor] + ) + assert result is mock_async_channel + assert mock_async_channel._unary_unary_interceptors == [interceptor] + + +def test_apply_otel_async_channel_interceptors_ignores_non_list_attribute( + mock_async_channel, ): - """Proves edge case handling: deduplication, non-list attributes, and untyped fallback.""" - interceptor = mock.Mock(spec=interceptor_spec) - setup_channel(mock_async_channel, interceptor) + """Proves that channel attributes that are not lists are gracefully ignored.""" + interceptor = mock.Mock(spec=["intercept_unary_unary"]) + mock_async_channel._unary_unary_interceptors = "not-a-list" + + result = _observability.apply_otel_async_channel_interceptors( + mock_async_channel, [interceptor] + ) + assert result is mock_async_channel + assert mock_async_channel._unary_unary_interceptors == "not-a-list" + + +def test_apply_otel_async_channel_interceptors_untyped_fallback(mock_async_channel): + """Proves that untyped interceptors fallback to being prepended to all channel lists.""" + interceptor = mock.Mock(spec=[]) + mock_async_channel._unary_unary_interceptors = ["existing"] result = _observability.apply_otel_async_channel_interceptors( mock_async_channel, [interceptor] ) assert result is mock_async_channel - assert expected_result(mock_async_channel, interceptor) + assert mock_async_channel._unary_unary_interceptors == [interceptor, "existing"] From 8885b74069fbf1eb189fac7fd653ab72003b865a Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 16 Oct 2026 05:57:41 -0400 Subject: [PATCH 11/11] chore: regenerate goldens to match updated template comments --- .../asset_v1/services/asset_service/transports/grpc_asyncio.py | 2 +- .../asset/tests/unit/gapic/asset_v1/test_asset_service.py | 2 +- .../services/iam_credentials/transports/grpc_asyncio.py | 2 +- .../tests/unit/gapic/credentials_v1/test_iam_credentials.py | 2 +- .../eventarc_v1/services/eventarc/transports/grpc_asyncio.py | 2 +- .../eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py | 2 +- .../services/config_service_v2/transports/grpc_asyncio.py | 2 +- .../services/logging_service_v2/transports/grpc_asyncio.py | 2 +- .../services/metrics_service_v2/transports/grpc_asyncio.py | 2 +- .../tests/unit/gapic/logging_v2/test_config_service_v2.py | 2 +- .../tests/unit/gapic/logging_v2/test_logging_service_v2.py | 2 +- .../tests/unit/gapic/logging_v2/test_metrics_service_v2.py | 2 +- .../services/config_service_v2/transports/grpc_asyncio.py | 2 +- .../services/logging_service_v2/transports/grpc_asyncio.py | 2 +- .../services/metrics_service_v2/transports/grpc_asyncio.py | 2 +- .../tests/unit/gapic/logging_v2/test_config_service_v2.py | 2 +- .../tests/unit/gapic/logging_v2/test_logging_service_v2.py | 2 +- .../tests/unit/gapic/logging_v2/test_metrics_service_v2.py | 2 +- .../redis_v1/services/cloud_redis/transports/grpc_asyncio.py | 2 +- .../goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py | 2 +- .../redis_v1/services/cloud_redis/transports/grpc_asyncio.py | 2 +- .../tests/unit/gapic/redis_v1/test_cloud_redis.py | 2 +- .../showcase_v1beta1/services/echo/transports/grpc_asyncio.py | 2 +- .../services/identity/transports/grpc_asyncio.py | 2 +- .../services/messaging/transports/grpc_asyncio.py | 2 +- .../resumable_upload_service/transports/grpc_asyncio.py | 2 +- .../services/sequence_service/transports/grpc_asyncio.py | 2 +- .../services/testing/transports/grpc_asyncio.py | 2 +- .../showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py | 2 +- .../showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py | 2 +- .../tests/unit/gapic/showcase_v1beta1/test_messaging.py | 2 +- .../gapic/showcase_v1beta1/test_resumable_upload_service.py | 2 +- .../tests/unit/gapic/showcase_v1beta1/test_sequence_service.py | 2 +- .../showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py | 2 +- .../storage_batch_operations/transports/grpc_asyncio.py | 2 +- .../storagebatchoperations_v1/test_storage_batch_operations.py | 2 +- 36 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py index cf2717ef9096..2d4465adcf4a 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/google/cloud/asset_v1/services/asset_service/transports/grpc_asyncio.py @@ -334,7 +334,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py index 9bddb6bf4b41..17fc4c8a5c07 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py +++ b/packages/gapic-generator/tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py @@ -901,7 +901,7 @@ def test_asset_service_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_asset_service_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py index 7f5174e076e6..2465db4b3328 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/google/iam/credentials_v1/services/iam_credentials/transports/grpc_asyncio.py @@ -339,7 +339,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py index 3b186bf7a53d..f39f23ababfa 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py +++ b/packages/gapic-generator/tests/integration/goldens/credentials/tests/unit/gapic/credentials_v1/test_iam_credentials.py @@ -891,7 +891,7 @@ def test_iam_credentials_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_iam_credentials_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py index f12a3a5871c8..1656b15c6881 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/google/cloud/eventarc_v1/services/eventarc/transports/grpc_asyncio.py @@ -348,7 +348,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py index 9dd4a4da6b9a..b5e055c25ec9 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/tests/unit/gapic/eventarc_v1/test_eventarc.py @@ -922,7 +922,7 @@ def test_eventarc_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_eventarc_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index 8dbec10b8531..c03327f2585e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -334,7 +334,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index cfb9f532c742..26b8c349c478 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -332,7 +332,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index 3c5ae0968108..54ba2dbe7b1d 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -332,7 +332,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py index e0a039bb5329..acea90dd0a47 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -877,7 +877,7 @@ def test_config_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py index b65230f51039..433ff946bd8b 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -878,7 +878,7 @@ def test_logging_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index dfc28a660b34..c3809e05816a 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -876,7 +876,7 @@ def test_metrics_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py index 8dbec10b8531..c03327f2585e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/config_service_v2/transports/grpc_asyncio.py @@ -334,7 +334,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py index cfb9f532c742..26b8c349c478 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/logging_service_v2/transports/grpc_asyncio.py @@ -332,7 +332,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py index 3c5ae0968108..54ba2dbe7b1d 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/google/cloud/logging_v2/services/metrics_service_v2/transports/grpc_asyncio.py @@ -332,7 +332,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py index e8fb09f043d7..5d7d971258ae 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_config_service_v2.py @@ -877,7 +877,7 @@ def test_config_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_config_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py index b65230f51039..433ff946bd8b 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_logging_service_v2.py @@ -878,7 +878,7 @@ def test_logging_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_logging_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py index 5df6538826bd..5fc12c22406b 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/tests/unit/gapic/logging_v2/test_metrics_service_v2.py @@ -876,7 +876,7 @@ def test_metrics_service_v2_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_metrics_service_v2_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index 8f5c3dec6b97..6f5ff0d28056 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -354,7 +354,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py index 73548cf6736e..e2215a295660 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -909,7 +909,7 @@ def test_cloud_redis_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py index ec2d9a1a7457..9a64d47d80aa 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/grpc_asyncio.py @@ -354,7 +354,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py index 000b37cd1ca4..7b29386993ee 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/tests/unit/gapic/redis_v1/test_cloud_redis.py @@ -909,7 +909,7 @@ def test_cloud_redis_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_cloud_redis_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py index eda679937993..c2dd99d6b774 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/echo/transports/grpc_asyncio.py @@ -343,7 +343,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py index ce63afdb575f..90527f137ad8 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/identity/transports/grpc_asyncio.py @@ -335,7 +335,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py index ee63afa722c0..318dff105c1a 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/messaging/transports/grpc_asyncio.py @@ -340,7 +340,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 0f3ad488eb01..48a2dc9140a1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -348,7 +348,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py index 7d208378e7a9..033643cbf9bd 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/sequence_service/transports/grpc_asyncio.py @@ -338,7 +338,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py index 7650aa1cf48a..0ce4f859b8df 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/testing/transports/grpc_asyncio.py @@ -341,7 +341,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py index 4ee73ecb2765..526eb660d4ff 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_echo.py @@ -1198,7 +1198,7 @@ def test_echo_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_echo_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py index 7981c131bd63..28a7dd420d23 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_identity.py @@ -899,7 +899,7 @@ def test_identity_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_identity_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py index 2e3eff92eb11..84906c414aa2 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_messaging.py @@ -903,7 +903,7 @@ def test_messaging_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_messaging_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index a125ac846820..3d2f99ea1ef7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -904,7 +904,7 @@ def test_resumable_upload_service_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_resumable_upload_service_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py index fa3bb02ec40e..444a6f91969a 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_sequence_service.py @@ -900,7 +900,7 @@ def test_sequence_service_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_sequence_service_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py index 00851721c522..dbda59a14f09 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_testing.py @@ -897,7 +897,7 @@ def test_testing_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_testing_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock() diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py index 685e85f8430d..364bc887126e 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/transports/grpc_asyncio.py @@ -340,7 +340,7 @@ def __init__(self, *, ): # NOTE: Coverage tool ignores async interceptors in environments running # legacy google-api-core (< 2.36.0) where OpenTelemetry is unavailable. - # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= 2.36.0. + # TODO (Issue #18657): Remove once lowest constraints require google-api-core >= v2.43.0. self._grpc_channel = _observability.apply_otel_async_channel_interceptors( self._grpc_channel, self._client_options ) # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py index ac8e81ab0d64..c1f5ed51ac5c 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py @@ -901,7 +901,7 @@ def test_storage_batch_operations_grpc_asyncio_transport_channel_interceptors(): @pytest.mark.skipif( _observability is None or not hasattr(_observability, "apply_otel_async_channel_interceptors"), - reason="Requires google-api-core with OpenTelemetry async channel interceptors support", + reason="Requires google-api-core with OpenTelemetry async channel interceptors support (>= v2.43.0)", ) def test_storage_batch_operations_grpc_asyncio_transport_otel_channel_interceptor(): mock_channel = mock.Mock()