Skip to content

Commit cb8a2b7

Browse files
authored
tracing: update OpenTelemetry dependencies from 2021 to 2024 (#1199)
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
1 parent 2415049 commit cb8a2b7

13 files changed

+285
-75
lines changed

docs/opentelemetry-tracing.rst

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,46 @@ To take advantage of these traces, we first need to install OpenTelemetry:
88

99
.. code-block:: sh
1010
11-
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation
12-
13-
# [Optional] Installs the cloud monitoring exporter, however you can use any exporter of your choice
14-
pip install opentelemetry-exporter-google-cloud
11+
pip install opentelemetry-api opentelemetry-sdk
12+
pip install opentelemetry-exporter-gcp-trace
1513
1614
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:
1715

1816
.. code:: python
1917
2018
from opentelemetry import trace
2119
from opentelemetry.sdk.trace import TracerProvider
22-
from opentelemetry.trace.sampling import ProbabilitySampler
20+
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
2321
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
24-
# BatchExportSpanProcessor exports spans to Cloud Trace
22+
# BatchSpanProcessor exports spans to Cloud Trace
2523
# in a seperate thread to not block on the main thread
26-
from opentelemetry.sdk.trace.export import BatchExportSpanProcessor
24+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
2725
2826
# Create and export one trace every 1000 requests
29-
sampler = ProbabilitySampler(1/1000)
27+
sampler = TraceIdRatioBased(1/1000)
3028
# Use the default tracer provider
3129
trace.set_tracer_provider(TracerProvider(sampler=sampler))
3230
trace.get_tracer_provider().add_span_processor(
3331
# Initialize the cloud tracing exporter
34-
BatchExportSpanProcessor(CloudTraceSpanExporter())
32+
BatchSpanProcessor(CloudTraceSpanExporter())
3533
)
3634
35+
36+
To get more fine-grained traces from gRPC, you can enable the gRPC instrumentation by the following
37+
38+
.. code-block:: sh
39+
40+
pip install opentelemetry-instrumentation opentelemetry-instrumentation-grpc
41+
42+
and then in your Python code, please add the following lines:
43+
44+
.. code:: python
45+
46+
from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient
47+
grpc_client_instrumentor = GrpcInstrumentorClient()
48+
grpc_client_instrumentor.instrument()
49+
50+
3751
Generated spanner traces should now be available on `Cloud Trace <https://console.cloud.google.com/traces>`_.
3852

3953
Tracing is most effective when many libraries are instrumented to provide insight over the entire lifespan of a request.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2024 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License
15+
16+
import os
17+
import time
18+
19+
import google.cloud.spanner as spanner
20+
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
21+
from opentelemetry.sdk.trace import TracerProvider
22+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
23+
from opentelemetry.sdk.trace.sampling import ALWAYS_ON
24+
from opentelemetry import trace
25+
26+
# Enable the gRPC instrumentation if you'd like more introspection.
27+
from opentelemetry.instrumentation.grpc import GrpcInstrumentorClient
28+
29+
grpc_client_instrumentor = GrpcInstrumentorClient()
30+
grpc_client_instrumentor.instrument()
31+
32+
33+
def main():
34+
# Setup common variables that'll be used between Spanner and traces.
35+
project_id = os.environ.get('SPANNER_PROJECT_ID', 'test-project')
36+
37+
# Setup OpenTelemetry, trace and Cloud Trace exporter.
38+
tracer_provider = TracerProvider(sampler=ALWAYS_ON)
39+
trace_exporter = CloudTraceSpanExporter(project_id=project_id)
40+
tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
41+
trace.set_tracer_provider(tracer_provider)
42+
# Retrieve a tracer from the global tracer provider.
43+
tracer = tracer_provider.get_tracer('MyApp')
44+
45+
# Setup the Cloud Spanner Client.
46+
spanner_client = spanner.Client(project_id)
47+
48+
instance = spanner_client.instance('test-instance')
49+
database = instance.database('test-db')
50+
51+
# Now run our queries
52+
with tracer.start_as_current_span('QueryInformationSchema'):
53+
with database.snapshot() as snapshot:
54+
with tracer.start_as_current_span('InformationSchema'):
55+
info_schema = snapshot.execute_sql(
56+
'SELECT * FROM INFORMATION_SCHEMA.TABLES')
57+
for row in info_schema:
58+
print(row)
59+
60+
with tracer.start_as_current_span('ServerTimeQuery'):
61+
with database.snapshot() as snapshot:
62+
# Purposefully issue a bad SQL statement to examine exceptions
63+
# that get recorded and a ERROR span status.
64+
try:
65+
data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMPx()')
66+
for row in data:
67+
print(row)
68+
except Exception as e:
69+
pass
70+
71+
72+
if __name__ == '__main__':
73+
main()

examples/trace.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2024 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License
15+
16+
import os
17+
import time
18+
19+
import google.cloud.spanner as spanner
20+
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
21+
from opentelemetry.sdk.trace import TracerProvider
22+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
23+
from opentelemetry.sdk.trace.sampling import ALWAYS_ON
24+
from opentelemetry import trace
25+
26+
27+
def main():
28+
# Setup common variables that'll be used between Spanner and traces.
29+
project_id = os.environ.get('SPANNER_PROJECT_ID', 'test-project')
30+
31+
# Setup OpenTelemetry, trace and Cloud Trace exporter.
32+
tracer_provider = TracerProvider(sampler=ALWAYS_ON)
33+
trace_exporter = CloudTraceSpanExporter(project_id=project_id)
34+
tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter))
35+
trace.set_tracer_provider(tracer_provider)
36+
# Retrieve a tracer from the global tracer provider.
37+
tracer = tracer_provider.get_tracer('MyApp')
38+
39+
# Setup the Cloud Spanner Client.
40+
spanner_client = spanner.Client(project_id)
41+
instance = spanner_client.instance('test-instance')
42+
database = instance.database('test-db')
43+
44+
# Now run our queries
45+
with tracer.start_as_current_span('QueryInformationSchema'):
46+
with database.snapshot() as snapshot:
47+
with tracer.start_as_current_span('InformationSchema'):
48+
info_schema = snapshot.execute_sql(
49+
'SELECT * FROM INFORMATION_SCHEMA.TABLES')
50+
for row in info_schema:
51+
print(row)
52+
53+
with tracer.start_as_current_span('ServerTimeQuery'):
54+
with database.snapshot() as snapshot:
55+
# Purposefully issue a bad SQL statement to examine exceptions
56+
# that get recorded and a ERROR span status.
57+
try:
58+
data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMPx()')
59+
for row in data:
60+
print(row)
61+
except Exception as e:
62+
print(e)
63+
64+
65+
if __name__ == '__main__':
66+
main()

