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..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 @@ -333,21 +333,25 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -319,21 +319,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -324,21 +324,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -333,21 +333,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -319,21 +319,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -317,21 +317,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -317,21 +317,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -319,21 +319,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -317,21 +317,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -317,21 +317,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -339,21 +339,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -339,21 +339,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -328,21 +328,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -320,21 +320,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -325,21 +325,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -333,21 +333,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -323,21 +323,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -326,21 +326,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -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 (>= v2.43.0)", ) 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..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 @@ -325,21 +325,25 @@ def __init__(self, *, ], ) + # Wrap channel with user-specified and logging interceptors. 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 + ) + # Configure OpenTelemetry tracing and span suppression on the channel. 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 >= v2.43.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..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 @@ -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 (>= v2.43.0)", ) 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..85398158a982 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,140 @@ 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, + ) + 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: 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] + + 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__ = () + + 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 +391,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 +428,91 @@ 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) + + # Section 1: Typed interceptors - prepend matching interceptors to each + # corresponding channel list (unary-unary, unary-stream, etc.) in order. + # 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): + matching = [ + i + for i in interceptor_list + if hasattr(i, method_name) and i not in target_list + ] + if matching: + target_list[:0] = matching + + # 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) + 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..2a2bfec7dc77 --- /dev/null +++ b/packages/google-api-core/tests/asyncio/test_observability_async.py @@ -0,0 +1,176 @@ +# 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() + + +@pytest.mark.parametrize( + "options_or_interceptors", + [ + None, + [], + 12345, + {}, + _observability.ClientOptions(), + ], + ids=[ + "none_options", + "empty_list", + "invalid_type", + "client_options_dict", + "client_options_tracing_disabled", + ], +) +def test_apply_otel_async_channel_interceptors_inactive_or_empty( + mock_async_channel, monkeypatch, options_or_interceptors +): + """Proves that passing inactive or empty options/interceptors returns channel unchanged.""" + monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "false") + + result = _observability.apply_otel_async_channel_interceptors( + mock_async_channel, options_or_interceptors + ) + assert result is mock_async_channel + assert mock_async_channel._unary_unary_interceptors == [] + assert mock_async_channel._unary_stream_interceptors == [] + + +@pytest.mark.parametrize( + "seq_type", + [list, tuple], + ids=["list", "tuple"], +) +def test_apply_otel_async_channel_interceptors_sequence_prepending( + mock_async_channel, seq_type +): + """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"]) + + 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, _observability.ClientOptions() + ) + 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(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 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 mock_async_channel._unary_unary_interceptors == [interceptor, "existing"] diff --git a/packages/google-api-core/tests/conftest.py b/packages/google-api-core/tests/conftest.py index 664ef7f95895..84cd3283f047 100644 --- a/packages/google-api-core/tests/conftest.py +++ b/packages/google-api-core/tests/conftest.py @@ -64,3 +64,33 @@ 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 + + +@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 90ac4acb3ba3..53425f42554e 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,50 @@ 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 - ) + 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_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 +@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") + 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_otel_missing(monkeypatch): - """Proves that get_otel_async_interceptor returns None when OpenTelemetry gRPC - instrumentation is not installed. - """ + +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") - 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", None): + 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 +257,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 +277,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 +1317,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__