From 3ff423dfd35a7eab5b67220ea4d671146b7baef3 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Tue, 10 Sep 2024 18:06:21 -1000 Subject: [PATCH] tracing: update OpenTelemetry dependencies from 2021 to 2024 This change non-invasively introduces dependencies of opentelemetry bringing in the latest dependencies and modernizing them. While here also brought in modern span attributes: * otel.scope.name * otel.scope.version Also added a modernized example to produce traces as well with gRPC-instrumentation enabled, and updated the docs. Updates #1170 Fixes #1173 Built from PR #1172 --- docs/opentelemetry-tracing.rst | 32 +++++--- examples/grpc_instrumentation_enabled.py | 73 +++++++++++++++++++ examples/trace.py | 66 +++++++++++++++++ .../spanner_v1/_opentelemetry_tracing.py | 27 ++++++- setup.py | 7 +- testing/constraints-3.7.txt | 7 +- tests/unit/test__opentelemetry_tracing.py | 14 ++++ 7 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 examples/grpc_instrumentation_enabled.py create mode 100644 examples/trace.py diff --git a/docs/opentelemetry-tracing.rst b/docs/opentelemetry-tracing.rst index 9b3dea276f..cb9a2b1350 100644 --- a/docs/opentelemetry-tracing.rst +++ b/docs/opentelemetry-tracing.rst @@ -8,10 +8,8 @@ To take advantage of these traces, we first need to install OpenTelemetry: .. code-block:: sh - pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation - - # [Optional] Installs the cloud monitoring exporter, however you can use any exporter of your choice - pip install opentelemetry-exporter-google-cloud + pip install opentelemetry-api opentelemetry-sdk + pip install opentelemetry-exporter-gcp-trace We also need to tell OpenTelemetry which exporter to use. To export Spanner traces to `Cloud Tracing `_, add the following lines to your application: @@ -19,21 +17,37 @@ We also need to tell OpenTelemetry which exporter to use. To export Spanner trac from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.trace.sampling import ProbabilitySampler + from opentelemetry.sdk.trace.sampling import TraceIdRatioBased from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter - # BatchExportSpanProcessor exports spans to Cloud Trace + # BatchSpanProcessor exports spans to Cloud Trace # in a seperate thread to not block on the main thread - from opentelemetry.sdk.trace.export import BatchExportSpanProcessor + from opentelemetry.sdk.trace.export import BatchSpanProcessor # Create and export one trace every 1000 requests - sampler = ProbabilitySampler(1/1000) + sampler = TraceIdRatioBased(1/1000) # Use the default tracer provider trace.set_tracer_provider(TracerProvider(sampler=sampler)) trace.get_tracer_provider().add_span_processor( # Initialize the cloud tracing exporter - BatchExportSpanProcessor(CloudTraceSpanExporter()) + BatchSpanProcessor(CloudTraceSpanExporter()) ) + +To get more fine-grained traces from gRPC, you can enable the gRPC instrumentation by the following + +.. code-block:: sh + + pip install opentelemetry-instrumentation opentelemetry-instrumentation-grpc + +and then in your Python code, please add the following lines: + +.. code:: python + + from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient + grpc_client_instrumentor = GrpcInstrumentorClient() + grpc_client_instrumentor.instrument() + + Generated spanner traces should now be available on `Cloud Trace `_. Tracing is most effective when many libraries are instrumented to provide insight over the entire lifespan of a request. diff --git a/examples/grpc_instrumentation_enabled.py b/examples/grpc_instrumentation_enabled.py new file mode 100644 index 0000000000..c8bccd0a9d --- /dev/null +++ b/examples/grpc_instrumentation_enabled.py @@ -0,0 +1,73 @@ +# -*- 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', 'test-project') + + # Setup OpenTelemetry, trace and Cloud Trace exporter. + tracer_provider = TracerProvider(sampler=ALWAYS_ON) + trace_exporter = CloudTraceSpanExporter(project_id=project_id) + tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) + trace.set_tracer_provider(tracer_provider) + # Retrieve a tracer from the global tracer provider. + tracer = tracer_provider.get_tracer('MyApp') + + # Setup the Cloud Spanner Client. + spanner_client = spanner.Client(project_id) + + 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. + try: + data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMPx()') + for row in data: + print(row) + except Exception as e: + pass + + +if __name__ == '__main__': + main() diff --git a/examples/trace.py b/examples/trace.py new file mode 100644 index 0000000000..791b6cd20b --- /dev/null +++ b/examples/trace.py @@ -0,0 +1,66 @@ +# -*- 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 + + +def main(): + # Setup common variables that'll be used between Spanner and traces. + project_id = os.environ.get('SPANNER_PROJECT_ID', 'test-project') + + # Setup OpenTelemetry, trace and Cloud Trace exporter. + tracer_provider = TracerProvider(sampler=ALWAYS_ON) + trace_exporter = CloudTraceSpanExporter(project_id=project_id) + tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) + trace.set_tracer_provider(tracer_provider) + # Retrieve a tracer from the global tracer provider. + tracer = tracer_provider.get_tracer('MyApp') + + # Setup the Cloud Spanner Client. + spanner_client = spanner.Client(project_id) + 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. + try: + data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMPx()') + for row in data: + print(row) + except Exception as e: + print(e) + + +if __name__ == '__main__': + main() diff --git a/google/cloud/spanner_v1/_opentelemetry_tracing.py b/google/cloud/spanner_v1/_opentelemetry_tracing.py index 8f9f8559ef..568d2f3a3d 100644 --- a/google/cloud/spanner_v1/_opentelemetry_tracing.py +++ b/google/cloud/spanner_v1/_opentelemetry_tracing.py @@ -18,15 +18,38 @@ from google.api_core.exceptions import GoogleAPICallError from google.cloud.spanner_v1 import SpannerClient +from google.cloud.spanner_v1 import gapic_version try: from opentelemetry import trace from opentelemetry.trace.status import Status, StatusCode + from opentelemetry.semconv.attributes.otel_attributes import ( + OTEL_SCOPE_NAME, + OTEL_SCOPE_VERSION, + ) HAS_OPENTELEMETRY_INSTALLED = True except ImportError: HAS_OPENTELEMETRY_INSTALLED = False +TRACER_NAME = 'cloud.google.com/python/spanner' +TRACER_VERSION = gapic_version.__version__ + + +def get_tracer(tracer_provider=None): + """ + get_tracer is a utility to unify and simplify retrieval of the tracer, without + leaking implementation details given that retrieving a tracer requires providing + the full qualified library name and version. + When the tracer_provider is set, it'll retrieve the tracer from it, otherwise + it'll fall back to the global tracer provider and use this library's specific semantics. + """ + if not tracer_provider: + # Acquire the global tracer provider. + tracer_provider = trace.get_tracer_provider() + + return tracer_provider.get_tracer(TRACER_NAME, TRACER_VERSION) + @contextmanager def trace_call(name, session, extra_attributes=None): @@ -35,7 +58,7 @@ def trace_call(name, session, extra_attributes=None): yield None return - tracer = trace.get_tracer(__name__) + tracer = get_tracer() # Set base attributes that we know for every trace created attributes = { @@ -43,6 +66,8 @@ def trace_call(name, session, extra_attributes=None): "db.url": SpannerClient.DEFAULT_ENDPOINT, "db.instance": session._database.name, "net.host.name": SpannerClient.DEFAULT_ENDPOINT, + OTEL_SCOPE_NAME: TRACER_NAME, + OTEL_SCOPE_VERSION: TRACER_VERSION, } if extra_attributes: diff --git a/setup.py b/setup.py index 98b1a61748..9efd0f3626 100644 --- a/setup.py +++ b/setup.py @@ -47,9 +47,10 @@ ] extras = { "tracing": [ - "opentelemetry-api >= 1.1.0", - "opentelemetry-sdk >= 1.1.0", - "opentelemetry-instrumentation >= 0.20b0, < 0.23dev", + "opentelemetry-api >= 1.24.0", + "opentelemetry-sdk >= 1.24.0", + "opentelemetry-instrumentation >= 0.46b0", + "opentelemetry-semantic-conventions >= 0.46b0", ], "libcst": "libcst >= 0.2.5", } diff --git a/testing/constraints-3.7.txt b/testing/constraints-3.7.txt index 20170203f5..ec9cd244b5 100644 --- a/testing/constraints-3.7.txt +++ b/testing/constraints-3.7.txt @@ -10,9 +10,10 @@ grpc-google-iam-v1==0.12.4 libcst==0.2.5 proto-plus==1.22.0 sqlparse==0.4.4 -opentelemetry-api==1.1.0 -opentelemetry-sdk==1.1.0 -opentelemetry-instrumentation==0.20b0 +opentelemetry-api>=1.24.0 +opentelemetry-sdk>=1.24.0 +opentelemetry-instrumentation==0.46b0 +opentelemetry-semantic-conventions==0.46b0 protobuf==3.20.2 deprecated==1.2.14 grpc-interceptor==0.15.4 diff --git a/tests/unit/test__opentelemetry_tracing.py b/tests/unit/test__opentelemetry_tracing.py index 25870227bf..e7b672b7ec 100644 --- a/tests/unit/test__opentelemetry_tracing.py +++ b/tests/unit/test__opentelemetry_tracing.py @@ -6,14 +6,20 @@ try: from opentelemetry import trace as trace_api from opentelemetry.trace.status import StatusCode + from opentelemetry.semconv.attributes import ( + OTEL_SCOPE_NAME, + OTEL_SCOPE_VERSION, + ) except ImportError: pass from google.api_core.exceptions import GoogleAPICallError from google.cloud.spanner_v1 import _opentelemetry_tracing +from google.cloud.spanner_v1 import gapic_version from tests._helpers import OpenTelemetryBase, HAS_OPENTELEMETRY_INSTALLED +LIB_VERSION = gapic_version.__version__ def _make_rpc_error(error_cls, trailing_metadata=None): import grpc @@ -59,6 +65,8 @@ def test_trace_call(self): "db.type": "spanner", "db.url": "spanner.googleapis.com", "net.host.name": "spanner.googleapis.com", + OTEL_SCOPE_NAME: "cloud.google.com/python/spanner", + OTEL_SCOPE_VERSION: LIB_VERSION, } expected_attributes.update(extra_attributes) @@ -84,6 +92,8 @@ def test_trace_error(self): "db.type": "spanner", "db.url": "spanner.googleapis.com", "net.host.name": "spanner.googleapis.com", + OTEL_SCOPE_NAME: "cloud.google.com/python/spanner", + OTEL_SCOPE_VERSION: LIB_VERSION, } expected_attributes.update(extra_attributes) @@ -110,6 +120,8 @@ def test_trace_grpc_error(self): "db.type": "spanner", "db.url": "spanner.googleapis.com:443", "net.host.name": "spanner.googleapis.com:443", + OTEL_SCOPE_NAME: "cloud.google.com/python/spanner", + OTEL_SCOPE_VERSION: LIB_VERSION, } expected_attributes.update(extra_attributes) @@ -133,6 +145,8 @@ def test_trace_codeless_error(self): "db.type": "spanner", "db.url": "spanner.googleapis.com:443", "net.host.name": "spanner.googleapis.com:443", + OTEL_SCOPE_NAME: "cloud.google.com/python/spanner", + OTEL_SCOPE_VERSION: LIB_VERSION, } expected_attributes.update(extra_attributes)