google/cloud/spanner_v1/_opentelemetry_tracing.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,39 @@
1616

1717
from contextlib import contextmanager
1818

19-
from google.api_core.exceptions import GoogleAPICallError
2019
from google.cloud.spanner_v1 import SpannerClient
20+
from google.cloud.spanner_v1 import gapic_version
2121

2222
try:
2323
from opentelemetry import trace
2424
from opentelemetry.trace.status import Status, StatusCode
25+
from opentelemetry.semconv.attributes.otel_attributes import (
26+
OTEL_SCOPE_NAME,
27+
OTEL_SCOPE_VERSION,
28+
)
2529

2630
HAS_OPENTELEMETRY_INSTALLED = True
2731
except ImportError:
2832
HAS_OPENTELEMETRY_INSTALLED = False
2933

34+
TRACER_NAME = "cloud.google.com/python/spanner"
35+
TRACER_VERSION = gapic_version.__version__
36+
37+
38+
def get_tracer(tracer_provider=None):
39+
"""
40+
get_tracer is a utility to unify and simplify retrieval of the tracer, without
41+
leaking implementation details given that retrieving a tracer requires providing
42+
the full qualified library name and version.
43+
When the tracer_provider is set, it'll retrieve the tracer from it, otherwise
44+
it'll fall back to the global tracer provider and use this library's specific semantics.
45+
"""
46+
if not tracer_provider:
47+
# Acquire the global tracer provider.
48+
tracer_provider = trace.get_tracer_provider()
49+
50+
return tracer_provider.get_tracer(TRACER_NAME, TRACER_VERSION)
51+
3052

3153
@contextmanager
3254
def trace_call(name, session, extra_attributes=None):
@@ -35,14 +57,16 @@ def trace_call(name, session, extra_attributes=None):
3557
yield None
3658
return
3759

