Skip to content

Commit

Permalink
tracing: update OpenTelemetry dependencies from 2021 to 2024 (#1199)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
odeke-em authored Sep 18, 2024
1 parent 2415049 commit cb8a2b7
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 75 deletions.
32 changes: 23 additions & 9 deletions docs/opentelemetry-tracing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,46 @@ 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 <https://cloud.google.com/trace>`_, add the following lines to your application:

.. code:: python
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 <https://console.cloud.google.com/traces>`_.

Tracing is most effective when many libraries are instrumented to provide insight over the entire lifespan of a request.
Expand Down
73 changes: 73 additions & 0 deletions examples/grpc_instrumentation_enabled.py
Original file line number Diff line number Diff line change
@@ -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()
66 changes: 66 additions & 0 deletions examples/trace.py
Original file line number Diff line number Diff line change
@@ -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()
35 changes: 30 additions & 5 deletions google/cloud/spanner_v1/_opentelemetry_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,39 @@

from contextlib import contextmanager

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):
Expand All @@ -35,14 +57,16 @@ 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 = {
"db.type": "spanner",
"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:
Expand All @@ -52,9 +76,10 @@ def trace_call(name, session, extra_attributes=None):
name, kind=trace.SpanKind.CLIENT, attributes=attributes
) as span:
try:
span.set_status(Status(StatusCode.OK))
yield span
except GoogleAPICallError as error:
span.set_status(Status(StatusCode.ERROR))
except Exception as error:
span.set_status(Status(StatusCode.ERROR, str(error)))
span.record_exception(error)
raise
else:
span.set_status(Status(StatusCode.OK))
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@
]
extras = {
"tracing": [
"opentelemetry-api >= 1.1.0",
"opentelemetry-sdk >= 1.1.0",
"opentelemetry-instrumentation >= 0.20b0, < 0.23dev",
"opentelemetry-api >= 1.22.0",
"opentelemetry-sdk >= 1.22.0",
"opentelemetry-semantic-conventions >= 0.43b0",
],
"libcst": "libcst >= 0.2.5",
}
Expand Down
6 changes: 3 additions & 3 deletions testing/constraints-3.7.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ 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.22.0
opentelemetry-sdk==1.22.0
opentelemetry-semantic-conventions==0.43b0
protobuf==3.20.2
deprecated==1.2.14
grpc-interceptor==0.15.4
21 changes: 21 additions & 0 deletions tests/_helpers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import unittest
import mock

from google.cloud.spanner_v1 import gapic_version

LIB_VERSION = gapic_version.__version__

try:
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,
)
from opentelemetry.semconv.attributes.otel_attributes import (
OTEL_SCOPE_NAME,
OTEL_SCOPE_VERSION,
)

from opentelemetry.trace.status import StatusCode

trace.set_tracer_provider(TracerProvider())
Expand All @@ -30,6 +39,18 @@ def get_test_ot_exporter():
return _TEST_OT_EXPORTER


def enrich_with_otel_scope(attrs):
"""
This helper enriches attrs with OTEL_SCOPE_NAME and OTEL_SCOPE_VERSION
for the purpose of avoiding cumbersome duplicated imports.
"""
if HAS_OPENTELEMETRY_INSTALLED:
attrs[OTEL_SCOPE_NAME] = "cloud.google.com/python/spanner"
attrs[OTEL_SCOPE_VERSION] = LIB_VERSION

return attrs


def use_test_ot_exporter():
global _TEST_OT_PROVIDER_INITIALIZED

Expand Down
2 changes: 2 additions & 0 deletions tests/system/test_session_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@ def _make_attributes(db_instance, **kwargs):
"net.host.name": "spanner.googleapis.com",
"db.instance": db_instance,
}
ot_helpers.enrich_with_otel_scope(attributes)

attributes.update(kwargs)

return attributes
Expand Down
Loading

0 comments on commit cb8a2b7

Please sign in to comment.