Skip to content

Commit b43e4f1

Browse files
committed
fix: use modernized and standardized OpenTelemetry when tracing
This change modernizes trace span attributes by using OpenTelemetry's semantic conventions that are standardized and allow for much better common ground adoption by broader systems, even more as Google Cloud Tracing & Monitoring pushes towards OpenTelemetry more. With this change we've made the replacement of these fields, directly with imports from `opentelemetry.semconv.trace.SpanAttributes`, as: * "db.type" => DB_SYSTEM aka "db.system" * "db.url" => DB_CONNECTION_STRING aka "db.connection_string" * "db.instance" => DB_NAME aka "db.name" * "net.host.name" => NET_HOST_NAME aka "net.host.name" While here, also updated opentelemetry-(api, sdk) dependencies to use versions "1.25.0", then opentelemetry-(instrumentation) to "0.46b0" Fixes #1170 Fixes #1173
1 parent fe20d41 commit b43e4f1

14 files changed

+190
-66
lines changed

docs/opentelemetry-tracing.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ To take advantage of these traces, we first need to install OpenTelemetry:
99
.. code-block:: sh
1010
1111
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation
12+
pip install opentelemetry-exporter-google-cloud
1213
1314
# [Optional] Installs the cloud monitoring exporter, however you can use any exporter of your choice
1415
pip install opentelemetry-exporter-google-cloud
@@ -19,14 +20,14 @@ We also need to tell OpenTelemetry which exporter to use. To export Spanner trac
1920
2021
from opentelemetry import trace
2122
from opentelemetry.sdk.trace import TracerProvider
22-
from opentelemetry.trace.sampling import ProbabilitySampler
23+
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
2324
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
2425
# BatchExportSpanProcessor exports spans to Cloud Trace
2526
# in a seperate thread to not block on the main thread
2627
from opentelemetry.sdk.trace.export import BatchExportSpanProcessor
2728
2829
# Create and export one trace every 1000 requests
29-
sampler = ProbabilitySampler(1/1000)
30+
sampler = TraceIdRatioBased(1/1000)
3031
# Use the default tracer provider
3132
trace.set_tracer_provider(TracerProvider(sampler=sampler))
3233
trace.get_tracer_provider().add_span_processor(

examples/trace.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 BatchExportSpanProcessor
23+
from opentelemetry.sdk.trace.sampling import ALWAYS_ON
24+
from opentelemetry import trace
25+
26+
27+
def main():
28+
# Setup OpenTelemetry, trace and Cloud Trace exporter.
29+
sampler = ALWAYS_ON
30+
tracerProvider = TracerProvider(sampler=sampler)
31+
tracerProvider.add_span_processor(
32+
BatchExportSpanProcessor(CloudTraceSpanExporter()))
33+
trace.set_tracer_provider(tracerProvider)
34+
tracer = trace.get_tracer(__name__)
35+
36+
# Setup the Cloud Spanner Client.
37+
project_id = os.environ.get('SPANNER_PROJECT_ID')
38+
spanner_client = spanner.Client(project_id)
39+
instance = spanner_client.instance('test-instance')
40+
database = instance.database('test-db')
41+
42+
# Now run our queries
43+
with tracer.start_as_current_span('QueryDatabase'):
44+
with database.snapshot() as snapshot:
45+
with tracer.start_as_current_span('InformationSchema'):
46+
info_schema = snapshot.execute_sql(
47+
'SELECT * FROM INFORMATION_SCHEMA.TABLES')
48+
for row in info_schema:
49+
print(row)
50+
51+
with tracer.start_as_current_span('ServerTimeQuery'):
52+
with database.snapshot() as snapshot:
53+
# Purposefully issue a bad SQL statement to examine exceptions
54+
# that get recorded and a ERROR span status.
55+
data = snapshot.execute_sql('SELECT CURRENT_TIMESTAMPx()')
56+
for row in data:
57+
print(row)
58+
59+
60+
if __name__ == '__main__':
61+
main()

google/cloud/spanner_v1/_opentelemetry_tracing.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,14 @@
2222
try:
2323
from opentelemetry import trace
2424
from opentelemetry.trace.status import Status, StatusCode
25+
from opentelemetry.semconv.trace import SpanAttributes
2526

2627
HAS_OPENTELEMETRY_INSTALLED = True
28+
DB_SYSTEM = SpanAttributes.DB_SYSTEM
29+
DB_NAME = SpanAttributes.DB_NAME
30+
DB_CONNECTION_STRING = SpanAttributes.DB_CONNECTION_STRING
31+
NET_HOST_NAME = SpanAttributes.NET_HOST_NAME
32+
DB_STATEMENT = spanAttributes.DB_STATEMENT
2733
except ImportError:
2834
HAS_OPENTELEMETRY_INSTALLED = False
2935

@@ -39,10 +45,10 @@ def trace_call(name, session, extra_attributes=None):
3945

4046
# Set base attributes that we know for every trace created
4147
attributes = {
42-
"db.type": "spanner",
43-
"db.url": SpannerClient.DEFAULT_ENDPOINT,
44-
"db.instance": session._database.name,
45-
"net.host.name": SpannerClient.DEFAULT_ENDPOINT,
48+
DB_SYSTEM: "google.cloud.spanner",
49+
DB_CONNECTION_STRING: SpannerClient.DEFAULT_ENDPOINT,
50+
DB_NAME: session._database.name,
51+
NET_HOST_NAME: SpannerClient.DEFAULT_ENDPOINT,
4652
}
4753

4854
if extra_attributes:

google/cloud/spanner_v1/snapshot.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
_check_rst_stream_error,
3939
_SessionWrapper,
4040
)
41-
from google.cloud.spanner_v1._opentelemetry_tracing import trace_call
41+
from google.cloud.spanner_v1._opentelemetry_tracing import (
42+
trace_call,
43+
DB_STATEMENT,
44+
)
4245
from google.cloud.spanner_v1.streamed import StreamedResultSet
4346
from google.cloud.spanner_v1 import RequestOptions
4447

