Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,16 @@
)

import google.protobuf
from google.api_core import _feature_gating_helpers, _observability, 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]
Expand All @@ -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

Expand Down Expand Up @@ -746,17 +744,45 @@ 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,
}

# NOTE: Eager channel wrapping is only required to accommodate standard gRPC.
# OTel gRPC interceptors use a custom protocol (grpcext) that crashes standard
# grpc.intercept_channel() inside the Transport. Other wrappers (like Asyncio/REST)
# do not suffer from this specific type incompatibility.
# We create a raw_channel, apply otel capabilities to the channel and
# pass the channel to transport_init. When the Transport finds an existing
# channel it will use that, otherwise it will create one lazily.
if transport_init is SecretManagerServiceGrpcTransport:
if _observability.is_otel_capabilities_enabled(self._client_options):
# Eagerly create the channel using the Transport's classmethod
raw_channel = SecretManagerServiceGrpcTransport.create_channel(
self._api_endpoint,
credentials=credentials,
credentials_file=self._client_options.credentials_file,
scopes=self._client_options.scopes,
quota_project_id=self._client_options.quota_project_id,
)

# Apply OTel capabilities to the channel
wrapped_channel = _observability.apply_otel_capabilities_to_channel(
raw_channel, self._client_options
)

# Inject the wrapped channel into transport kwargs
transport_kwargs["channel"] = wrapped_channel

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 @@ -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

Expand All @@ -45,6 +44,13 @@

_LOGGER = std_logging.getLogger(__name__)

ClientInterceptor = Union[
grpc.UnaryUnaryClientInterceptor,
grpc.UnaryStreamClientInterceptor,
grpc.StreamUnaryClientInterceptor,
grpc.StreamStreamClientInterceptor,
]


class _LoggingClientInterceptor(grpc.UnaryUnaryClientInterceptor): # pragma: NO COVER
def intercept_unary_unary(self, continuation, client_call_details, request):
Expand Down Expand Up @@ -148,6 +154,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[ClientInterceptor]] = None,
) -> None:
"""Instantiate the transport.

Expand Down Expand Up @@ -198,6 +205,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[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
Expand Down Expand Up @@ -274,6 +284,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
Expand Down
8 changes: 8 additions & 0 deletions packages/google-cloud-secret-manager/noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,14 @@ def system(session):
)


@nox.session(python=DEFAULT_PYTHON_VERSION)
def integration(session):
"""Run integration tests with OpenTelemetry."""
session.install("-e", ".")
session.install("pytest", "opentelemetry-sdk", "opentelemetry-instrumentation-grpc")
session.run("py.test", "tests/integration", *session.posargs)


@nox.session(python=DEFAULT_PYTHON_VERSION)
def cover(session):
"""Run the final coverage report.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,14 @@
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,
pagers,
transports,
)
from google.cloud.secretmanager_v1.types import resources, service
from google.oauth2 import service_account

CRED_INFO_JSON = {
"credential_source": "/path/to/file",
Expand Down Expand Up @@ -770,6 +769,71 @@ def test_secret_manager_service_client_client_options(
)


def test_secret_manager_service_client_otel_eager_channel_injection():
# Mock the helpers to simulate OTel being enabled
mock_raw_channel = mock.Mock()
mock_wrapped_channel = mock.Mock()

# We use parenthesized context managers (Python 3.10+) to flatten the mocks.
# This prevents deep indentation while ensuring all mocks are active during
# Client instantiation.
with (
mock.patch(
"google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.is_otel_capabilities_enabled",
return_value=True,
) as mock_is_enabled,
mock.patch(
"google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.apply_otel_capabilities_to_channel",
return_value=mock_wrapped_channel,
) as mock_apply_otel,
mock.patch.object(
transports.SecretManagerServiceGrpcTransport,
"create_channel",
return_value=mock_raw_channel,
) as mock_create_channel,
mock.patch.object(
transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None
) as patched_transport_init,
):
client = SecretManagerServiceClient(transport="grpc")


mock_is_enabled.assert_called_once()
mock_create_channel.assert_called_once()
mock_apply_otel.assert_called_once_with(mock_raw_channel, mock.ANY)

called_kwargs = patched_transport_init.call_args.kwargs
assert "channel" in called_kwargs
assert called_kwargs["channel"] == mock_wrapped_channel


def test_secret_manager_service_client_otel_eager_channel_injection_disabled():
# Simulate OTel being disabled
# We use parenthesized context managers (Python 3.10+) to flatten the mocks.
# This prevents deep indentation while ensuring all mocks are active during
# Client instantiation.
with (
mock.patch(
"google.cloud.secretmanager_v1.services.secret_manager_service.client._observability.is_otel_capabilities_enabled",
return_value=False,
) as mock_is_enabled,
mock.patch.object(
transports.SecretManagerServiceGrpcTransport, "create_channel"
) as mock_create_channel,
mock.patch.object(
transports.SecretManagerServiceGrpcTransport, "__init__", return_value=None
) as patched_transport_init,
):
client = SecretManagerServiceClient(transport="grpc")

mock_is_enabled.assert_called_once()
mock_create_channel.assert_not_called()

called_kwargs = patched_transport_init.call_args.kwargs
assert "channel" not in called_kwargs



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