From e77ec272364689ba1a5fec775b4139a14a1e3827 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Tue, 18 Aug 2026 08:10:13 -0400 Subject: [PATCH 1/2] feat(secretmanager): inject OTel interceptor explicitly in gRPC transport --- .../services/secret_manager_service/client.py | 49 +++++++++++++------ .../secret_manager_service/transports/grpc.py | 11 ++++- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py index ff26bddcc57d..fea3ebfcf210 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py @@ -35,17 +35,16 @@ ) import google.protobuf +from google.api_core import _feature_gating_helpers, gapic_v1 from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions -from google.api_core import gapic_v1 from google.api_core import retry as retries from google.auth import credentials as ga_credentials # type: ignore from google.auth.exceptions import MutualTLSChannelError # type: ignore from google.auth.transport import mtls # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore -from google.oauth2 import service_account # type: ignore - from google.cloud.secretmanager_v1 import gapic_version as package_version +from google.oauth2 import service_account # type: ignore try: OptionalRetry = Union[retries.Retry, gapic_v1.method._MethodDefault, None] @@ -68,7 +67,6 @@ import google.protobuf.field_mask_pb2 as field_mask_pb2 # type: ignore import google.protobuf.timestamp_pb2 as timestamp_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore - from google.cloud.secretmanager_v1.services.secret_manager_service import pagers from google.cloud.secretmanager_v1.types import resources, service @@ -745,18 +743,39 @@ def __init__( if isinstance(transport, str) or transport is None else cast(Callable[..., SecretManagerServiceTransport], transport) ) + # Resolve interceptors for gRPC + interceptors = None + if transport_init is SecretManagerServiceGrpcTransport: + is_tracing_enabled = _feature_gating_helpers.resolve_feature_flags( + env_var="GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", + feature_key="tracer_provider", + configuration=self._client_options, + ) + if is_tracing_enabled: + try: + import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] + tracer_provider = getattr(self._client_options, "tracer_provider", None) + interceptor = otel_grpc.client_interceptor(tracer_provider=tracer_provider) + interceptors = [interceptor] + except ImportError: + pass + # initialize with the provided callable or the passed in class - self._transport = transport_init( - credentials=credentials, - credentials_file=self._client_options.credentials_file, - host=self._api_endpoint, - scopes=self._client_options.scopes, - client_cert_source_for_mtls=self._client_cert_source, - quota_project_id=self._client_options.quota_project_id, - client_info=client_info, - always_use_jwt_access=True, - api_audience=self._client_options.api_audience, - ) + transport_kwargs = { + "credentials": credentials, + "credentials_file": self._client_options.credentials_file, + "host": self._api_endpoint, + "scopes": self._client_options.scopes, + "client_cert_source_for_mtls": self._client_cert_source, + "quota_project_id": self._client_options.quota_project_id, + "client_info": client_info, + "always_use_jwt_access": True, + "api_audience": self._client_options.api_audience, + } + if interceptors is not None: + transport_kwargs["interceptors"] = interceptors + + self._transport = transport_init(**transport_kwargs) if "async" not in str(self._transport): if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor( diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py index 51530553e705..fa9f87d2d81e 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/transports/grpc.py @@ -30,9 +30,8 @@ from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.cloud.location import locations_pb2 # type: ignore -from google.protobuf.json_format import MessageToJson - from google.cloud.secretmanager_v1.types import resources, service +from google.protobuf.json_format import MessageToJson from .base import DEFAULT_CLIENT_INFO, SecretManagerServiceTransport @@ -148,6 +147,7 @@ def __init__( client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, api_audience: Optional[str] = None, + interceptors: Optional[Sequence[object]] = None, ) -> None: """Instantiate the transport. @@ -198,6 +198,9 @@ def __init__( to the service that will be set when using certain 3rd party authentication flows. Audience is typically a resource identifier. If not set, the host value will be used as a default. + interceptors (Optional[Sequence[grpc.ClientInterceptor]]): + Additional interceptors to be injected into the gRPC channel pipeline. + These are executed in order. Raises: google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport @@ -274,6 +277,10 @@ def __init__( ], ) + if interceptors: + for i in interceptors: + self._grpc_channel = grpc.intercept_channel(self._grpc_channel, i) + self._interceptor = _LoggingClientInterceptor() self._logged_channel = grpc.intercept_channel( self._grpc_channel, self._interceptor From d1a7a0ff857f3d67f762b9d7e0f4fa7e1192f974 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 21 Aug 2026 07:54:23 -0400 Subject: [PATCH 2/2] feat(secretmanager): refactor OTel interceptor injection to use centralized helper --- .../services/secret_manager_service/client.py | 18 ++------ .../test_secret_manager_service.py | 46 ++++++++++++++++++- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py index fea3ebfcf210..e1de597276f4 100644 --- a/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py +++ b/packages/google-cloud-secret-manager/google/cloud/secretmanager_v1/services/secret_manager_service/client.py @@ -35,7 +35,7 @@ ) import google.protobuf -from google.api_core import _feature_gating_helpers, gapic_v1 +from google.api_core import _feature_gating_helpers, _otel_helpers, gapic_v1 from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import retry as retries @@ -746,19 +746,11 @@ def __init__( # Resolve interceptors for gRPC interceptors = None if transport_init is SecretManagerServiceGrpcTransport: - is_tracing_enabled = _feature_gating_helpers.resolve_feature_flags( - env_var="GOOGLE_CLOUD_PYTHON_TRACING_ENABLED", - feature_key="tracer_provider", - configuration=self._client_options, + otel_interceptor = _otel_helpers.get_otel_grpc_interceptor( + self._client_options ) - if is_tracing_enabled: - try: - import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] - tracer_provider = getattr(self._client_options, "tracer_provider", None) - interceptor = otel_grpc.client_interceptor(tracer_provider=tracer_provider) - interceptors = [interceptor] - except ImportError: - pass + if otel_interceptor: + interceptors = [otel_interceptor] # initialize with the provided callable or the passed in class transport_kwargs = { diff --git a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py index 722ac1109a06..83be4cdc9e40 100644 --- a/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py +++ b/packages/google-cloud-secret-manager/tests/unit/gapic/secretmanager_v1/test_secret_manager_service.py @@ -61,8 +61,6 @@ from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError from google.cloud.location import locations_pb2 -from google.oauth2 import service_account - from google.cloud.secretmanager_v1.services.secret_manager_service import ( SecretManagerServiceAsyncClient, SecretManagerServiceClient, @@ -70,6 +68,7 @@ transports, ) from google.cloud.secretmanager_v1.types import resources, service +from google.oauth2 import service_account CRED_INFO_JSON = { "credential_source": "/path/to/file", @@ -770,6 +769,49 @@ def test_secret_manager_service_client_client_options( ) +def test_secret_manager_service_client_otel_interceptor_injection(): + # Mock the helper to return a sentinel interceptor + mock_interceptor = mock.Mock() + + with mock.patch( + "google.cloud.secretmanager_v1.services.secret_manager_service.client._otel_helpers.get_otel_grpc_interceptor" + ) as mock_get_interceptor: + mock_get_interceptor.return_value = mock_interceptor + + with mock.patch.object( + transports.SecretManagerServiceGrpcTransport, "__init__" + ) as patched_transport_init: + patched_transport_init.return_value = None + + client = SecretManagerServiceClient(transport="grpc") + + mock_get_interceptor.assert_called_once() + + called_kwargs = patched_transport_init.call_args.kwargs + assert "interceptors" in called_kwargs + assert called_kwargs["interceptors"] == [mock_interceptor] + + +def test_secret_manager_service_client_otel_interceptor_injection_disabled(): + # The helper does not return an interceptor + with mock.patch( + "google.cloud.secretmanager_v1.services.secret_manager_service.client._otel_helpers.get_otel_grpc_interceptor" + ) as mock_get_interceptor: + mock_get_interceptor.return_value = None + + with mock.patch.object( + transports.SecretManagerServiceGrpcTransport, "__init__" + ) as patched_transport_init: + patched_transport_init.return_value = None + + client = SecretManagerServiceClient(transport="grpc") + + mock_get_interceptor.assert_called_once() + + called_kwargs = patched_transport_init.call_args.kwargs + assert "interceptors" not in called_kwargs + + @pytest.mark.parametrize( "client_class,transport_class,transport_name,use_client_cert_env", [