@@ -488,7 +491,9 @@ def execute_sql(
488491
timeout=timeout,
489492
)
490493

491-
trace_attributes = {"db.statement": sql}
494+
# TODO(@odeke-em): only annotate this span's SQL
495+
# only if we have EXTENDED_TRACING=true enabled.
496+
trace_attributes = {DB_STATEMENT: sql}
492497

493498
if self._transaction_id is None:
494499
# lock is added to handle the inline begin for first rpc
@@ -696,7 +701,7 @@ def partition_query(
696701
partition_options=partition_options,
697702
)
698703

699-
trace_attributes = {"db.statement": sql}
704+
trace_attributes = {DB_STATEMENT: sql}
700705
with trace_call(
701706
"CloudSpanner.PartitionReadWriteTransaction",
702707
self._session,

setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@
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.25.0",
51+
"opentelemetry-sdk >= 1.25.0",
52+
"opentelemetry-instrumentation >= 0.46b0",
53+
"opentelemetry-semantic-conventions >= 0.46b0",
5354
],
5455
"libcst": "libcst >= 0.2.5",
5556
}

testing/constraints-3.7.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ 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.25.0
14+
opentelemetry-sdk==1.25.0
15+
opentelemetry-instrumentation==0.46b0
16+
opentelemetry-semantic-conventions==0.46b0
1617
protobuf==3.20.2
1718
deprecated==1.2.14
1819
grpc-interceptor==0.15.4

tests/_helpers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,25 @@
1010
)
1111
from opentelemetry.trace.status import StatusCode
1212

13+
from opentelemetry.semconv.trace import SpanAttributes
14+
1315
trace.set_tracer_provider(TracerProvider())
1416

1517
HAS_OPENTELEMETRY_INSTALLED = True
18+
19+
DB_SYSTEM = SpanAttributes.DB_SYSTEM
20+
DB_NAME = SpanAttributes.DB_NAME
21+
DB_CONNECTION_STRING = SpanAttributes.DB_CONNECTION_STRING
22+
NET_HOST_NAME = SpanAttributes.NET_HOST_NAME
23+
1624
except ImportError:
1725
HAS_OPENTELEMETRY_INSTALLED = False
1826

1927
StatusCode = mock.Mock()
28+
DB_SYSTEM = "db.system"
29+
DB_NAME = "db.name"
30+
DB_CONNECTION_STRING = "db.connection_string"
31+
NET_HOST_NAME = "net.host.name"
2032

2133
_TEST_OT_EXPORTER = None
2234
_TEST_OT_PROVIDER_INITIALIZED = False

