From 90e36eda82b693a0487c8c84c83003acb5728bc9 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Sun, 25 Aug 2024 02:32:37 +0300 Subject: [PATCH] tracing: properly record the spanner endpoint and show how to enable gRPC --- docs/opentelemetry-tracing.rst | 8 ++- examples/grpc_instrumentation_enabled.py | 71 +++++++++++++++++++ examples/trace.py | 11 +-- .../spanner_v1/_opentelemetry_tracing.py | 15 ++-- google/cloud/spanner_v1/snapshot.py | 4 +- setup.py | 4 +- testing/constraints-3.7.txt | 4 +- tests/_helpers.py | 2 + 8 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 examples/grpc_instrumentation_enabled.py diff --git a/docs/opentelemetry-tracing.rst b/docs/opentelemetry-tracing.rst index 4628896904..de713392ad 100644 --- a/docs/opentelemetry-tracing.rst +++ b/docs/opentelemetry-tracing.rst @@ -83,4 +83,10 @@ Tracing is most effective when many libraries are instrumented to provide insigh For a list of libraries that can be instrumented, see the `OpenTelemetry Integrations` section of the `OpenTelemetry Python docs `_ To allow for SQL statements to be annotated in your spans, please set -the environment variable `SPANNER_ENABLE_EXTENDED_TRACING=true` or please set the configuration field `enable_extended_tracing` to `True` when configuring the Cloud Spanner client. +the environment variable `SPANNER_ENABLE_EXTENDED_TRACING=true` or please set the configuration field `enable_extended_tracing` to `True` when configuring the Cloud Spanner client, like this: + +.. code:: python + + tracerProvider = TracerProvider(sampler=sampler) + opts = dict(tracer_provider=tracerProvider, enable_extended_tracing=true) + spanner_client = spanner.Client(project_id, observability_options=opts) diff --git a/examples/grpc_instrumentation_enabled.py b/examples/grpc_instrumentation_enabled.py new file mode 100644 index 0000000000..98a16cee69 --- /dev/null +++ b/examples/grpc_instrumentation_enabled.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 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 + +import os +import time + +import google.cloud.spanner as spanner +from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.sdk.trace.sampling import ALWAYS_ON +from opentelemetry import trace + +# Enable the gRPC instrumentation if you'd like more introspection. +from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient +grpc_client_instrumentor = GrpcInstrumentorClient() +grpc_client_instrumentor.instrument() + +def main(): + # Setup common variables that'll be used between Spanner and traces. + project_id = os.environ.get('SPANNER_PROJECT_ID') + + # Setup OpenTelemetry, trace and Cloud Trace exporter. + sampler = ALWAYS_ON + tracerProvider = TracerProvider(sampler=sampler) + traceExporter = CloudTraceSpanExporter(project_id) + tracerProvider.add_span_processor( + BatchSpanProcessor(traceExporter)) + trace.set_tracer_provider(tracerProvider) + # Retrieve the set shared tracer. + tracer = tracerProvider.get_tracer('cloud.google.com/python/spanner', spanner.__version__) + + # Setup the Cloud Spanner Client. + # Here we directly pass in the tracerProvider into the spanner client. + opts = dict(tracer_provider=tracerProvider) + spanner_client = spanner.Client(project_id, observability_options=opts) + + instance = spanner_client.instance('test-instance') + database = instance.database('test-db') + + # Now run our queries + with tracer.start_as_current_span('QueryInformationSchema'): + with database.snapshot() as snapshot: + with tracer.start_as_current_span('InformationSchema'): + info_schema = snapshot.execute_sql( + 'SELECT * FROM INFORMATION_SCHEMA.TABLES') + for row in info_schema: + print(row) + + with tracer.start_as_current_span('ServerTimeQuery'): + with database.snapshot() as snapshot: + # Purposefully issue a bad SQL statement to examine exceptions + # that get recorded and a ERROR span status. + data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMP()') + for row in data: + print(row) + +if __name__ == '__main__': + main() diff --git a/examples/trace.py b/examples/trace.py index 76eb47cbea..d0611c8272 100644 --- a/examples/trace.py +++ b/examples/trace.py @@ -23,14 +23,9 @@ from opentelemetry.sdk.trace.sampling import ALWAYS_ON from opentelemetry import trace -# Enable the gRPC instrumentation if you'd like more introspection. -from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient -grpc_client_instrumentor = GrpcInstrumentorClient() -grpc_client_instrumentor.instrument() - def main(): # Setup common variables that'll be used between Spanner and traces. - project_id = os.environ.get('XSPANNER_PROJECT_ID', 'basic-advantage-416211') + project_id = os.environ.get('SPANNER_PROJECT_ID') # Setup OpenTelemetry, trace and Cloud Trace exporter. sampler = ALWAYS_ON @@ -49,8 +44,8 @@ def main(): opts = dict(tracer_provider=tracerProvider) spanner_client = spanner.Client(project_id, observability_options=opts) - instance = spanner_client.instance('afropay-spanner') - database = instance.database('afropay-prod') + instance = spanner_client.instance('test-instance') + database = instance.database('test-db') # Now run our queries with tracer.start_as_current_span('QueryInformationSchema'): diff --git a/google/cloud/spanner_v1/_opentelemetry_tracing.py b/google/cloud/spanner_v1/_opentelemetry_tracing.py index c6b41d338c..944852cd59 100644 --- a/google/cloud/spanner_v1/_opentelemetry_tracing.py +++ b/google/cloud/spanner_v1/_opentelemetry_tracing.py @@ -37,7 +37,6 @@ EXTENDED_TRACING_ENABLED = os.environ.get('SPANNER_ENABLE_EXTENDED_TRACING', '') == 'true' - TRACER_NAME = 'cloud.google.com/python/spanner' def get_tracer(tracer_provider=None): @@ -69,12 +68,19 @@ def trace_call(name, session, extra_attributes=None, observability_options=None) if tracer is None: # Acquire the global tracer if none was provided. tracer = get_tracer() + # It is imperative that we properly record the Cloud Spanner + # endpoint tracks whether we are connecting to the emulator + # or to production. + spanner_endpoint = os.getenv("SPANNER_EMULATOR_HOST") + if not spanner_endpoint: + spanner_endpoint = SpannerClient.DEFAULT_ENDPOINT + # Set base attributes that we know for every trace created attributes = { DB_SYSTEM: "spanner", - DB_CONNECTION_STRING: SpannerClient.DEFAULT_ENDPOINT, + DB_CONNECTION_STRING: spanner_endpoint, DB_NAME: session._database.name, - NET_HOST_NAME: SpannerClient.DEFAULT_ENDPOINT, + NET_HOST_NAME: spanner_endpoint, } if extra_attributes: @@ -87,9 +93,8 @@ def trace_call(name, session, extra_attributes=None, observability_options=None) if not extended_tracing: attributes.pop(DB_STATEMENT, None) - span_name = TRACER_NAME + '/' + name with tracer.start_as_current_span( - span_name, kind=trace.SpanKind.CLIENT, attributes=attributes + name, kind=trace.SpanKind.CLIENT, attributes=attributes ) as span: try: span.set_status(Status(StatusCode.OK)) diff --git a/google/cloud/spanner_v1/snapshot.py b/google/cloud/spanner_v1/snapshot.py index b9e7ce0150..1d4249e61b 100644 --- a/google/cloud/spanner_v1/snapshot.py +++ b/google/cloud/spanner_v1/snapshot.py @@ -310,7 +310,7 @@ def read( iterator = _restart_on_unavailable( restart, request, - "CloudSpanner.ReadOnlyTransaction", + "Snapshot.read", self._session, trace_attributes, transaction=self, @@ -326,7 +326,7 @@ def read( iterator = _restart_on_unavailable( restart, request, - "CloudSpanner.ReadOnlyTransaction", + "Snapshot.read", self._session, trace_attributes, transaction=self, diff --git a/setup.py b/setup.py index 48ee347ea7..9efd0f3626 100644 --- a/setup.py +++ b/setup.py @@ -47,8 +47,8 @@ ] extras = { "tracing": [ - "opentelemetry-api >= 1.25.0", - "opentelemetry-sdk >= 1.25.0", + "opentelemetry-api >= 1.24.0", + "opentelemetry-sdk >= 1.24.0", "opentelemetry-instrumentation >= 0.46b0", "opentelemetry-semantic-conventions >= 0.46b0", ], diff --git a/testing/constraints-3.7.txt b/testing/constraints-3.7.txt index c59c2f9ef1..ec9cd244b5 100644 --- a/testing/constraints-3.7.txt +++ b/testing/constraints-3.7.txt @@ -10,8 +10,8 @@ grpc-google-iam-v1==0.12.4 libcst==0.2.5 proto-plus==1.22.0 sqlparse==0.4.4 -opentelemetry-api==1.25.0 -opentelemetry-sdk==1.25.0 +opentelemetry-api>=1.24.0 +opentelemetry-sdk>=1.24.0 opentelemetry-instrumentation==0.46b0 opentelemetry-semantic-conventions==0.46b0 protobuf==3.20.2 diff --git a/tests/_helpers.py b/tests/_helpers.py index 685730022d..d77e0af572 100644 --- a/tests/_helpers.py +++ b/tests/_helpers.py @@ -21,6 +21,7 @@ DB_NAME = SpanAttributes.DB_NAME DB_CONNECTION_STRING = SpanAttributes.DB_CONNECTION_STRING NET_HOST_NAME = SpanAttributes.NET_HOST_NAME + DB_STATEMENT = SpanAttributes.DB_STATEMENT except ImportError: HAS_OPENTELEMETRY_INSTALLED = False @@ -30,6 +31,7 @@ DB_NAME = "db.name" DB_CONNECTION_STRING = "db.connection_string" NET_HOST_NAME = "net.host.name" + DB_STATEMENT = "db.statement" _TEST_OT_EXPORTER = None _TEST_OT_PROVIDER_INITIALIZED = False