Skip to content
27 changes: 27 additions & 0 deletions secretmanager/snippets/conftest.py
Original file line number Diff line number Diff line change
@@ -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."""

from opentelemetry import trace
import pytest


@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
3 changes: 2 additions & 1 deletion secretmanager/snippets/requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pytest==9.0.3
pytest==9.0.3
pytest-asyncio>=0.23.0
3 changes: 3 additions & 0 deletions secretmanager/snippets/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
90 changes: 90 additions & 0 deletions secretmanager/snippets/secretmanager_trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# -*- 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 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
# applications (e.g., web servers), configure the TracerProvider once at
# application startup.
#
# 1. Initialize the OpenTelemetry TracerProvider with a Console exporter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unnecessary numbering on the comments.

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})
return response.name
finally:
# Flush buffered spans before program completion
provider.shutdown()
Comment thread
chalmerlowe marked this conversation as resolved.


# [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", 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()
version_name = 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}")
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
93 changes: 93 additions & 0 deletions secretmanager/snippets/secretmanager_trace_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# -*- 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
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 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
# 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()))
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.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})
return response.name
finally:
# Flush buffered spans before program completion
provider.shutdown()
Comment thread
chalmerlowe marked this conversation as resolved.


# [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", 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()
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"Successfully accessed secret version: {version_name}")
158 changes: 158 additions & 0 deletions secretmanager/snippets/secretmanager_trace_async_test.py
Original file line number Diff line number Diff line change
@@ -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

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,
)
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,
)

# 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
):
version_name = await access_secret_version_with_tracing_async(
project_id="test-project",
secret_id="my-secret",
version_id="1",
)
Comment thread
chalmerlowe marked this conversation as resolved.

# 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()
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
Loading
Loading