tests/system/test_session_api.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,10 +341,10 @@ def assert_span_attributes(
341341

342342
def _make_attributes(db_instance, **kwargs):
343343
attributes = {
344-
"db.type": "spanner",
345-
"db.url": "spanner.googleapis.com",
346-
"net.host.name": "spanner.googleapis.com",
347-
"db.instance": db_instance,
344+
ot_helpers.DB_SYSTEM: "google.cloud.spanner",
345+
ot_helpers.DB_CONNECTION_STRING: "spanner.googleapis.com",
346+
ot_helpers.DB_NAME: db_instance,
347+
ot_helpers.NET_HOST_NAME: "spanner.googleapis.com",
348348
}
349349
attributes.update(kwargs)
350350

tests/unit/test__opentelemetry_tracing.py

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@
1212
from google.api_core.exceptions import GoogleAPICallError
1313
from google.cloud.spanner_v1 import _opentelemetry_tracing
1414

15-
from tests._helpers import OpenTelemetryBase, HAS_OPENTELEMETRY_INSTALLED
16-
15+
from tests._helpers import (
16+
OpenTelemetryBase,
17+
StatusCode,
18+
DB_SYSTEM,
19+
DB_NAME,
20+
DB_CONNECTION_STRING,
21+
HAS_OPENTELEMETRY_INSTALLED,
22+
NET_HOST_NAME
23+
)
1724

1825
def _make_rpc_error(error_cls, trailing_metadata=None):
1926
import grpc
@@ -51,14 +58,14 @@ class TestTracing(OpenTelemetryBase):
5158
def test_trace_call(self):
5259
extra_attributes = {
5360
"attribute1": "value1",
54-
# Since our database is mocked, we have to override the db.instance parameter so it is a string
55-
"db.instance": "database_name",
61+
# Since our database is mocked, we have to override the DB_NAME parameter so it is a string
62+
DB_NAME: "database_name",
5663
}
5764

5865
expected_attributes = {
59-
"db.type": "spanner",
60-
"db.url": "spanner.googleapis.com",
61-
"net.host.name": "spanner.googleapis.com",
66+
DB_SYSTEM: "google.cloud.spanner",
67+
DB_CONNECTION_STRING: "spanner.googleapis.com",
68+
NET_HOST_NAME: "spanner.googleapis.com",
6269
}
6370
expected_attributes.update(extra_attributes)
6471

@@ -78,13 +85,14 @@ def test_trace_call(self):
7885
self.assertEqual(span.status.status_code, StatusCode.OK)
7986

8087
def test_trace_error(self):
81-
extra_attributes = {"db.instance": "database_name"}
88+
extra_attributes = {DB_NAME: "database_name"}
8289

8390
expected_attributes = {
84-
"db.type": "spanner",
85-
"db.url": "spanner.googleapis.com",
86-
"net.host.name": "spanner.googleapis.com",
91+
DB_SYSTEM: "google.cloud.spanner",
92+
DB_CONNECTION_STRING: "spanner.googleapis.com",
93+
NET_HOST_NAME: "spanner.googleapis.com",
8794
}
95+
8896
expected_attributes.update(extra_attributes)
8997

9098
with self.assertRaises(GoogleAPICallError):
@@ -104,13 +112,14 @@ def test_trace_error(self):
104112
self.assertEqual(span.status.status_code, StatusCode.ERROR)
105113

106114
def test_trace_grpc_error(self):
107-
extra_attributes = {"db.instance": "database_name"}
115+
extra_attributes = {DB_NAME: "database_name"}
108116

109117
expected_attributes = {
110-
"db.type": "spanner",
111-
"db.url": "spanner.googleapis.com:443",
112-
"net.host.name": "spanner.googleapis.com:443",
118+
DB_SYSTEM: "google.cloud.spanner",
119+
DB_CONNECTION_STRING: "spanner.googleapis.com",
120+
NET_HOST_NAME: "spanner.googleapis.com",
113121
}
122+
114123
expected_attributes.update(extra_attributes)
115124

116125
with self.assertRaises(GoogleAPICallError):
@@ -127,13 +136,14 @@ def test_trace_grpc_error(self):
127136
self.assertEqual(span.status.status_code, StatusCode.ERROR)
128137

129138
def test_trace_codeless_error(self):
130-
extra_attributes = {"db.instance": "database_name"}
139+
extra_attributes = {DB_NAME: "database_name"}
131140

132141
expected_attributes = {
133-
"db.type": "spanner",
134-
"db.url": "spanner.googleapis.com:443",
135-
"net.host.name": "spanner.googleapis.com:443",
142+
DB_SYSTEM: "google.cloud.spanner",
143+
DB_CONNECTION_STRING: "spanner.googleapis.com",
144+
NET_HOST_NAME: "spanner.googleapis.com",
136145
}
146+
137147
expected_attributes.update(extra_attributes)
138148

139149
with self.assertRaises(GoogleAPICallError):

tests/unit/test_batch.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@
1414

1515

1616
import unittest
17-
from tests._helpers import OpenTelemetryBase, StatusCode
17+
from tests._helpers import (
18+
OpenTelemetryBase,
19+
StatusCode,
20+
DB_SYSTEM,
21+
DB_NAME,
22+
DB_CONNECTION_STRING,
23+
NET_HOST_NAME,
24+
)
1825
from google.cloud.spanner_v1 import RequestOptions
1926

2027
TABLE_NAME = "citizens"
@@ -24,10 +31,10 @@
2431
["[email protected]", "Bharney", "Rhubble", 31],
2532
]
2633
BASE_ATTRIBUTES = {
27-
"db.type": "spanner",
28-
"db.url": "spanner.googleapis.com",
29-
"db.instance": "testing",
30-
"net.host.name": "spanner.googleapis.com",
34+
DB_SYSTEM: "google.cloud.spanner",
35+
DB_CONNECTION_STRING: "spanner.googleapis.com",
36+
DB_NAME: "testing",
37+
NET_HOST_NAME: "spanner.googleapis.com",
3138
}
3239

3340

0 commit comments

Comments
 (0)