38-
tracer = trace.get_tracer(__name__)
60+
tracer = get_tracer()
3961

4062
# Set base attributes that we know for every trace created
4163
attributes = {
4264
"db.type": "spanner",
4365
"db.url": SpannerClient.DEFAULT_ENDPOINT,
4466
"db.instance": session._database.name,
4567
"net.host.name": SpannerClient.DEFAULT_ENDPOINT,
68+
OTEL_SCOPE_NAME: TRACER_NAME,
69+
OTEL_SCOPE_VERSION: TRACER_VERSION,
4670
}
4771

4872
if extra_attributes:
@@ -52,9 +76,10 @@ def trace_call(name, session, extra_attributes=None):
5276
name, kind=trace.SpanKind.CLIENT, attributes=attributes
5377
) as span:
5478
try:
55-
span.set_status(Status(StatusCode.OK))
5679
yield span
57-
except GoogleAPICallError as error:
58-
span.set_status(Status(StatusCode.ERROR))
80+
except Exception as error:
81+
span.set_status(Status(StatusCode.ERROR, str(error)))
5982
span.record_exception(error)
6083
raise
84+
else:
85+
span.set_status(Status(StatusCode.OK))

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@
4747
]
4848
extras = {
4949
"tracing": [
50-
"opentelemetry-api >= 1.1.0",
51-
"opentelemetry-sdk >= 1.1.0",
52-
"opentelemetry-instrumentation >= 0.20b0, < 0.23dev",
50+
"opentelemetry-api >= 1.22.0",
51+
"opentelemetry-sdk >= 1.22.0",
52+
"opentelemetry-semantic-conventions >= 0.43b0",
5353
],
5454
"libcst": "libcst >= 0.2.5",
5555
}

testing/constraints-3.7.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ grpc-google-iam-v1==0.12.4
1010
libcst==0.2.5
1111
proto-plus==1.22.0
1212
sqlparse==0.4.4
13-
opentelemetry-api==1.1.0
14-
opentelemetry-sdk==1.1.0
15-
opentelemetry-instrumentation==0.20b0
13+
opentelemetry-api==1.22.0
14+
opentelemetry-sdk==1.22.0
15+
opentelemetry-semantic-conventions==0.43b0
1616
protobuf==3.20.2
1717
deprecated==1.2.14
1818
grpc-interceptor==0.15.4

tests/_helpers.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import unittest
22
import mock
33

4+
from google.cloud.spanner_v1 import gapic_version
5+
6+
LIB_VERSION = gapic_version.__version__
7+
48
try:
59
from opentelemetry import trace
610
from opentelemetry.sdk.trace import TracerProvider
711
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
812
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
913
InMemorySpanExporter,
1014
)
15+
from opentelemetry.semconv.attributes.otel_attributes import (
16+
OTEL_SCOPE_NAME,
17+
OTEL_SCOPE_VERSION,
18+
)
19+
1120
from opentelemetry.trace.status import StatusCode
1221

1322
trace.set_tracer_provider(TracerProvider())
@@ -30,6 +39,18 @@ def get_test_ot_exporter():
3039
return _TEST_OT_EXPORTER
3140

3241

42+
def enrich_with_otel_scope(attrs):
43+
"""
44+
This helper enriches attrs with OTEL_SCOPE_NAME and OTEL_SCOPE_VERSION
45+
for the purpose of avoiding cumbersome duplicated imports.
46+
"""
47+
if HAS_OPENTELEMETRY_INSTALLED:
48+
attrs[OTEL_SCOPE_NAME] = "cloud.google.com/python/spanner"
49+
attrs[OTEL_SCOPE_VERSION] = LIB_VERSION
50+
51+
return attrs
52+
53+
3354
def use_test_ot_exporter():
3455
global _TEST_OT_PROVIDER_INITIALIZED
3556

tests/system/test_session_api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,8 @@ def _make_attributes(db_instance, **kwargs):
346346
"net.host.name": "spanner.googleapis.com",
347347
"db.instance": db_instance,
348348
}
349+
ot_helpers.enrich_with_otel_scope(attributes)
350+
349351
attributes.update(kwargs)
350352

351353
return attributes

0 commit comments

Comments
 (0)