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..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,17 +35,16 @@ ) import google.protobuf +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 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,31 @@ 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: + otel_interceptor = _otel_helpers.get_otel_grpc_interceptor( + self._client_options + ) + if otel_interceptor: + interceptors = [otel_interceptor] + # 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 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", [