Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
91 changes: 91 additions & 0 deletions secretmanager/snippets/secretmanager_trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# -*- 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.
"""
# 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:
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()
Comment on lines +47 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Initializing and shutting down the global TracerProvider inside a reusable utility function like access_secret_version_with_tracing is a major anti-pattern and can cause bugs in production.

  1. Global Side Effects: Calling trace.set_tracer_provider(provider) inside the function will log warnings or fail on subsequent calls because the global tracer provider can only be set once.
  2. Premature Shutdown: Calling provider.shutdown() in the finally block will shut down the global tracer provider. Any subsequent client calls or other parts of the application trying to use the global tracer provider will fail to emit spans.

Recommendation:
Move the OpenTelemetry configuration (TracerProvider initialization, global registration, and shutdown) to the application's entry point (the __main__ block). This keeps the utility function clean, reusable, and free of global side effects.

Suggested change
# 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()
# Enable Google Cloud client library tracing
os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true"
# Instantiate the client (automatically discovers the active TracerProvider)
client = secretmanager.SecretManagerServiceClient()
# Construct the resource name and invoke the RPC
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"
response = client.access_secret_version(request={"name": name})
payload = response.payload.data.decode("UTF-8")
return payload



# [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()
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}")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
Comment on lines +73 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Update the __main__ block to initialize the global TracerProvider and ensure it is properly shut down when the program exits. Note that the --project-id argument should be optional, as the project can be inferred from Application Default Credentials (ADC) if not provided.

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description="Access a Secret Manager secret version with OpenTelemetry tracing."
    )
    parser.add_argument("--project-id", 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()

    # Initialize the OpenTelemetry TracerProvider with a Console exporter
    provider = TracerProvider()
    provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
    trace.set_tracer_provider(provider)

    try:
        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}")
    finally:
        # Flush buffered spans before program completion
        provider.shutdown()
References
  1. When using argparse for a command-line script, the --project_id argument can be optional, as the project can be inferred from Application Default Credentials (ADC) if not provided.

94 changes: 94 additions & 0 deletions secretmanager/snippets/secretmanager_trace_async.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 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.
"""
# 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_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()
Comment on lines +48 to +68

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Similar to the synchronous sample, initializing and shutting down the global TracerProvider inside access_secret_version_with_tracing_async introduces global side effects and prematurely shuts down tracing for the rest of the application.

Recommendation:
Move the OpenTelemetry configuration and shutdown to the __main__ block.

Suggested change
# 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()
# Enable Google Cloud client library tracing
os.environ["GOOGLE_SDK_PYTHON_TRACING_ENABLED"] = "true"
# Instantiate the async client
client = secretmanager_v1.SecretManagerServiceAsyncClient()
# Construct resource name and invoke the async RPC
name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"
response = await client.access_secret_version(request={"name": name})
payload = response.payload.data.decode("UTF-8")
return payload



# [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()
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}")

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

This expression logs
sensitive data (secret)
as clear text.
Comment on lines +74 to +94

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Update the __main__ block to initialize the global TracerProvider and ensure it is properly shut down when the program exits. Note that the --project-id argument should be optional, as the project can be inferred from Application Default Credentials (ADC) if not provided.

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="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()

    # Initialize the OpenTelemetry TracerProvider with a Console exporter
    provider = TracerProvider()
    provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
    trace.set_tracer_provider(provider)

    try:
        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}")
    finally:
        # Flush buffered spans before program completion
        provider.shutdown()
References
  1. When using argparse for a command-line script, the --project_id argument can be optional, as the project can be inferred from Application Default Credentials (ADC) if not provided.

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
):
payload = await access_secret_version_with_tracing_async(
project_id="test-project",
secret_id="my-secret",
version_id="1",
)
Comment on lines +111 to +119

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Since we removed the TracerProvider instantiation from the async utility function, we no longer need to mock it in the test. Instead, we can set the global tracer provider directly in the test setup.

Suggested change
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",
)
trace.set_tracer_provider(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
Loading
Loading