Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@
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

try:
# mypy: _observability was added in google-api-core 2.35.0; guard for older versions
from google.api_core import _observability # type: ignore[attr-defined]
except ImportError:
_observability = None # type: ignore[assignment]

from google.cloud.secretmanager_v1 import gapic_version as package_version
from google.cloud.secretmanager_v1._compat import (
get_api_endpoint,
Expand Down Expand Up @@ -609,17 +614,34 @@ def __init__(
else cast(Callable[..., SecretManagerServiceTransport], transport)
)
# 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,
}

# When OpenTelemetry tracing is enabled, obtain the channel interceptor
# and pass it to the transport.
if (
transport_init is SecretManagerServiceGrpcTransport
and _observability is not None
):
otel_interceptor = _observability.get_otel_interceptor(
self._client_options
)
if otel_interceptor is not None:
interceptors = transport_kwargs.get("interceptors", [])
transport_kwargs["interceptors"] = [otel_interceptor] + list(
interceptors
)

self._transport = transport_init(**transport_kwargs)

if "async" not in str(self._transport):
if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
import grpc # type: ignore
import proto # type: ignore
from google.api_core import gapic_v1, grpc_helpers

try:
# mypy: ClientInterceptor was added in google-api-core 2.35.0; guard for older versions
from google.api_core.grpc_helpers import ClientInterceptor # type: ignore[attr-defined]
except ImportError:
ClientInterceptor = grpc.ClientInterceptor # type: ignore[misc,assignment]
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
Expand Down Expand Up @@ -148,6 +154,14 @@ 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[
Union[
ClientInterceptor,
Callable[[grpc.Channel], grpc.Channel],
]
]
] = None,
) -> None:
"""Instantiate the transport.

Expand Down Expand Up @@ -198,6 +212,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[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]):
Additional interceptors (or callables that apply interceptors) to apply to the
gRPC channel.

Raises:
google.auth.exceptions.MutualTLSChannelError: If mutual TLS transport
Expand Down Expand Up @@ -274,6 +291,10 @@ def __init__(
],
)

apply_interceptors = getattr(grpc_helpers, "apply_channel_interceptors", None)
if apply_interceptors is not None:
self._grpc_channel = apply_interceptors(self._grpc_channel, interceptors)

self._interceptor = _LoggingClientInterceptor()
self._logged_channel = grpc.intercept_channel(
self._grpc_channel, self._interceptor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# limitations under the License.
#
import asyncio
import functools
import json
import math
import os
Expand Down Expand Up @@ -476,6 +477,122 @@ def test_secret_manager_service_client_client_options(
)


def test_secret_manager_service_client_otel_channel_injection_enabled():
"""Proves that when OpenTelemetry tracing is enabled:

1. SecretManagerServiceClient obtains the channel interceptor via
_observability.get_otel_interceptor passing client_options.
2. The interceptor is passed into transport kwargs under 'interceptors',
allowing the Transport to apply it via apply_channel_interceptors.
"""
mock_interceptor = mock.Mock()
with (
mock.patch(
"google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_interceptor",
return_value=mock_interceptor,
) as mock_get_interceptor,
mock.patch.object(
transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None
) as patched_transport_init,
):
client = SecretManagerServiceClient(transport="grpc")

mock_get_interceptor.assert_called_once_with(client._client_options)
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_channel_injection_disabled():
"""Proves that when OpenTelemetry tracing is disabled:

1. SecretManagerServiceClient checks for an OTel interceptor and receives None.
2. No OTel interceptor is added to the transport constructor kwargs.
"""
with (
mock.patch(
"google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.get_otel_interceptor",
return_value=None,
) as mock_get_interceptor,
mock.patch.object(
transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None
) as patched_transport_init,
):
client = SecretManagerServiceClient(transport="grpc")

mock_get_interceptor.assert_called_once_with(client._client_options)
called_kwargs = patched_transport_init.call_args.kwargs
interceptors = called_kwargs.get("interceptors", [])
assert not interceptors


def test_secret_manager_service_client_observability_import_error():
"""Proves that when _observability cannot be imported (older google-api-core),
SecretManagerServiceClient instantiates gracefully without error.
"""
with (
mock.patch(
"google.cloud.secretmanager_v1.services.secret_manager_service.client._observability",
None,
),
mock.patch.object(
transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None
) as patched_transport_init,
):
SecretManagerServiceClient(transport="grpc")
called_kwargs = patched_transport_init.call_args.kwargs
assert not called_kwargs.get("interceptors", [])


def test_secret_manager_service_grpc_transport_interceptors():
"""Proves that SecretManagerServiceGrpcTransport accepts channel interceptors
and invokes grpc_helpers.apply_channel_interceptors to apply them to the underlying
gRPC channel.
"""
mock_interceptor = mock.Mock()
mock_channel = mock.Mock()

with (
mock.patch.object(
transports.SecretManagerServiceGrpcTransport,
"create_channel",
return_value=mock_channel,
),
mock.patch(
"google.api_core.grpc_helpers.apply_channel_interceptors",
return_value=mock_channel,
) as mock_apply_interceptors,
):
transports.SecretManagerServiceGrpcTransport(
interceptors=[mock_interceptor],
)

mock_apply_interceptors.assert_called_once_with(
mock_channel, [mock_interceptor]
)


def test_secret_manager_service_grpc_transport_custom_channel_interceptors():
"""Proves that SecretManagerServiceGrpcTransport wraps explicitly passed custom channels
using grpc_helpers.apply_channel_interceptors.
"""
mock_interceptor = mock.Mock()
mock_custom_channel = mock.Mock(spec=grpc.Channel)

with mock.patch(
"google.api_core.grpc_helpers.apply_channel_interceptors",
return_value=mock_custom_channel,
) as mock_apply_interceptors:
transports.SecretManagerServiceGrpcTransport(
channel=mock_custom_channel,
interceptors=[mock_interceptor],
)

mock_apply_interceptors.assert_called_once_with(
mock_custom_channel, [mock_interceptor]
)


@pytest.mark.parametrize(
"client_class,transport_class,transport_name,use_client_cert_env",
[
Expand Down
Loading