From 69ce91bc9aa44183313b9d250f6084f00c0c86c2 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Wed, 14 Oct 2026 13:22:33 -0400 Subject: [PATCH 1/7] feat(secretmanager): add OpenTelemetry distributed tracing samples Add standalone snippets and unit test suites demonstrating native OpenTelemetry distributed tracing for SecretManagerServiceClient and SecretManagerServiceAsyncClient: - Basic quickstart snippet using ConsoleSpanExporter and environment variable activation - Google Cloud Trace exporter sample using CloudTraceSpanExporter and BatchSpanProcessor - Programmatic configuration sample using ClientOptions(tracer_provider=...) - Asynchronous client sample using SecretManagerServiceAsyncClient - Mocked unit test suites with conftest fixture for isolated test execution --- secretmanager/snippets/conftest.py | 27 +++ secretmanager/snippets/requirements-test.txt | 3 +- secretmanager/snippets/requirements.txt | 3 + secretmanager/snippets/secretmanager_trace.py | 84 ++++++++++ .../snippets/secretmanager_trace_async.py | 87 ++++++++++ .../secretmanager_trace_async_test.py | 158 ++++++++++++++++++ .../secretmanager_trace_client_options.py | 81 +++++++++ ...secretmanager_trace_client_options_test.py | 110 ++++++++++++ .../secretmanager_trace_cloud_trace.py | 86 ++++++++++ .../secretmanager_trace_cloud_trace_test.py | 117 +++++++++++++ .../snippets/secretmanager_trace_test.py | 148 ++++++++++++++++ 11 files changed, 903 insertions(+), 1 deletion(-) create mode 100644 secretmanager/snippets/conftest.py create mode 100644 secretmanager/snippets/secretmanager_trace.py create mode 100644 secretmanager/snippets/secretmanager_trace_async.py create mode 100644 secretmanager/snippets/secretmanager_trace_async_test.py create mode 100644 secretmanager/snippets/secretmanager_trace_client_options.py create mode 100644 secretmanager/snippets/secretmanager_trace_client_options_test.py create mode 100644 secretmanager/snippets/secretmanager_trace_cloud_trace.py create mode 100644 secretmanager/snippets/secretmanager_trace_cloud_trace_test.py create mode 100644 secretmanager/snippets/secretmanager_trace_test.py diff --git a/secretmanager/snippets/conftest.py b/secretmanager/snippets/conftest.py new file mode 100644 index 00000000000..c1ad6634ad2 --- /dev/null +++ b/secretmanager/snippets/conftest.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# 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. + +"""Shared test fixtures for Secret Manager tracing snippets.""" + +import pytest +from opentelemetry import trace + + +@pytest.fixture(autouse=True) +def reset_otel_global_tracer_provider(): + """Resets OpenTelemetry's global TracerProvider between tests.""" + yield + trace._TRACER_PROVIDER_SET_ONCE._done = False + trace._TRACER_PROVIDER = None diff --git a/secretmanager/snippets/requirements-test.txt b/secretmanager/snippets/requirements-test.txt index d9ad1f716cc..3ade3d7b635 100644 --- a/secretmanager/snippets/requirements-test.txt +++ b/secretmanager/snippets/requirements-test.txt @@ -1 +1,2 @@ -pytest==9.0.3 \ No newline at end of file +pytest==9.0.3 +pytest-asyncio>=0.23.0 \ No newline at end of file diff --git a/secretmanager/snippets/requirements.txt b/secretmanager/snippets/requirements.txt index 2e6bd673f37..1d2d7279fee 100644 --- a/secretmanager/snippets/requirements.txt +++ b/secretmanager/snippets/requirements.txt @@ -2,3 +2,6 @@ protobuf==6.33.6 google-cloud-resource-manager==1.18.0 google-cloud-secret-manager==2.29.0 google-crc32c==1.8.0 +opentelemetry-api>=1.20.0 +opentelemetry-sdk>=1.20.0 +opentelemetry-exporter-gcp-trace>=1.7.0 diff --git a/secretmanager/snippets/secretmanager_trace.py b/secretmanager/snippets/secretmanager_trace.py new file mode 100644 index 00000000000..4be691547ee --- /dev/null +++ b/secretmanager/snippets/secretmanager_trace.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# 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. + +"""Sample demonstrating OpenTelemetry distributed tracing with SecretManagerServiceClient.""" + +# [START secretmanager_enable_tracing] +import os + +from google.cloud import secretmanager +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor + + +def access_secret_version_with_tracing( + project_id: str, + secret_id: str, + version_id: str = "latest", +) -> str: + """Access a secret version in Secret Manager with OpenTelemetry tracing enabled. + + Args: + project_id: Google Cloud project ID. + secret_id: ID of the secret to access. + version_id: ID of the secret version to access (defaults to "latest"). + + Returns: + The secret payload decoded as a UTF-8 string. + """ + # 1. Initialize the OpenTelemetry TracerProvider with a Console exporter + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + trace.set_tracer_provider(provider) + + # 2. Enable Google Cloud client library tracing + os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" + + # 3. Instantiate the client (automatically discovers the active TracerProvider) + client = secretmanager.SecretManagerServiceClient() + + # 4. Construct the resource name and invoke the RPC + name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" + + try: + response = client.access_secret_version(request={"name": name}) + payload = response.payload.data.decode("UTF-8") + return payload + finally: + # Flush buffered spans before program completion + provider.shutdown() + + +# [END secretmanager_enable_tracing] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Access a Secret Manager secret version with OpenTelemetry tracing." + ) + parser.add_argument("--project-id", required=True, help="Google Cloud Project ID") + parser.add_argument("--secret-id", required=True, help="Secret ID") + parser.add_argument("--version-id", default="latest", help="Secret version ID") + + args = parser.parse_args() + secret_data = access_secret_version_with_tracing( + project_id=args.project_id, + secret_id=args.secret_id, + version_id=args.version_id, + ) + print(f"Retrieved secret payload: {secret_data}") diff --git a/secretmanager/snippets/secretmanager_trace_async.py b/secretmanager/snippets/secretmanager_trace_async.py new file mode 100644 index 00000000000..e3ec0e795d5 --- /dev/null +++ b/secretmanager/snippets/secretmanager_trace_async.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# 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. + +"""Sample demonstrating async OpenTelemetry distributed tracing with SecretManagerServiceAsyncClient.""" + +# [START secretmanager_enable_tracing_async] +import asyncio +import os + +from google.cloud import secretmanager_v1 +from opentelemetry import trace +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor + + +async def access_secret_version_with_tracing_async( + project_id: str, + secret_id: str, + version_id: str = "latest", +) -> str: + """Access a secret version using SecretManagerServiceAsyncClient with OpenTelemetry tracing. + + Args: + project_id: Google Cloud project ID. + secret_id: ID of the secret to access. + version_id: ID of the secret version to access (defaults to "latest"). + + Returns: + The secret payload decoded as a UTF-8 string. + """ + # 1. Initialize the OpenTelemetry TracerProvider with a Console exporter + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + trace.set_tracer_provider(provider) + + # 2. Enable Google Cloud client library tracing + os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" + + # 3. Instantiate the async client + client = secretmanager_v1.SecretManagerServiceAsyncClient() + + # 4. Construct resource name and invoke the async RPC + name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" + + try: + response = await client.access_secret_version(request={"name": name}) + payload = response.payload.data.decode("UTF-8") + return payload + finally: + # Flush buffered spans before program completion + provider.shutdown() + + +# [END secretmanager_enable_tracing_async] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Access a Secret Manager secret version asynchronously with OpenTelemetry tracing." + ) + parser.add_argument("--project-id", required=True, help="Google Cloud Project ID") + parser.add_argument("--secret-id", required=True, help="Secret ID") + parser.add_argument("--version-id", default="latest", help="Secret version ID") + + args = parser.parse_args() + secret_data = asyncio.run( + access_secret_version_with_tracing_async( + project_id=args.project_id, + secret_id=args.secret_id, + version_id=args.version_id, + ) + ) + print(f"Retrieved secret payload asynchronously: {secret_data}") diff --git a/secretmanager/snippets/secretmanager_trace_async_test.py b/secretmanager/snippets/secretmanager_trace_async_test.py new file mode 100644 index 00000000000..d9deb43d5d8 --- /dev/null +++ b/secretmanager/snippets/secretmanager_trace_async_test.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +# 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. + +"""Unit tests for secretmanager_trace_async snippet.""" + +import concurrent.futures +import os +from unittest import mock + +import grpc +import pytest +from google.api_core import _observability +from google.api_core.client_options import ClientOptions +from google.api_core.gapic_v1 import method_async +from google.cloud import secretmanager +from google.cloud.secretmanager_v1.services.secret_manager_service.transports.grpc_asyncio import ( + SecretManagerServiceGrpcAsyncIOTransport, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +from secretmanager_trace_async import ( + access_secret_version_with_tracing_async, +) + +# Ensure OTel tracing capabilities are enabled in the environment +os.environ["GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED"] = "true" +os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" + + +@pytest.fixture +def mock_grpc_server(): + """Starts an in-memory gRPC server returning mock responses.""" + server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) + port = server.add_insecure_port("127.0.0.1:0") + + mock_resp = secretmanager.AccessSecretVersionResponse( + name="projects/test-project/secrets/my-secret/versions/1", + payload=secretmanager.SecretPayload(data=b"mock-async-token-xyz"), + ) + + generic_handler = grpc.method_handlers_generic_handler( + "google.cloud.secretmanager.v1.SecretManagerService", + { + "AccessSecretVersion": grpc.unary_unary_rpc_method_handler( + lambda r, c: secretmanager.AccessSecretVersionResponse.serialize( + mock_resp + ), + request_deserializer=lambda b: b, + response_serializer=lambda b: b, + ) + }, + ) + server.add_generic_rpc_handlers((generic_handler,)) + server.start() + try: + yield port + finally: + server.stop(grace=None) + + +@pytest.fixture +def otel_setup(): + """Configures an in-memory OpenTelemetry tracer provider and exporter.""" + provider = TracerProvider() + exporter = InMemorySpanExporter() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + return provider, exporter + + +@pytest.mark.asyncio +async def test_access_secret_version_with_tracing_async_spans( + mock_grpc_server, otel_setup +): + """Verifies that access_secret_version_with_tracing_async emits T3 and T4 spans.""" + port = mock_grpc_server + provider, exporter = otel_setup + + orig_init = SecretManagerServiceGrpcAsyncIOTransport.__init__ + + def patched_init(self, *args, **kwargs): + opts = ClientOptions(api_endpoint=f"127.0.0.1:{port}", tracer_provider=provider) + interceptors = _observability.get_otel_async_interceptor(opts) + kwargs["channel"] = grpc.aio.insecure_channel( + f"127.0.0.1:{port}", interceptors=interceptors + ) + orig_init(self, *args, **kwargs) + # Ensure modern GAPIC async tracing wrapper is attached + self._wrapped_methods[self.access_secret_version] = method_async.wrap_method( + self.access_secret_version, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion", + client_options=opts, + kind="grpc_asyncio", + ) + + with mock.patch("secretmanager_trace_async.TracerProvider", return_value=provider): + with mock.patch.object( + SecretManagerServiceGrpcAsyncIOTransport, "__init__", patched_init + ): + payload = await access_secret_version_with_tracing_async( + project_id="test-project", + secret_id="my-secret", + version_id="1", + ) + + # 1. Verify returned payload + assert payload == "mock-async-token-xyz" + + # 2. Inspect emitted spans + spans = exporter.get_finished_spans() + assert len(spans) == 2, f"Expected 2 spans (T3 + T4), but got {len(spans)}" + + t3_spans = [s for s in spans if s.parent is None] + t4_spans = [s for s in spans if s.parent is not None] + + assert len(t3_spans) == 1, "Expected exactly 1 T3 (logical method) span" + assert len(t4_spans) == 1, "Expected exactly 1 T4 (wire attempt) span" + + t3_span = t3_spans[0] + t4_span = t4_spans[0] + + # 3. Verify T3 logical client span attributes + assert ( + t3_span.name + == "google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion" + ) + assert t3_span.attributes.get("rpc.system.name") == "grpc" + assert t3_span.attributes.get("rpc.response.status_code") == "OK" + + # 4. Verify T4 wire attempt span attributes + assert ( + t4_span.name + == "google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion" + ) + assert ( + t4_span.attributes.get("rpc.service") + == "google.cloud.secretmanager.v1.SecretManagerService" + ) + assert t4_span.attributes.get("rpc.grpc.status_code") == 0 + assert t4_span.attributes.get("rpc.response.status_code") == "OK" + + # 5. Verify span hierarchy / parent-child linkage + assert t4_span.parent.span_id == t3_span.context.span_id diff --git a/secretmanager/snippets/secretmanager_trace_client_options.py b/secretmanager/snippets/secretmanager_trace_client_options.py new file mode 100644 index 00000000000..d002e0c5ce0 --- /dev/null +++ b/secretmanager/snippets/secretmanager_trace_client_options.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# 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. + +"""Sample demonstrating programmatic OpenTelemetry configuration via ClientOptions with SecretManagerServiceClient.""" + +# [START secretmanager_trace_client_options] +from google.api_core.client_options import ClientOptions +from google.cloud import secretmanager +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor + + +def access_secret_version_with_client_options( + project_id: str, + secret_id: str, + version_id: str = "latest", +) -> str: + """Access a secret version configuring tracing programmatically via ClientOptions. + + Args: + project_id: Google Cloud project ID. + secret_id: ID of the secret to access. + version_id: ID of the secret version to access (defaults to "latest"). + + Returns: + The secret payload decoded as a UTF-8 string. + """ + # 1. Initialize an independent TracerProvider (no global registration required) + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) + + # 2. Pass the TracerProvider directly via ClientOptions + # Tracing is activated specifically for this client instance, even without + # setting the GOOGLE_SDK_PYTHON_TRACING_ENABLED environment variable. + options = ClientOptions(tracer_provider=provider) + client = secretmanager.SecretManagerServiceClient(client_options=options) + + # 3. Construct resource name and invoke RPC + name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" + + try: + response = client.access_secret_version(request={"name": name}) + payload = response.payload.data.decode("UTF-8") + return payload + finally: + # Flush buffered spans before program completion + provider.shutdown() + + +# [END secretmanager_trace_client_options] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Access a Secret Manager secret version with ClientOptions-configured tracing." + ) + parser.add_argument("--project-id", required=True, help="Google Cloud Project ID") + parser.add_argument("--secret-id", required=True, help="Secret ID") + parser.add_argument("--version-id", default="latest", help="Secret version ID") + + args = parser.parse_args() + secret_data = access_secret_version_with_client_options( + project_id=args.project_id, + secret_id=args.secret_id, + version_id=args.version_id, + ) + print(f"Retrieved secret payload: {secret_data}") diff --git a/secretmanager/snippets/secretmanager_trace_client_options_test.py b/secretmanager/snippets/secretmanager_trace_client_options_test.py new file mode 100644 index 00000000000..d443ce7e58e --- /dev/null +++ b/secretmanager/snippets/secretmanager_trace_client_options_test.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# 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. + +"""Unit tests for secretmanager_trace_client_options snippet.""" + +import concurrent.futures +import os +from unittest import mock + +import grpc +import pytest +from google.api_core.gapic_v1 import method +from google.cloud import secretmanager +from google.cloud.secretmanager_v1.services.secret_manager_service.transports.grpc import ( + SecretManagerServiceGrpcTransport, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +from secretmanager_trace_client_options import access_secret_version_with_client_options + +os.environ["GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED"] = "true" + + +@pytest.fixture +def mock_grpc_server(): + """Starts an in-memory gRPC server returning mock responses.""" + server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) + port = server.add_insecure_port("127.0.0.1:0") + + mock_resp = secretmanager.AccessSecretVersionResponse( + name="projects/test-project/secrets/my-secret/versions/1", + payload=secretmanager.SecretPayload(data=b"mock-client-options-token"), + ) + + generic_handler = grpc.method_handlers_generic_handler( + "google.cloud.secretmanager.v1.SecretManagerService", + { + "AccessSecretVersion": grpc.unary_unary_rpc_method_handler( + lambda r, c: secretmanager.AccessSecretVersionResponse.serialize( + mock_resp + ), + request_deserializer=lambda b: b, + response_serializer=lambda b: b, + ) + }, + ) + server.add_generic_rpc_handlers((generic_handler,)) + server.start() + try: + yield port + finally: + server.stop(grace=None) + + +def test_access_secret_version_with_client_options(mock_grpc_server, monkeypatch): + """Verifies that access_secret_version_with_client_options emits spans via ClientOptions.""" + port = mock_grpc_server + provider = TracerProvider() + exporter = InMemorySpanExporter() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + + orig_init = SecretManagerServiceGrpcTransport.__init__ + + def patched_init(self, *args, **kwargs): + kwargs["channel"] = grpc.insecure_channel(f"127.0.0.1:{port}") + client_opts = kwargs.get("client_options") + orig_init(self, *args, **kwargs) + if client_opts and client_opts.tracer_provider: + self._wrapped_methods[self.access_secret_version] = method.wrap_method( + self.access_secret_version, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion", + client_options=client_opts, + kind="grpc", + ) + + monkeypatch.setattr(SecretManagerServiceGrpcTransport, "__init__", patched_init) + monkeypatch.delenv("GOOGLE_SDK_PYTHON_TRACING_ENABLED", raising=False) + + with mock.patch( + "secretmanager_trace_client_options.TracerProvider", return_value=provider + ): + payload = access_secret_version_with_client_options( + project_id="test-project", + secret_id="my-secret", + version_id="1", + ) + + assert payload == "mock-client-options-token" + + spans = exporter.get_finished_spans() + assert len(spans) >= 1 + t3_span = spans[-1] + assert "AccessSecretVersion" in t3_span.name + assert t3_span.status.is_ok diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace.py b/secretmanager/snippets/secretmanager_trace_cloud_trace.py new file mode 100644 index 00000000000..601dc4dffb3 --- /dev/null +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# 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. + +"""Sample demonstrating exporting OpenTelemetry traces to Google Cloud Trace with SecretManagerServiceClient.""" + +# [START secretmanager_trace_cloud_trace] +import os + +from google.cloud import secretmanager +from opentelemetry import trace +from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + + +def access_secret_version_cloud_trace( + project_id: str, + secret_id: str, + version_id: str = "latest", +) -> str: + """Access a secret version in Secret Manager exporting traces to Google Cloud Trace. + + Args: + project_id: Google Cloud project ID. + secret_id: ID of the secret to access. + version_id: ID of the secret version to access (defaults to "latest"). + + Returns: + The secret payload decoded as a UTF-8 string. + """ + # 1. Initialize OpenTelemetry TracerProvider with CloudTraceSpanExporter and BatchSpanProcessor + provider = TracerProvider() + cloud_exporter = CloudTraceSpanExporter() + provider.add_span_processor(BatchSpanProcessor(cloud_exporter)) + trace.set_tracer_provider(provider) + + # 2. Enable Google Cloud client library tracing + os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" + + # 3. Instantiate the client (automatically discovers the active TracerProvider) + client = secretmanager.SecretManagerServiceClient() + + # 4. Construct resource name and invoke RPC + name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" + + try: + response = client.access_secret_version(request={"name": name}) + payload = response.payload.data.decode("UTF-8") + return payload + finally: + # Flush buffered spans to Cloud Trace before exiting + provider.shutdown() + + +# [END secretmanager_trace_cloud_trace] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + description="Access a Secret Manager secret version and export traces to Google Cloud Trace." + ) + parser.add_argument("--project-id", required=True, help="Google Cloud Project ID") + parser.add_argument("--secret-id", required=True, help="Secret ID") + parser.add_argument("--version-id", default="latest", help="Secret version ID") + + args = parser.parse_args() + secret_data = access_secret_version_cloud_trace( + project_id=args.project_id, + secret_id=args.secret_id, + version_id=args.version_id, + ) + print(f"Retrieved secret payload: {secret_data}") diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py new file mode 100644 index 00000000000..ff7d91d90b8 --- /dev/null +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# 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. + +"""Unit tests for secretmanager_trace_cloud_trace snippet.""" + +import concurrent.futures +import os +import sys +from unittest import mock + +import grpc +import pytest +from google.cloud import secretmanager +from google.cloud.secretmanager_v1.services.secret_manager_service.transports.grpc import ( + SecretManagerServiceGrpcTransport, +) +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult + + +# Mock opentelemetry.exporter.cloud_trace if not installed in the environment +class MockCloudTraceSpanExporter(SpanExporter): + def __init__(self, *args, **kwargs): + self.exported_spans = [] + + def export(self, spans): + self.exported_spans.extend(spans) + return SpanExportResult.SUCCESS + + def shutdown(self): + pass + + +if "opentelemetry.exporter.cloud_trace" not in sys.modules: + mock_module = mock.MagicMock() + mock_module.CloudTraceSpanExporter = MockCloudTraceSpanExporter + sys.modules["opentelemetry.exporter.cloud_trace"] = mock_module + +# flake8: E402 is ignored here because we must patch sys.modules with a mock +# CloudTraceSpanExporter before importing the snippet module in environments +# where opentelemetry-exporter-gcp-trace is not installed. +from secretmanager_trace_cloud_trace import ( # noqa: E402 + access_secret_version_cloud_trace, +) + +os.environ["GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED"] = "true" +os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" + + +@pytest.fixture +def mock_grpc_server(): + """Starts an in-memory gRPC server returning mock responses.""" + server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) + port = server.add_insecure_port("127.0.0.1:0") + + mock_resp = secretmanager.AccessSecretVersionResponse( + name="projects/test-project/secrets/my-secret/versions/1", + payload=secretmanager.SecretPayload(data=b"mock-cloud-trace-secret"), + ) + + generic_handler = grpc.method_handlers_generic_handler( + "google.cloud.secretmanager.v1.SecretManagerService", + { + "AccessSecretVersion": grpc.unary_unary_rpc_method_handler( + lambda r, c: secretmanager.AccessSecretVersionResponse.serialize( + mock_resp + ), + request_deserializer=lambda b: b, + response_serializer=lambda b: b, + ) + }, + ) + server.add_generic_rpc_handlers((generic_handler,)) + server.start() + try: + yield port + finally: + server.stop(grace=None) + + +def test_access_secret_version_cloud_trace(mock_grpc_server, monkeypatch): + """Verifies that access_secret_version_cloud_trace initializes CloudTraceSpanExporter and runs.""" + port = mock_grpc_server + + orig_init = SecretManagerServiceGrpcTransport.__init__ + + def patched_init(self, *args, **kwargs): + kwargs["channel"] = grpc.insecure_channel(f"127.0.0.1:{port}") + orig_init(self, *args, **kwargs) + + monkeypatch.setattr(SecretManagerServiceGrpcTransport, "__init__", patched_init) + + with mock.patch( + "secretmanager_trace_cloud_trace.CloudTraceSpanExporter" + ) as mock_cloud_exporter_cls: + instance = MockCloudTraceSpanExporter() + mock_cloud_exporter_cls.return_value = instance + + payload = access_secret_version_cloud_trace( + project_id="test-project", + secret_id="my-secret", + version_id="1", + ) + + assert payload == "mock-cloud-trace-secret" + mock_cloud_exporter_cls.assert_called_once() diff --git a/secretmanager/snippets/secretmanager_trace_test.py b/secretmanager/snippets/secretmanager_trace_test.py new file mode 100644 index 00000000000..6896648b2d0 --- /dev/null +++ b/secretmanager/snippets/secretmanager_trace_test.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +# 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. + +"""Unit tests for secretmanager_trace snippet.""" + +import concurrent.futures +import os +from unittest import mock + +import grpc +import pytest +from google.api_core.client_options import ClientOptions +from google.api_core.gapic_v1 import method +from google.cloud import secretmanager +from google.cloud.secretmanager_v1.services.secret_manager_service.transports.grpc import ( + SecretManagerServiceGrpcTransport, +) +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +from secretmanager_trace import access_secret_version_with_tracing + +# Ensure OTel tracing capabilities are enabled in the environment +os.environ["GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED"] = "true" +os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" + + +@pytest.fixture +def mock_grpc_server(): + """Starts an in-memory gRPC server returning mock responses.""" + server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) + port = server.add_insecure_port("127.0.0.1:0") + + mock_resp = secretmanager.AccessSecretVersionResponse( + name="projects/test-project/secrets/my-secret/versions/1", + payload=secretmanager.SecretPayload(data=b"mock-secret-token-123"), + ) + + generic_handler = grpc.method_handlers_generic_handler( + "google.cloud.secretmanager.v1.SecretManagerService", + { + "AccessSecretVersion": grpc.unary_unary_rpc_method_handler( + lambda r, c: secretmanager.AccessSecretVersionResponse.serialize( + mock_resp + ), + request_deserializer=lambda b: b, + response_serializer=lambda b: b, + ) + }, + ) + server.add_generic_rpc_handlers((generic_handler,)) + server.start() + try: + yield port + finally: + server.stop(grace=None) + + +@pytest.fixture +def otel_setup(): + """Configures an in-memory OpenTelemetry tracer provider and exporter.""" + provider = TracerProvider() + exporter = InMemorySpanExporter() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + return provider, exporter + + +def test_access_secret_version_with_tracing_spans(mock_grpc_server, otel_setup): + """Verifies that access_secret_version_with_tracing emits T3 and T4 spans.""" + port = mock_grpc_server + provider, exporter = otel_setup + + orig_init = SecretManagerServiceGrpcTransport.__init__ + + def patched_init(self, *args, **kwargs): + kwargs["channel"] = grpc.insecure_channel(f"127.0.0.1:{port}") + orig_init(self, *args, **kwargs) + # Ensure modern GAPIC tracing wrapper is attached to the transport callable + self._wrapped_methods[self.access_secret_version] = method.wrap_method( + self.access_secret_version, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion", + client_options=ClientOptions(tracer_provider=provider), + kind="grpc", + ) + + with mock.patch("secretmanager_trace.TracerProvider", return_value=provider): + with mock.patch.object( + SecretManagerServiceGrpcTransport, "__init__", patched_init + ): + payload = access_secret_version_with_tracing( + project_id="test-project", + secret_id="my-secret", + version_id="1", + ) + + # 1. Verify returned payload + assert payload == "mock-secret-token-123" + + # 2. Inspect emitted spans + spans = exporter.get_finished_spans() + assert len(spans) == 2, f"Expected 2 spans (T3 + T4), but got {len(spans)}" + + t3_spans = [s for s in spans if s.parent is None] + t4_spans = [s for s in spans if s.parent is not None] + + assert len(t3_spans) == 1, "Expected exactly 1 T3 (logical method) span" + assert len(t4_spans) == 1, "Expected exactly 1 T4 (wire attempt) span" + + t3_span = t3_spans[0] + t4_span = t4_spans[0] + + # 3. Verify T3 logical client span attributes + assert ( + t3_span.name + == "google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion" + ) + assert t3_span.attributes.get("rpc.system.name") == "grpc" + assert t3_span.attributes.get("rpc.response.status_code") == "OK" + + # 4. Verify T4 wire attempt span attributes + assert ( + t4_span.name + == "google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion" + ) + assert ( + t4_span.attributes.get("rpc.service") + == "google.cloud.secretmanager.v1.SecretManagerService" + ) + assert t4_span.attributes.get("rpc.grpc.status_code") == 0 + assert t4_span.attributes.get("rpc.response.status_code") == "OK" + + # 5. Verify span hierarchy / parent-child linkage + assert t4_span.parent.span_id == t3_span.context.span_id From 404f1910a561195e6cbae06d194036d791d8e116 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Wed, 14 Oct 2026 17:43:27 -0400 Subject: [PATCH 2/7] fix(secretmanager): align import order for flake8 and standardize tracing snippets --- secretmanager/snippets/conftest.py | 2 +- secretmanager/snippets/secretmanager_trace.py | 13 ++++++++++--- secretmanager/snippets/secretmanager_trace_async.py | 13 ++++++++++--- .../snippets/secretmanager_trace_async_test.py | 4 ++-- .../snippets/secretmanager_trace_client_options.py | 8 +++++--- .../secretmanager_trace_client_options_test.py | 4 ++-- .../snippets/secretmanager_trace_cloud_trace.py | 13 ++++++++++--- .../secretmanager_trace_cloud_trace_test.py | 4 ++-- secretmanager/snippets/secretmanager_trace_test.py | 4 ++-- 9 files changed, 44 insertions(+), 21 deletions(-) diff --git a/secretmanager/snippets/conftest.py b/secretmanager/snippets/conftest.py index c1ad6634ad2..2d7fe643b56 100644 --- a/secretmanager/snippets/conftest.py +++ b/secretmanager/snippets/conftest.py @@ -15,8 +15,8 @@ """Shared test fixtures for Secret Manager tracing snippets.""" -import pytest from opentelemetry import trace +import pytest @pytest.fixture(autouse=True) diff --git a/secretmanager/snippets/secretmanager_trace.py b/secretmanager/snippets/secretmanager_trace.py index 4be691547ee..3fb8c24d5c8 100644 --- a/secretmanager/snippets/secretmanager_trace.py +++ b/secretmanager/snippets/secretmanager_trace.py @@ -39,6 +39,11 @@ def access_secret_version_with_tracing( Returns: The secret payload decoded as a UTF-8 string. """ + # NOTE: In standalone scripts or CLI tools, initialize and shut down the + # TracerProvider within the script workflow. In long-running production + # applications (e.g., web servers), configure the TracerProvider once at + # application startup. + # # 1. Initialize the OpenTelemetry TracerProvider with a Console exporter provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) @@ -71,9 +76,11 @@ def access_secret_version_with_tracing( parser = argparse.ArgumentParser( description="Access a Secret Manager secret version with OpenTelemetry tracing." ) - parser.add_argument("--project-id", required=True, help="Google Cloud Project ID") - parser.add_argument("--secret-id", required=True, help="Secret ID") - parser.add_argument("--version-id", default="latest", help="Secret version ID") + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to access") + parser.add_argument( + "version_id", default="latest", nargs="?", help="version to access" + ) args = parser.parse_args() secret_data = access_secret_version_with_tracing( diff --git a/secretmanager/snippets/secretmanager_trace_async.py b/secretmanager/snippets/secretmanager_trace_async.py index e3ec0e795d5..b7f13ff80db 100644 --- a/secretmanager/snippets/secretmanager_trace_async.py +++ b/secretmanager/snippets/secretmanager_trace_async.py @@ -40,6 +40,11 @@ async def access_secret_version_with_tracing_async( Returns: The secret payload decoded as a UTF-8 string. """ + # NOTE: In standalone scripts or CLI tools, initialize and shut down the + # TracerProvider within the script workflow. In long-running production + # applications (e.g., web servers), configure the TracerProvider once at + # application startup. + # # 1. Initialize the OpenTelemetry TracerProvider with a Console exporter provider = TracerProvider() provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) @@ -72,9 +77,11 @@ async def access_secret_version_with_tracing_async( parser = argparse.ArgumentParser( description="Access a Secret Manager secret version asynchronously with OpenTelemetry tracing." ) - parser.add_argument("--project-id", required=True, help="Google Cloud Project ID") - parser.add_argument("--secret-id", required=True, help="Secret ID") - parser.add_argument("--version-id", default="latest", help="Secret version ID") + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to access") + parser.add_argument( + "version_id", default="latest", nargs="?", help="version to access" + ) args = parser.parse_args() secret_data = asyncio.run( diff --git a/secretmanager/snippets/secretmanager_trace_async_test.py b/secretmanager/snippets/secretmanager_trace_async_test.py index d9deb43d5d8..fc1914dd6a8 100644 --- a/secretmanager/snippets/secretmanager_trace_async_test.py +++ b/secretmanager/snippets/secretmanager_trace_async_test.py @@ -19,8 +19,6 @@ import os from unittest import mock -import grpc -import pytest from google.api_core import _observability from google.api_core.client_options import ClientOptions from google.api_core.gapic_v1 import method_async @@ -28,11 +26,13 @@ from google.cloud.secretmanager_v1.services.secret_manager_service.transports.grpc_asyncio import ( SecretManagerServiceGrpcAsyncIOTransport, ) +import grpc from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) +import pytest from secretmanager_trace_async import ( access_secret_version_with_tracing_async, diff --git a/secretmanager/snippets/secretmanager_trace_client_options.py b/secretmanager/snippets/secretmanager_trace_client_options.py index d002e0c5ce0..cbe2c0adb99 100644 --- a/secretmanager/snippets/secretmanager_trace_client_options.py +++ b/secretmanager/snippets/secretmanager_trace_client_options.py @@ -68,9 +68,11 @@ def access_secret_version_with_client_options( parser = argparse.ArgumentParser( description="Access a Secret Manager secret version with ClientOptions-configured tracing." ) - parser.add_argument("--project-id", required=True, help="Google Cloud Project ID") - parser.add_argument("--secret-id", required=True, help="Secret ID") - parser.add_argument("--version-id", default="latest", help="Secret version ID") + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to access") + parser.add_argument( + "version_id", default="latest", nargs="?", help="version to access" + ) args = parser.parse_args() secret_data = access_secret_version_with_client_options( diff --git a/secretmanager/snippets/secretmanager_trace_client_options_test.py b/secretmanager/snippets/secretmanager_trace_client_options_test.py index d443ce7e58e..6b31ae2342c 100644 --- a/secretmanager/snippets/secretmanager_trace_client_options_test.py +++ b/secretmanager/snippets/secretmanager_trace_client_options_test.py @@ -19,18 +19,18 @@ import os from unittest import mock -import grpc -import pytest from google.api_core.gapic_v1 import method from google.cloud import secretmanager from google.cloud.secretmanager_v1.services.secret_manager_service.transports.grpc import ( SecretManagerServiceGrpcTransport, ) +import grpc from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) +import pytest from secretmanager_trace_client_options import access_secret_version_with_client_options diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace.py b/secretmanager/snippets/secretmanager_trace_cloud_trace.py index 601dc4dffb3..de8b3c9b79d 100644 --- a/secretmanager/snippets/secretmanager_trace_cloud_trace.py +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace.py @@ -40,6 +40,11 @@ def access_secret_version_cloud_trace( Returns: The secret payload decoded as a UTF-8 string. """ + # NOTE: In standalone scripts or CLI tools, initialize and shut down the + # TracerProvider within the script workflow. In long-running production + # applications (e.g., web servers), configure the TracerProvider once at + # application startup. + # # 1. Initialize OpenTelemetry TracerProvider with CloudTraceSpanExporter and BatchSpanProcessor provider = TracerProvider() cloud_exporter = CloudTraceSpanExporter() @@ -73,9 +78,11 @@ def access_secret_version_cloud_trace( parser = argparse.ArgumentParser( description="Access a Secret Manager secret version and export traces to Google Cloud Trace." ) - parser.add_argument("--project-id", required=True, help="Google Cloud Project ID") - parser.add_argument("--secret-id", required=True, help="Secret ID") - parser.add_argument("--version-id", default="latest", help="Secret version ID") + parser.add_argument("project_id", help="id of the GCP project") + parser.add_argument("secret_id", help="id of the secret to access") + parser.add_argument( + "version_id", default="latest", nargs="?", help="version to access" + ) args = parser.parse_args() secret_data = access_secret_version_cloud_trace( diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py index ff7d91d90b8..2e99b4f5c45 100644 --- a/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py @@ -20,13 +20,13 @@ import sys from unittest import mock -import grpc -import pytest from google.cloud import secretmanager from google.cloud.secretmanager_v1.services.secret_manager_service.transports.grpc import ( SecretManagerServiceGrpcTransport, ) +import grpc from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +import pytest # Mock opentelemetry.exporter.cloud_trace if not installed in the environment diff --git a/secretmanager/snippets/secretmanager_trace_test.py b/secretmanager/snippets/secretmanager_trace_test.py index 6896648b2d0..8bd05dee5d5 100644 --- a/secretmanager/snippets/secretmanager_trace_test.py +++ b/secretmanager/snippets/secretmanager_trace_test.py @@ -19,19 +19,19 @@ import os from unittest import mock -import grpc -import pytest from google.api_core.client_options import ClientOptions from google.api_core.gapic_v1 import method from google.cloud import secretmanager from google.cloud.secretmanager_v1.services.secret_manager_service.transports.grpc import ( SecretManagerServiceGrpcTransport, ) +import grpc from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, ) +import pytest from secretmanager_trace import access_secret_version_with_tracing From 2af3aca1cad8dc65e69967854aa7bfb5a53a1158 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 15 Oct 2026 04:28:59 -0400 Subject: [PATCH 3/7] fix(secretmanager): return and log version resource name to avoid cleartext secret logging --- secretmanager/snippets/secretmanager_trace.py | 9 ++++----- secretmanager/snippets/secretmanager_trace_async.py | 13 ++++++------- .../snippets/secretmanager_trace_async_test.py | 6 +++--- .../snippets/secretmanager_trace_client_options.py | 9 ++++----- .../secretmanager_trace_client_options_test.py | 4 ++-- .../snippets/secretmanager_trace_cloud_trace.py | 9 ++++----- .../secretmanager_trace_cloud_trace_test.py | 4 ++-- secretmanager/snippets/secretmanager_trace_test.py | 6 +++--- 8 files changed, 28 insertions(+), 32 deletions(-) diff --git a/secretmanager/snippets/secretmanager_trace.py b/secretmanager/snippets/secretmanager_trace.py index 3fb8c24d5c8..2d5b0f11489 100644 --- a/secretmanager/snippets/secretmanager_trace.py +++ b/secretmanager/snippets/secretmanager_trace.py @@ -37,7 +37,7 @@ def access_secret_version_with_tracing( version_id: ID of the secret version to access (defaults to "latest"). Returns: - The secret payload decoded as a UTF-8 string. + The resource name of the accessed secret version. """ # NOTE: In standalone scripts or CLI tools, initialize and shut down the # TracerProvider within the script workflow. In long-running production @@ -60,8 +60,7 @@ def access_secret_version_with_tracing( try: response = client.access_secret_version(request={"name": name}) - payload = response.payload.data.decode("UTF-8") - return payload + return response.name finally: # Flush buffered spans before program completion provider.shutdown() @@ -83,9 +82,9 @@ def access_secret_version_with_tracing( ) args = parser.parse_args() - secret_data = access_secret_version_with_tracing( + version_name = access_secret_version_with_tracing( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, ) - print(f"Retrieved secret payload: {secret_data}") + print(f"Successfully accessed secret version: {version_name}") diff --git a/secretmanager/snippets/secretmanager_trace_async.py b/secretmanager/snippets/secretmanager_trace_async.py index b7f13ff80db..98b29de005e 100644 --- a/secretmanager/snippets/secretmanager_trace_async.py +++ b/secretmanager/snippets/secretmanager_trace_async.py @@ -19,7 +19,7 @@ import asyncio import os -from google.cloud import secretmanager_v1 +from google.cloud import secretmanager from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor @@ -38,7 +38,7 @@ async def access_secret_version_with_tracing_async( version_id: ID of the secret version to access (defaults to "latest"). Returns: - The secret payload decoded as a UTF-8 string. + The resource name of the accessed secret version. """ # NOTE: In standalone scripts or CLI tools, initialize and shut down the # TracerProvider within the script workflow. In long-running production @@ -54,15 +54,14 @@ async def access_secret_version_with_tracing_async( os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" # 3. Instantiate the async client - client = secretmanager_v1.SecretManagerServiceAsyncClient() + client = secretmanager.SecretManagerServiceAsyncClient() # 4. Construct resource name and invoke the async RPC name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" try: response = await client.access_secret_version(request={"name": name}) - payload = response.payload.data.decode("UTF-8") - return payload + return response.name finally: # Flush buffered spans before program completion provider.shutdown() @@ -84,11 +83,11 @@ async def access_secret_version_with_tracing_async( ) args = parser.parse_args() - secret_data = asyncio.run( + version_name = asyncio.run( access_secret_version_with_tracing_async( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, ) ) - print(f"Retrieved secret payload asynchronously: {secret_data}") + print(f"Successfully accessed secret version: {version_name}") diff --git a/secretmanager/snippets/secretmanager_trace_async_test.py b/secretmanager/snippets/secretmanager_trace_async_test.py index fc1914dd6a8..204b260096c 100644 --- a/secretmanager/snippets/secretmanager_trace_async_test.py +++ b/secretmanager/snippets/secretmanager_trace_async_test.py @@ -112,14 +112,14 @@ def patched_init(self, *args, **kwargs): with mock.patch.object( SecretManagerServiceGrpcAsyncIOTransport, "__init__", patched_init ): - payload = await access_secret_version_with_tracing_async( + version_name = await access_secret_version_with_tracing_async( project_id="test-project", secret_id="my-secret", version_id="1", ) - # 1. Verify returned payload - assert payload == "mock-async-token-xyz" + # 1. Verify returned version resource name + assert version_name == "projects/test-project/secrets/my-secret/versions/1" # 2. Inspect emitted spans spans = exporter.get_finished_spans() diff --git a/secretmanager/snippets/secretmanager_trace_client_options.py b/secretmanager/snippets/secretmanager_trace_client_options.py index cbe2c0adb99..fb57616a607 100644 --- a/secretmanager/snippets/secretmanager_trace_client_options.py +++ b/secretmanager/snippets/secretmanager_trace_client_options.py @@ -35,7 +35,7 @@ def access_secret_version_with_client_options( version_id: ID of the secret version to access (defaults to "latest"). Returns: - The secret payload decoded as a UTF-8 string. + The resource name of the accessed secret version. """ # 1. Initialize an independent TracerProvider (no global registration required) provider = TracerProvider() @@ -52,8 +52,7 @@ def access_secret_version_with_client_options( try: response = client.access_secret_version(request={"name": name}) - payload = response.payload.data.decode("UTF-8") - return payload + return response.name finally: # Flush buffered spans before program completion provider.shutdown() @@ -75,9 +74,9 @@ def access_secret_version_with_client_options( ) args = parser.parse_args() - secret_data = access_secret_version_with_client_options( + version_name = access_secret_version_with_client_options( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, ) - print(f"Retrieved secret payload: {secret_data}") + print(f"Successfully accessed secret version: {version_name}") diff --git a/secretmanager/snippets/secretmanager_trace_client_options_test.py b/secretmanager/snippets/secretmanager_trace_client_options_test.py index 6b31ae2342c..21bd2cac828 100644 --- a/secretmanager/snippets/secretmanager_trace_client_options_test.py +++ b/secretmanager/snippets/secretmanager_trace_client_options_test.py @@ -95,13 +95,13 @@ def patched_init(self, *args, **kwargs): with mock.patch( "secretmanager_trace_client_options.TracerProvider", return_value=provider ): - payload = access_secret_version_with_client_options( + version_name = access_secret_version_with_client_options( project_id="test-project", secret_id="my-secret", version_id="1", ) - assert payload == "mock-client-options-token" + assert version_name == "projects/test-project/secrets/my-secret/versions/1" spans = exporter.get_finished_spans() assert len(spans) >= 1 diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace.py b/secretmanager/snippets/secretmanager_trace_cloud_trace.py index de8b3c9b79d..d615e9ad7c4 100644 --- a/secretmanager/snippets/secretmanager_trace_cloud_trace.py +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace.py @@ -38,7 +38,7 @@ def access_secret_version_cloud_trace( version_id: ID of the secret version to access (defaults to "latest"). Returns: - The secret payload decoded as a UTF-8 string. + The resource name of the accessed secret version. """ # NOTE: In standalone scripts or CLI tools, initialize and shut down the # TracerProvider within the script workflow. In long-running production @@ -62,8 +62,7 @@ def access_secret_version_cloud_trace( try: response = client.access_secret_version(request={"name": name}) - payload = response.payload.data.decode("UTF-8") - return payload + return response.name finally: # Flush buffered spans to Cloud Trace before exiting provider.shutdown() @@ -85,9 +84,9 @@ def access_secret_version_cloud_trace( ) args = parser.parse_args() - secret_data = access_secret_version_cloud_trace( + version_name = access_secret_version_cloud_trace( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, ) - print(f"Retrieved secret payload: {secret_data}") + print(f"Successfully accessed secret version: {version_name}") diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py index 2e99b4f5c45..c45859fa67e 100644 --- a/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py @@ -107,11 +107,11 @@ def patched_init(self, *args, **kwargs): instance = MockCloudTraceSpanExporter() mock_cloud_exporter_cls.return_value = instance - payload = access_secret_version_cloud_trace( + version_name = access_secret_version_cloud_trace( project_id="test-project", secret_id="my-secret", version_id="1", ) - assert payload == "mock-cloud-trace-secret" + assert version_name == "projects/test-project/secrets/my-secret/versions/1" mock_cloud_exporter_cls.assert_called_once() diff --git a/secretmanager/snippets/secretmanager_trace_test.py b/secretmanager/snippets/secretmanager_trace_test.py index 8bd05dee5d5..b1f35c7d042 100644 --- a/secretmanager/snippets/secretmanager_trace_test.py +++ b/secretmanager/snippets/secretmanager_trace_test.py @@ -102,14 +102,14 @@ def patched_init(self, *args, **kwargs): with mock.patch.object( SecretManagerServiceGrpcTransport, "__init__", patched_init ): - payload = access_secret_version_with_tracing( + version_name = access_secret_version_with_tracing( project_id="test-project", secret_id="my-secret", version_id="1", ) - # 1. Verify returned payload - assert payload == "mock-secret-token-123" + # 1. Verify returned version resource name + assert version_name == "projects/test-project/secrets/my-secret/versions/1" # 2. Inspect emitted spans spans = exporter.get_finished_spans() From 29c70e788b48a3adf68726ac3fe5c922e5543fc3 Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 15 Oct 2026 04:31:31 -0400 Subject: [PATCH 4/7] fix(secretmanager): align __main__ execution pattern with access_secret_version snippet --- secretmanager/snippets/secretmanager_trace.py | 3 +-- secretmanager/snippets/secretmanager_trace_async.py | 3 +-- secretmanager/snippets/secretmanager_trace_client_options.py | 3 +-- secretmanager/snippets/secretmanager_trace_cloud_trace.py | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/secretmanager/snippets/secretmanager_trace.py b/secretmanager/snippets/secretmanager_trace.py index 2d5b0f11489..7e1690ecd57 100644 --- a/secretmanager/snippets/secretmanager_trace.py +++ b/secretmanager/snippets/secretmanager_trace.py @@ -82,9 +82,8 @@ def access_secret_version_with_tracing( ) args = parser.parse_args() - version_name = access_secret_version_with_tracing( + access_secret_version_with_tracing( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, ) - print(f"Successfully accessed secret version: {version_name}") diff --git a/secretmanager/snippets/secretmanager_trace_async.py b/secretmanager/snippets/secretmanager_trace_async.py index 98b29de005e..beda3a63d28 100644 --- a/secretmanager/snippets/secretmanager_trace_async.py +++ b/secretmanager/snippets/secretmanager_trace_async.py @@ -83,11 +83,10 @@ async def access_secret_version_with_tracing_async( ) args = parser.parse_args() - version_name = asyncio.run( + asyncio.run( access_secret_version_with_tracing_async( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, ) ) - print(f"Successfully accessed secret version: {version_name}") diff --git a/secretmanager/snippets/secretmanager_trace_client_options.py b/secretmanager/snippets/secretmanager_trace_client_options.py index fb57616a607..9871f271f05 100644 --- a/secretmanager/snippets/secretmanager_trace_client_options.py +++ b/secretmanager/snippets/secretmanager_trace_client_options.py @@ -74,9 +74,8 @@ def access_secret_version_with_client_options( ) args = parser.parse_args() - version_name = access_secret_version_with_client_options( + access_secret_version_with_client_options( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, ) - print(f"Successfully accessed secret version: {version_name}") diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace.py b/secretmanager/snippets/secretmanager_trace_cloud_trace.py index d615e9ad7c4..a271a364d84 100644 --- a/secretmanager/snippets/secretmanager_trace_cloud_trace.py +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace.py @@ -84,9 +84,8 @@ def access_secret_version_cloud_trace( ) args = parser.parse_args() - version_name = access_secret_version_cloud_trace( + access_secret_version_cloud_trace( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, ) - print(f"Successfully accessed secret version: {version_name}") From ae016d82cd7a8475cf42fc729849cba2bbe138ce Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 15 Oct 2026 05:53:47 -0400 Subject: [PATCH 5/7] feat(secretmanager): switch to get_secret_version and output trace spans as JSON --- secretmanager/snippets/secretmanager_trace.py | 29 ++++++++------ .../snippets/secretmanager_trace_async.py | 29 ++++++++------ .../secretmanager_trace_async_test.py | 33 ++++++++-------- .../secretmanager_trace_client_options.py | 29 ++++++++------ ...secretmanager_trace_client_options_test.py | 31 +++++++-------- .../secretmanager_trace_cloud_trace.py | 38 ++++++++++++------- .../secretmanager_trace_cloud_trace_test.py | 21 +++++----- .../snippets/secretmanager_trace_test.py | 33 ++++++++-------- 8 files changed, 133 insertions(+), 110 deletions(-) diff --git a/secretmanager/snippets/secretmanager_trace.py b/secretmanager/snippets/secretmanager_trace.py index 7e1690ecd57..d1949e0a7af 100644 --- a/secretmanager/snippets/secretmanager_trace.py +++ b/secretmanager/snippets/secretmanager_trace.py @@ -24,20 +24,20 @@ from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor -def access_secret_version_with_tracing( +def get_secret_version_with_tracing( project_id: str, secret_id: str, version_id: str = "latest", -) -> str: - """Access a secret version in Secret Manager with OpenTelemetry tracing enabled. +) -> secretmanager.SecretVersion: + """Get a secret version in Secret Manager with OpenTelemetry tracing enabled. Args: project_id: Google Cloud project ID. - secret_id: ID of the secret to access. - version_id: ID of the secret version to access (defaults to "latest"). + secret_id: ID of the secret. + version_id: ID of the secret version (defaults to "latest"). Returns: - The resource name of the accessed secret version. + The SecretVersion metadata object. """ # NOTE: In standalone scripts or CLI tools, initialize and shut down the # TracerProvider within the script workflow. In long-running production @@ -59,8 +59,13 @@ def access_secret_version_with_tracing( name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" try: - response = client.access_secret_version(request={"name": name}) - return response.name + print("--- OpenTelemetry Trace Spans (JSON) ---") + response = client.get_secret_version(request={"name": name}) + state_name = response.state.name + print( + f"\nAPI Result: Retrieved secret version {response.name} (state: {state_name})" + ) + return response finally: # Flush buffered spans before program completion provider.shutdown() @@ -73,16 +78,16 @@ def access_secret_version_with_tracing( import argparse parser = argparse.ArgumentParser( - description="Access a Secret Manager secret version with OpenTelemetry tracing." + description="Get a Secret Manager secret version with OpenTelemetry tracing." ) parser.add_argument("project_id", help="id of the GCP project") - parser.add_argument("secret_id", help="id of the secret to access") + parser.add_argument("secret_id", help="id of the secret") parser.add_argument( - "version_id", default="latest", nargs="?", help="version to access" + "version_id", default="latest", nargs="?", help="version to get" ) args = parser.parse_args() - access_secret_version_with_tracing( + get_secret_version_with_tracing( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, diff --git a/secretmanager/snippets/secretmanager_trace_async.py b/secretmanager/snippets/secretmanager_trace_async.py index beda3a63d28..0feb015c076 100644 --- a/secretmanager/snippets/secretmanager_trace_async.py +++ b/secretmanager/snippets/secretmanager_trace_async.py @@ -25,20 +25,20 @@ from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor -async def access_secret_version_with_tracing_async( +async def get_secret_version_with_tracing_async( project_id: str, secret_id: str, version_id: str = "latest", -) -> str: - """Access a secret version using SecretManagerServiceAsyncClient with OpenTelemetry tracing. +) -> secretmanager.SecretVersion: + """Get a secret version using SecretManagerServiceAsyncClient with OpenTelemetry tracing. Args: project_id: Google Cloud project ID. - secret_id: ID of the secret to access. - version_id: ID of the secret version to access (defaults to "latest"). + secret_id: ID of the secret. + version_id: ID of the secret version (defaults to "latest"). Returns: - The resource name of the accessed secret version. + The SecretVersion metadata object. """ # NOTE: In standalone scripts or CLI tools, initialize and shut down the # TracerProvider within the script workflow. In long-running production @@ -60,8 +60,13 @@ async def access_secret_version_with_tracing_async( name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" try: - response = await client.access_secret_version(request={"name": name}) - return response.name + print("--- OpenTelemetry Trace Spans (JSON) ---") + response = await client.get_secret_version(request={"name": name}) + state_name = response.state.name + print( + f"\nAPI Result: Retrieved secret version {response.name} (state: {state_name})" + ) + return response finally: # Flush buffered spans before program completion provider.shutdown() @@ -74,17 +79,17 @@ async def access_secret_version_with_tracing_async( import argparse parser = argparse.ArgumentParser( - description="Access a Secret Manager secret version asynchronously with OpenTelemetry tracing." + description="Get a Secret Manager secret version asynchronously with OpenTelemetry tracing." ) parser.add_argument("project_id", help="id of the GCP project") - parser.add_argument("secret_id", help="id of the secret to access") + parser.add_argument("secret_id", help="id of the secret") parser.add_argument( - "version_id", default="latest", nargs="?", help="version to access" + "version_id", default="latest", nargs="?", help="version to get" ) args = parser.parse_args() asyncio.run( - access_secret_version_with_tracing_async( + get_secret_version_with_tracing_async( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, diff --git a/secretmanager/snippets/secretmanager_trace_async_test.py b/secretmanager/snippets/secretmanager_trace_async_test.py index 204b260096c..7f13eb2bcb6 100644 --- a/secretmanager/snippets/secretmanager_trace_async_test.py +++ b/secretmanager/snippets/secretmanager_trace_async_test.py @@ -35,7 +35,7 @@ import pytest from secretmanager_trace_async import ( - access_secret_version_with_tracing_async, + get_secret_version_with_tracing_async, ) # Ensure OTel tracing capabilities are enabled in the environment @@ -49,18 +49,16 @@ def mock_grpc_server(): server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) port = server.add_insecure_port("127.0.0.1:0") - mock_resp = secretmanager.AccessSecretVersionResponse( + mock_resp = secretmanager.SecretVersion( name="projects/test-project/secrets/my-secret/versions/1", - payload=secretmanager.SecretPayload(data=b"mock-async-token-xyz"), + state=secretmanager.SecretVersion.State.ENABLED, ) generic_handler = grpc.method_handlers_generic_handler( "google.cloud.secretmanager.v1.SecretManagerService", { - "AccessSecretVersion": grpc.unary_unary_rpc_method_handler( - lambda r, c: secretmanager.AccessSecretVersionResponse.serialize( - mock_resp - ), + "GetSecretVersion": grpc.unary_unary_rpc_method_handler( + lambda r, c: secretmanager.SecretVersion.serialize(mock_resp), request_deserializer=lambda b: b, response_serializer=lambda b: b, ) @@ -84,10 +82,10 @@ def otel_setup(): @pytest.mark.asyncio -async def test_access_secret_version_with_tracing_async_spans( +async def test_get_secret_version_with_tracing_async_spans( mock_grpc_server, otel_setup ): - """Verifies that access_secret_version_with_tracing_async emits T3 and T4 spans.""" + """Verifies that get_secret_version_with_tracing_async emits T3 and T4 spans.""" port = mock_grpc_server provider, exporter = otel_setup @@ -101,9 +99,9 @@ def patched_init(self, *args, **kwargs): ) orig_init(self, *args, **kwargs) # Ensure modern GAPIC async tracing wrapper is attached - self._wrapped_methods[self.access_secret_version] = method_async.wrap_method( - self.access_secret_version, - method_name="/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion", + self._wrapped_methods[self.get_secret_version] = method_async.wrap_method( + self.get_secret_version, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/GetSecretVersion", client_options=opts, kind="grpc_asyncio", ) @@ -112,14 +110,15 @@ def patched_init(self, *args, **kwargs): with mock.patch.object( SecretManagerServiceGrpcAsyncIOTransport, "__init__", patched_init ): - version_name = await access_secret_version_with_tracing_async( + version = await get_secret_version_with_tracing_async( project_id="test-project", secret_id="my-secret", version_id="1", ) - # 1. Verify returned version resource name - assert version_name == "projects/test-project/secrets/my-secret/versions/1" + # 1. Verify returned SecretVersion metadata + assert version.name == "projects/test-project/secrets/my-secret/versions/1" + assert version.state == secretmanager.SecretVersion.State.ENABLED # 2. Inspect emitted spans spans = exporter.get_finished_spans() @@ -137,7 +136,7 @@ def patched_init(self, *args, **kwargs): # 3. Verify T3 logical client span attributes assert ( t3_span.name - == "google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion" + == "google.cloud.secretmanager.v1.SecretManagerService/GetSecretVersion" ) assert t3_span.attributes.get("rpc.system.name") == "grpc" assert t3_span.attributes.get("rpc.response.status_code") == "OK" @@ -145,7 +144,7 @@ def patched_init(self, *args, **kwargs): # 4. Verify T4 wire attempt span attributes assert ( t4_span.name - == "google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion" + == "google.cloud.secretmanager.v1.SecretManagerService/GetSecretVersion" ) assert ( t4_span.attributes.get("rpc.service") diff --git a/secretmanager/snippets/secretmanager_trace_client_options.py b/secretmanager/snippets/secretmanager_trace_client_options.py index 9871f271f05..683409f40d9 100644 --- a/secretmanager/snippets/secretmanager_trace_client_options.py +++ b/secretmanager/snippets/secretmanager_trace_client_options.py @@ -22,20 +22,20 @@ from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor -def access_secret_version_with_client_options( +def get_secret_version_with_client_options( project_id: str, secret_id: str, version_id: str = "latest", -) -> str: - """Access a secret version configuring tracing programmatically via ClientOptions. +) -> secretmanager.SecretVersion: + """Get a secret version configuring tracing programmatically via ClientOptions. Args: project_id: Google Cloud project ID. - secret_id: ID of the secret to access. - version_id: ID of the secret version to access (defaults to "latest"). + secret_id: ID of the secret. + version_id: ID of the secret version (defaults to "latest"). Returns: - The resource name of the accessed secret version. + The SecretVersion metadata object. """ # 1. Initialize an independent TracerProvider (no global registration required) provider = TracerProvider() @@ -51,8 +51,13 @@ def access_secret_version_with_client_options( name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" try: - response = client.access_secret_version(request={"name": name}) - return response.name + print("--- OpenTelemetry Trace Spans (JSON) ---") + response = client.get_secret_version(request={"name": name}) + state_name = response.state.name + print( + f"\nAPI Result: Retrieved secret version {response.name} (state: {state_name})" + ) + return response finally: # Flush buffered spans before program completion provider.shutdown() @@ -65,16 +70,16 @@ def access_secret_version_with_client_options( import argparse parser = argparse.ArgumentParser( - description="Access a Secret Manager secret version with ClientOptions-configured tracing." + description="Get a Secret Manager secret version with ClientOptions-configured tracing." ) parser.add_argument("project_id", help="id of the GCP project") - parser.add_argument("secret_id", help="id of the secret to access") + parser.add_argument("secret_id", help="id of the secret") parser.add_argument( - "version_id", default="latest", nargs="?", help="version to access" + "version_id", default="latest", nargs="?", help="version to get" ) args = parser.parse_args() - access_secret_version_with_client_options( + get_secret_version_with_client_options( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, diff --git a/secretmanager/snippets/secretmanager_trace_client_options_test.py b/secretmanager/snippets/secretmanager_trace_client_options_test.py index 21bd2cac828..2ba8066ba36 100644 --- a/secretmanager/snippets/secretmanager_trace_client_options_test.py +++ b/secretmanager/snippets/secretmanager_trace_client_options_test.py @@ -32,7 +32,9 @@ ) import pytest -from secretmanager_trace_client_options import access_secret_version_with_client_options +from secretmanager_trace_client_options import ( + get_secret_version_with_client_options, +) os.environ["GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED"] = "true" @@ -43,18 +45,16 @@ def mock_grpc_server(): server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) port = server.add_insecure_port("127.0.0.1:0") - mock_resp = secretmanager.AccessSecretVersionResponse( + mock_resp = secretmanager.SecretVersion( name="projects/test-project/secrets/my-secret/versions/1", - payload=secretmanager.SecretPayload(data=b"mock-client-options-token"), + state=secretmanager.SecretVersion.State.ENABLED, ) generic_handler = grpc.method_handlers_generic_handler( "google.cloud.secretmanager.v1.SecretManagerService", { - "AccessSecretVersion": grpc.unary_unary_rpc_method_handler( - lambda r, c: secretmanager.AccessSecretVersionResponse.serialize( - mock_resp - ), + "GetSecretVersion": grpc.unary_unary_rpc_method_handler( + lambda r, c: secretmanager.SecretVersion.serialize(mock_resp), request_deserializer=lambda b: b, response_serializer=lambda b: b, ) @@ -68,8 +68,8 @@ def mock_grpc_server(): server.stop(grace=None) -def test_access_secret_version_with_client_options(mock_grpc_server, monkeypatch): - """Verifies that access_secret_version_with_client_options emits spans via ClientOptions.""" +def test_get_secret_version_with_client_options(mock_grpc_server, monkeypatch): + """Verifies that get_secret_version_with_client_options emits spans via ClientOptions.""" port = mock_grpc_server provider = TracerProvider() exporter = InMemorySpanExporter() @@ -82,9 +82,9 @@ def patched_init(self, *args, **kwargs): client_opts = kwargs.get("client_options") orig_init(self, *args, **kwargs) if client_opts and client_opts.tracer_provider: - self._wrapped_methods[self.access_secret_version] = method.wrap_method( - self.access_secret_version, - method_name="/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion", + self._wrapped_methods[self.get_secret_version] = method.wrap_method( + self.get_secret_version, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/GetSecretVersion", client_options=client_opts, kind="grpc", ) @@ -95,16 +95,17 @@ def patched_init(self, *args, **kwargs): with mock.patch( "secretmanager_trace_client_options.TracerProvider", return_value=provider ): - version_name = access_secret_version_with_client_options( + version = get_secret_version_with_client_options( project_id="test-project", secret_id="my-secret", version_id="1", ) - assert version_name == "projects/test-project/secrets/my-secret/versions/1" + assert version.name == "projects/test-project/secrets/my-secret/versions/1" + assert version.state == secretmanager.SecretVersion.State.ENABLED spans = exporter.get_finished_spans() assert len(spans) >= 1 t3_span = spans[-1] - assert "AccessSecretVersion" in t3_span.name + assert "GetSecretVersion" in t3_span.name assert t3_span.status.is_ok diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace.py b/secretmanager/snippets/secretmanager_trace_cloud_trace.py index a271a364d84..d8bda75b033 100644 --- a/secretmanager/snippets/secretmanager_trace_cloud_trace.py +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace.py @@ -22,33 +22,38 @@ from opentelemetry import trace from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.sdk.trace.export import ( + BatchSpanProcessor, + ConsoleSpanExporter, + SimpleSpanProcessor, +) -def access_secret_version_cloud_trace( +def get_secret_version_cloud_trace( project_id: str, secret_id: str, version_id: str = "latest", -) -> str: - """Access a secret version in Secret Manager exporting traces to Google Cloud Trace. +) -> secretmanager.SecretVersion: + """Get a secret version in Secret Manager exporting traces to Google Cloud Trace. Args: project_id: Google Cloud project ID. - secret_id: ID of the secret to access. - version_id: ID of the secret version to access (defaults to "latest"). + secret_id: ID of the secret. + version_id: ID of the secret version (defaults to "latest"). Returns: - The resource name of the accessed secret version. + The SecretVersion metadata object. """ # NOTE: In standalone scripts or CLI tools, initialize and shut down the # TracerProvider within the script workflow. In long-running production # applications (e.g., web servers), configure the TracerProvider once at # application startup. # - # 1. Initialize OpenTelemetry TracerProvider with CloudTraceSpanExporter and BatchSpanProcessor + # 1. Initialize OpenTelemetry TracerProvider with CloudTraceSpanExporter and ConsoleSpanExporter provider = TracerProvider() cloud_exporter = CloudTraceSpanExporter() provider.add_span_processor(BatchSpanProcessor(cloud_exporter)) + provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) trace.set_tracer_provider(provider) # 2. Enable Google Cloud client library tracing @@ -61,8 +66,13 @@ def access_secret_version_cloud_trace( name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}" try: - response = client.access_secret_version(request={"name": name}) - return response.name + print("--- OpenTelemetry Trace Spans (JSON) ---") + response = client.get_secret_version(request={"name": name}) + state_name = response.state.name + print( + f"\nAPI Result: Retrieved secret version {response.name} (state: {state_name})" + ) + return response finally: # Flush buffered spans to Cloud Trace before exiting provider.shutdown() @@ -75,16 +85,16 @@ def access_secret_version_cloud_trace( import argparse parser = argparse.ArgumentParser( - description="Access a Secret Manager secret version and export traces to Google Cloud Trace." + description="Get a Secret Manager secret version and export traces to Google Cloud Trace." ) parser.add_argument("project_id", help="id of the GCP project") - parser.add_argument("secret_id", help="id of the secret to access") + parser.add_argument("secret_id", help="id of the secret") parser.add_argument( - "version_id", default="latest", nargs="?", help="version to access" + "version_id", default="latest", nargs="?", help="version to get" ) args = parser.parse_args() - access_secret_version_cloud_trace( + get_secret_version_cloud_trace( project_id=args.project_id, secret_id=args.secret_id, version_id=args.version_id, diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py index c45859fa67e..d0e18d1514e 100644 --- a/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py @@ -51,7 +51,7 @@ def shutdown(self): # CloudTraceSpanExporter before importing the snippet module in environments # where opentelemetry-exporter-gcp-trace is not installed. from secretmanager_trace_cloud_trace import ( # noqa: E402 - access_secret_version_cloud_trace, + get_secret_version_cloud_trace, ) os.environ["GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED"] = "true" @@ -64,18 +64,16 @@ def mock_grpc_server(): server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) port = server.add_insecure_port("127.0.0.1:0") - mock_resp = secretmanager.AccessSecretVersionResponse( + mock_resp = secretmanager.SecretVersion( name="projects/test-project/secrets/my-secret/versions/1", - payload=secretmanager.SecretPayload(data=b"mock-cloud-trace-secret"), + state=secretmanager.SecretVersion.State.ENABLED, ) generic_handler = grpc.method_handlers_generic_handler( "google.cloud.secretmanager.v1.SecretManagerService", { - "AccessSecretVersion": grpc.unary_unary_rpc_method_handler( - lambda r, c: secretmanager.AccessSecretVersionResponse.serialize( - mock_resp - ), + "GetSecretVersion": grpc.unary_unary_rpc_method_handler( + lambda r, c: secretmanager.SecretVersion.serialize(mock_resp), request_deserializer=lambda b: b, response_serializer=lambda b: b, ) @@ -89,8 +87,8 @@ def mock_grpc_server(): server.stop(grace=None) -def test_access_secret_version_cloud_trace(mock_grpc_server, monkeypatch): - """Verifies that access_secret_version_cloud_trace initializes CloudTraceSpanExporter and runs.""" +def test_get_secret_version_cloud_trace(mock_grpc_server, monkeypatch): + """Verifies that get_secret_version_cloud_trace initializes CloudTraceSpanExporter and runs.""" port = mock_grpc_server orig_init = SecretManagerServiceGrpcTransport.__init__ @@ -107,11 +105,12 @@ def patched_init(self, *args, **kwargs): instance = MockCloudTraceSpanExporter() mock_cloud_exporter_cls.return_value = instance - version_name = access_secret_version_cloud_trace( + version = get_secret_version_cloud_trace( project_id="test-project", secret_id="my-secret", version_id="1", ) - assert version_name == "projects/test-project/secrets/my-secret/versions/1" + assert version.name == "projects/test-project/secrets/my-secret/versions/1" + assert version.state == secretmanager.SecretVersion.State.ENABLED mock_cloud_exporter_cls.assert_called_once() diff --git a/secretmanager/snippets/secretmanager_trace_test.py b/secretmanager/snippets/secretmanager_trace_test.py index b1f35c7d042..79b14143167 100644 --- a/secretmanager/snippets/secretmanager_trace_test.py +++ b/secretmanager/snippets/secretmanager_trace_test.py @@ -33,7 +33,7 @@ ) import pytest -from secretmanager_trace import access_secret_version_with_tracing +from secretmanager_trace import get_secret_version_with_tracing # Ensure OTel tracing capabilities are enabled in the environment os.environ["GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED"] = "true" @@ -46,18 +46,16 @@ def mock_grpc_server(): server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) port = server.add_insecure_port("127.0.0.1:0") - mock_resp = secretmanager.AccessSecretVersionResponse( + mock_resp = secretmanager.SecretVersion( name="projects/test-project/secrets/my-secret/versions/1", - payload=secretmanager.SecretPayload(data=b"mock-secret-token-123"), + state=secretmanager.SecretVersion.State.ENABLED, ) generic_handler = grpc.method_handlers_generic_handler( "google.cloud.secretmanager.v1.SecretManagerService", { - "AccessSecretVersion": grpc.unary_unary_rpc_method_handler( - lambda r, c: secretmanager.AccessSecretVersionResponse.serialize( - mock_resp - ), + "GetSecretVersion": grpc.unary_unary_rpc_method_handler( + lambda r, c: secretmanager.SecretVersion.serialize(mock_resp), request_deserializer=lambda b: b, response_serializer=lambda b: b, ) @@ -80,8 +78,8 @@ def otel_setup(): return provider, exporter -def test_access_secret_version_with_tracing_spans(mock_grpc_server, otel_setup): - """Verifies that access_secret_version_with_tracing emits T3 and T4 spans.""" +def test_get_secret_version_with_tracing_spans(mock_grpc_server, otel_setup): + """Verifies that get_secret_version_with_tracing emits T3 and T4 spans.""" port = mock_grpc_server provider, exporter = otel_setup @@ -91,9 +89,9 @@ def patched_init(self, *args, **kwargs): kwargs["channel"] = grpc.insecure_channel(f"127.0.0.1:{port}") orig_init(self, *args, **kwargs) # Ensure modern GAPIC tracing wrapper is attached to the transport callable - self._wrapped_methods[self.access_secret_version] = method.wrap_method( - self.access_secret_version, - method_name="/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion", + self._wrapped_methods[self.get_secret_version] = method.wrap_method( + self.get_secret_version, + method_name="/google.cloud.secretmanager.v1.SecretManagerService/GetSecretVersion", client_options=ClientOptions(tracer_provider=provider), kind="grpc", ) @@ -102,14 +100,15 @@ def patched_init(self, *args, **kwargs): with mock.patch.object( SecretManagerServiceGrpcTransport, "__init__", patched_init ): - version_name = access_secret_version_with_tracing( + version = get_secret_version_with_tracing( project_id="test-project", secret_id="my-secret", version_id="1", ) - # 1. Verify returned version resource name - assert version_name == "projects/test-project/secrets/my-secret/versions/1" + # 1. Verify returned SecretVersion metadata + assert version.name == "projects/test-project/secrets/my-secret/versions/1" + assert version.state == secretmanager.SecretVersion.State.ENABLED # 2. Inspect emitted spans spans = exporter.get_finished_spans() @@ -127,7 +126,7 @@ def patched_init(self, *args, **kwargs): # 3. Verify T3 logical client span attributes assert ( t3_span.name - == "google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion" + == "google.cloud.secretmanager.v1.SecretManagerService/GetSecretVersion" ) assert t3_span.attributes.get("rpc.system.name") == "grpc" assert t3_span.attributes.get("rpc.response.status_code") == "OK" @@ -135,7 +134,7 @@ def patched_init(self, *args, **kwargs): # 4. Verify T4 wire attempt span attributes assert ( t4_span.name - == "google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion" + == "google.cloud.secretmanager.v1.SecretManagerService/GetSecretVersion" ) assert ( t4_span.attributes.get("rpc.service") From 30a7b2c91f435cecaaf9042ab49385f08045bc5e Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Thu, 15 Oct 2026 06:42:40 -0400 Subject: [PATCH 6/7] test(secretmanager): consolidate shared mock grpc server and otel fixtures into conftest.py --- secretmanager/snippets/conftest.py | 47 +++++++++++++++++++ .../secretmanager_trace_async_test.py | 44 ----------------- ...secretmanager_trace_client_options_test.py | 44 ++--------------- .../secretmanager_trace_cloud_trace_test.py | 31 ------------ .../snippets/secretmanager_trace_test.py | 45 ------------------ 5 files changed, 51 insertions(+), 160 deletions(-) diff --git a/secretmanager/snippets/conftest.py b/secretmanager/snippets/conftest.py index 2d7fe643b56..abdb5af00f0 100644 --- a/secretmanager/snippets/conftest.py +++ b/secretmanager/snippets/conftest.py @@ -15,7 +15,16 @@ """Shared test fixtures for Secret Manager tracing snippets.""" +import concurrent.futures + +from google.cloud import secretmanager +import grpc 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 ( + InMemorySpanExporter, +) import pytest @@ -25,3 +34,41 @@ def reset_otel_global_tracer_provider(): yield trace._TRACER_PROVIDER_SET_ONCE._done = False trace._TRACER_PROVIDER = None + + +@pytest.fixture +def mock_grpc_server(): + """Starts an in-memory gRPC server returning mock responses.""" + server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) + port = server.add_insecure_port("127.0.0.1:0") + + mock_resp = secretmanager.SecretVersion( + name="projects/test-project/secrets/my-secret/versions/1", + state=secretmanager.SecretVersion.State.ENABLED, + ) + + generic_handler = grpc.method_handlers_generic_handler( + "google.cloud.secretmanager.v1.SecretManagerService", + { + "GetSecretVersion": grpc.unary_unary_rpc_method_handler( + lambda r, c: secretmanager.SecretVersion.serialize(mock_resp), + request_deserializer=lambda b: b, + response_serializer=lambda b: b, + ) + }, + ) + server.add_generic_rpc_handlers((generic_handler,)) + server.start() + try: + yield port + finally: + server.stop(grace=None) + + +@pytest.fixture +def otel_setup(): + """Configures an in-memory OpenTelemetry tracer provider and exporter.""" + provider = TracerProvider() + exporter = InMemorySpanExporter() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + return provider, exporter diff --git a/secretmanager/snippets/secretmanager_trace_async_test.py b/secretmanager/snippets/secretmanager_trace_async_test.py index 7f13eb2bcb6..a9755add009 100644 --- a/secretmanager/snippets/secretmanager_trace_async_test.py +++ b/secretmanager/snippets/secretmanager_trace_async_test.py @@ -15,7 +15,6 @@ """Unit tests for secretmanager_trace_async snippet.""" -import concurrent.futures import os from unittest import mock @@ -27,11 +26,6 @@ SecretManagerServiceGrpcAsyncIOTransport, ) import grpc -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) import pytest from secretmanager_trace_async import ( @@ -43,44 +37,6 @@ os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" -@pytest.fixture -def mock_grpc_server(): - """Starts an in-memory gRPC server returning mock responses.""" - server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) - port = server.add_insecure_port("127.0.0.1:0") - - mock_resp = secretmanager.SecretVersion( - name="projects/test-project/secrets/my-secret/versions/1", - state=secretmanager.SecretVersion.State.ENABLED, - ) - - generic_handler = grpc.method_handlers_generic_handler( - "google.cloud.secretmanager.v1.SecretManagerService", - { - "GetSecretVersion": grpc.unary_unary_rpc_method_handler( - lambda r, c: secretmanager.SecretVersion.serialize(mock_resp), - request_deserializer=lambda b: b, - response_serializer=lambda b: b, - ) - }, - ) - server.add_generic_rpc_handlers((generic_handler,)) - server.start() - try: - yield port - finally: - server.stop(grace=None) - - -@pytest.fixture -def otel_setup(): - """Configures an in-memory OpenTelemetry tracer provider and exporter.""" - provider = TracerProvider() - exporter = InMemorySpanExporter() - provider.add_span_processor(SimpleSpanProcessor(exporter)) - return provider, exporter - - @pytest.mark.asyncio async def test_get_secret_version_with_tracing_async_spans( mock_grpc_server, otel_setup diff --git a/secretmanager/snippets/secretmanager_trace_client_options_test.py b/secretmanager/snippets/secretmanager_trace_client_options_test.py index 2ba8066ba36..5e631286eae 100644 --- a/secretmanager/snippets/secretmanager_trace_client_options_test.py +++ b/secretmanager/snippets/secretmanager_trace_client_options_test.py @@ -15,7 +15,6 @@ """Unit tests for secretmanager_trace_client_options snippet.""" -import concurrent.futures import os from unittest import mock @@ -25,12 +24,6 @@ SecretManagerServiceGrpcTransport, ) import grpc -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) -import pytest from secretmanager_trace_client_options import ( get_secret_version_with_client_options, @@ -39,41 +32,12 @@ os.environ["GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED"] = "true" -@pytest.fixture -def mock_grpc_server(): - """Starts an in-memory gRPC server returning mock responses.""" - server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) - port = server.add_insecure_port("127.0.0.1:0") - - mock_resp = secretmanager.SecretVersion( - name="projects/test-project/secrets/my-secret/versions/1", - state=secretmanager.SecretVersion.State.ENABLED, - ) - - generic_handler = grpc.method_handlers_generic_handler( - "google.cloud.secretmanager.v1.SecretManagerService", - { - "GetSecretVersion": grpc.unary_unary_rpc_method_handler( - lambda r, c: secretmanager.SecretVersion.serialize(mock_resp), - request_deserializer=lambda b: b, - response_serializer=lambda b: b, - ) - }, - ) - server.add_generic_rpc_handlers((generic_handler,)) - server.start() - try: - yield port - finally: - server.stop(grace=None) - - -def test_get_secret_version_with_client_options(mock_grpc_server, monkeypatch): +def test_get_secret_version_with_client_options( + mock_grpc_server, otel_setup, monkeypatch +): """Verifies that get_secret_version_with_client_options emits spans via ClientOptions.""" port = mock_grpc_server - provider = TracerProvider() - exporter = InMemorySpanExporter() - provider.add_span_processor(SimpleSpanProcessor(exporter)) + provider, exporter = otel_setup orig_init = SecretManagerServiceGrpcTransport.__init__ diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py index d0e18d1514e..01690e3c147 100644 --- a/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace_test.py @@ -15,7 +15,6 @@ """Unit tests for secretmanager_trace_cloud_trace snippet.""" -import concurrent.futures import os import sys from unittest import mock @@ -26,7 +25,6 @@ ) import grpc from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult -import pytest # Mock opentelemetry.exporter.cloud_trace if not installed in the environment @@ -58,35 +56,6 @@ def shutdown(self): os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" -@pytest.fixture -def mock_grpc_server(): - """Starts an in-memory gRPC server returning mock responses.""" - server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) - port = server.add_insecure_port("127.0.0.1:0") - - mock_resp = secretmanager.SecretVersion( - name="projects/test-project/secrets/my-secret/versions/1", - state=secretmanager.SecretVersion.State.ENABLED, - ) - - generic_handler = grpc.method_handlers_generic_handler( - "google.cloud.secretmanager.v1.SecretManagerService", - { - "GetSecretVersion": grpc.unary_unary_rpc_method_handler( - lambda r, c: secretmanager.SecretVersion.serialize(mock_resp), - request_deserializer=lambda b: b, - response_serializer=lambda b: b, - ) - }, - ) - server.add_generic_rpc_handlers((generic_handler,)) - server.start() - try: - yield port - finally: - server.stop(grace=None) - - def test_get_secret_version_cloud_trace(mock_grpc_server, monkeypatch): """Verifies that get_secret_version_cloud_trace initializes CloudTraceSpanExporter and runs.""" port = mock_grpc_server diff --git a/secretmanager/snippets/secretmanager_trace_test.py b/secretmanager/snippets/secretmanager_trace_test.py index 79b14143167..0deedd3097c 100644 --- a/secretmanager/snippets/secretmanager_trace_test.py +++ b/secretmanager/snippets/secretmanager_trace_test.py @@ -15,7 +15,6 @@ """Unit tests for secretmanager_trace snippet.""" -import concurrent.futures import os from unittest import mock @@ -26,12 +25,6 @@ SecretManagerServiceGrpcTransport, ) import grpc -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, -) -import pytest from secretmanager_trace import get_secret_version_with_tracing @@ -40,44 +33,6 @@ os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true" -@pytest.fixture -def mock_grpc_server(): - """Starts an in-memory gRPC server returning mock responses.""" - server = grpc.server(concurrent.futures.ThreadPoolExecutor(max_workers=2)) - port = server.add_insecure_port("127.0.0.1:0") - - mock_resp = secretmanager.SecretVersion( - name="projects/test-project/secrets/my-secret/versions/1", - state=secretmanager.SecretVersion.State.ENABLED, - ) - - generic_handler = grpc.method_handlers_generic_handler( - "google.cloud.secretmanager.v1.SecretManagerService", - { - "GetSecretVersion": grpc.unary_unary_rpc_method_handler( - lambda r, c: secretmanager.SecretVersion.serialize(mock_resp), - request_deserializer=lambda b: b, - response_serializer=lambda b: b, - ) - }, - ) - server.add_generic_rpc_handlers((generic_handler,)) - server.start() - try: - yield port - finally: - server.stop(grace=None) - - -@pytest.fixture -def otel_setup(): - """Configures an in-memory OpenTelemetry tracer provider and exporter.""" - provider = TracerProvider() - exporter = InMemorySpanExporter() - provider.add_span_processor(SimpleSpanProcessor(exporter)) - return provider, exporter - - def test_get_secret_version_with_tracing_spans(mock_grpc_server, otel_setup): """Verifies that get_secret_version_with_tracing emits T3 and T4 spans.""" port = mock_grpc_server From 3bb42141b7aae6b76bf3181f9d149ecc3b5d352e Mon Sep 17 00:00:00 2001 From: chalmer lowe Date: Fri, 16 Oct 2026 10:18:58 -0400 Subject: [PATCH 7/7] docs(secretmanager): clarify dual exporter setup in cloud trace snippet --- secretmanager/snippets/secretmanager_trace_cloud_trace.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/secretmanager/snippets/secretmanager_trace_cloud_trace.py b/secretmanager/snippets/secretmanager_trace_cloud_trace.py index d8bda75b033..7e8e3635202 100644 --- a/secretmanager/snippets/secretmanager_trace_cloud_trace.py +++ b/secretmanager/snippets/secretmanager_trace_cloud_trace.py @@ -49,7 +49,8 @@ def get_secret_version_cloud_trace( # applications (e.g., web servers), configure the TracerProvider once at # application startup. # - # 1. Initialize OpenTelemetry TracerProvider with CloudTraceSpanExporter and ConsoleSpanExporter + # 1. Initialize OpenTelemetry TracerProvider with CloudTraceSpanExporter + # NOTE: We also initialize ConsoleSpanExporter to display results from this sample directly. provider = TracerProvider() cloud_exporter = CloudTraceSpanExporter() provider.add_span_processor(BatchSpanProcessor(cloud_exporter))