Skip to content
74 changes: 74 additions & 0 deletions secretmanager/snippets/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- 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 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


@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


@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
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
94 changes: 94 additions & 0 deletions secretmanager/snippets/secretmanager_trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# -*- 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 get_secret_version_with_tracing(
project_id: str,
secret_id: str,
version_id: str = "latest",
) -> 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.
version_id: ID of the secret version (defaults to "latest").

Returns:
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 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:
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()
Comment thread
chalmerlowe marked this conversation as resolved.


# [END secretmanager_enable_tracing]


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(
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")
parser.add_argument(
"version_id", default="latest", nargs="?", help="version to get"
)

args = parser.parse_args()
get_secret_version_with_tracing(
project_id=args.project_id,
secret_id=args.secret_id,
version_id=args.version_id,
)
97 changes: 97 additions & 0 deletions secretmanager/snippets/secretmanager_trace_async.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# -*- 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 get_secret_version_with_tracing_async(
project_id: str,
secret_id: str,
version_id: str = "latest",
) -> secretmanager.SecretVersion:
"""Get a secret version using SecretManagerServiceAsyncClient with OpenTelemetry tracing.

Args:
project_id: Google Cloud project ID.
secret_id: ID of the secret.
version_id: ID of the secret version (defaults to "latest").

Returns:
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 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:
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()
Comment thread
chalmerlowe marked this conversation as resolved.


# [END secretmanager_enable_tracing_async]


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(
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")
parser.add_argument(
"version_id", default="latest", nargs="?", help="version to get"
)

args = parser.parse_args()
asyncio.run(
get_secret_version_with_tracing_async(
project_id=args.project_id,
secret_id=args.secret_id,
version_id=args.version_id,
)
)
113 changes: 113 additions & 0 deletions secretmanager/snippets/secretmanager_trace_async_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# -*- 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 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
import pytest

from secretmanager_trace_async import (
get_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.mark.asyncio
async def test_get_secret_version_with_tracing_async_spans(
mock_grpc_server, otel_setup
):
"""Verifies that get_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.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",
)

with mock.patch("secretmanager_trace_async.TracerProvider", return_value=provider):
with mock.patch.object(
SecretManagerServiceGrpcAsyncIOTransport, "__init__", patched_init
):
version = await get_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 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()
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/GetSecretVersion"
)
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/GetSecretVersion"
)
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