Repository navigation
feat(secretmanager): add OpenTelemetry distributed tracing samples #14647
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
chalmerlowe
wants to merge
7
commits into
main
Choose a base branch
from
feat/secretmanager-otel-tracing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
69ce91b
feat(secretmanager): add OpenTelemetry distributed tracing samples
chalmerlowe 404f191
fix(secretmanager): align import order for flake8 and standardize tra…
chalmerlowe 2af3aca
fix(secretmanager): return and log version resource name to avoid cle…
chalmerlowe 29c70e7
fix(secretmanager): align __main__ execution pattern with access_secr…
chalmerlowe ae016d8
feat(secretmanager): switch to get_secret_version and output trace sp…
chalmerlowe 30a7b2c
test(secretmanager): consolidate shared mock grpc server and otel fix…
chalmerlowe 3bb4214
docs(secretmanager): clarify dual exporter setup in cloud trace snippet
chalmerlowe File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
|
|
||
|
|
||
| # [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, | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
|
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
113
secretmanager/snippets/secretmanager_trace_async_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| ) | ||
|
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 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.