Skip to content

Commit 285c578

Browse files
committed
all: implement custom tracer_provider injection
An important feature for observability is to allow the injection of a custom tracer_provider instead of always using the global tracer_provider by sending in observability_options=dict( tracer_provider=tracer_provider, enable_extended_tracing=True, )
1 parent 41604fe commit 285c578

File tree

9 files changed

+117
-18
lines changed

9 files changed

+117
-18
lines changed

docs/opentelemetry-tracing.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,16 @@ We also need to tell OpenTelemetry which exporter to use. To export Spanner trac
2525
2626
# Create and export one trace every 1000 requests
2727
sampler = TraceIdRatioBased(1/1000)
28-
# Use the default tracer provider
29-
trace.set_tracer_provider(TracerProvider(sampler=sampler))
30-
trace.get_tracer_provider().add_span_processor(
28+
tracer_provider = TracerProvider(sampler=sampler)
29+
tracer_provider.add_span_processor(
3130
# Initialize the cloud tracing exporter
3231
BatchSpanProcessor(CloudTraceSpanExporter())
3332
)
33+
observability_options = dict(
34+
tracer_provider=tracer_provider,
35+
enable_extended_tracing=True,
36+
)
37+
spanner = spanner.NewClient(project_id, observability_options=observability_options)
3438
3539
3640
To get more fine-grained traces from gRPC, you can enable the gRPC instrumentation by the following

examples/trace.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,18 @@ def main():
3232
tracer_provider = TracerProvider(sampler=ALWAYS_ON)
3333
trace_exporter = CloudTraceSpanExporter(project_id=project_id)
3434
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')
3835

3936
# Setup the Cloud Spanner Client.
40-
spanner_client = spanner.Client(project_id)
37+
spanner_client = spanner.Client(
38+
project_id,
39+
observability_options=dict(tracer_provider=tracer_provider, enable_extended_tracing=True),
40+
)
4141
instance = spanner_client.instance('test-instance')
4242
database = instance.database('test-db')
4343

44+
# Retrieve a tracer from our custom tracer provider.
45+
tracer = tracer_provider.get_tracer('MyApp')
46+
4447
# Now run our queries
4548
with tracer.start_as_current_span('QueryInformationSchema'):
4649
with database.snapshot() as snapshot:

google/cloud/spanner_v1/_opentelemetry_tracing.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@
3535
TRACER_VERSION = gapic_version.__version__
3636

3737

38+
class ObservabilityOptions:
39+
def __init__(self, tracer_provider=None, enable_extended_tracing=False):
40+
self.__tracer_provider = tracer_provider
41+
self.__enable_extended_tracing = enable_extended_tracing
42+
43+
@property
44+
def tracer_provider(self):
45+
return self.__tracer_provider
46+
47+
@property
48+
def enable_extended_tracing(self):
49+
return self.__enable_extended_tracing
50+
51+
3852
def get_tracer(tracer_provider=None):
3953
"""
4054
get_tracer is a utility to unify and simplify retrieval of the tracer, without
@@ -51,13 +65,21 @@ def get_tracer(tracer_provider=None):
5165

5266

5367
@contextmanager
54-
def trace_call(name, session, extra_attributes=None):
68+
def trace_call(name, session, extra_attributes=None, observability_options=None):
5569
if not HAS_OPENTELEMETRY_INSTALLED or not session:
5670
# Empty context manager. Users will have to check if the generated value is None or a span
5771
yield None
5872
return
5973

60-
tracer = get_tracer()
74+
tracer_provider = None
75+
enable_extended_tracing = False
76+
if getattr(session, "_observability_options", None):
77+
opts = session._observability_options
78+
if opts:
79+
tracer_provider = opts.tracer_provider
80+
enable_extended_tracing = opts.enable_extended_tracing
81+
82+
tracer = get_tracer(tracer_provider)
6183

6284
# Set base attributes that we know for every trace created
6385
attributes = {
@@ -72,6 +94,13 @@ def trace_call(name, session, extra_attributes=None):
7294
if extra_attributes:
7395
attributes.update(extra_attributes)
7496

97+
# TODO(@odeke-em) enable after discussion with team and agreement
98+
# over extended tracing changes as the legacy default is always to
99+
# record SQL statements on spans.
100+
if False and not enable_extended_tracing:
101+
attributes.pop("db.statement", False)
102+
attributes.pop("sql", False)
103+
75104
with tracer.start_as_current_span(
76105
name, kind=trace.SpanKind.CLIENT, attributes=attributes
77106
) as span:

google/cloud/spanner_v1/client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ class Client(ClientWithProject):
126126
for all ReadRequests and ExecuteSqlRequests that indicates which replicas
127127
or regions should be used for non-transactional reads or queries.
128128
129+
:type labels: dict (str -> any) or None
130+
:param observability_options: (Optional) the configuration to control
131+
the tracer's behavior.
132+
tracer_provider is the injected tracer provider
133+
enable_extended_tracing: :type:boolean when set to true will allow for
134+
spans that issue SQL statements to be annotated with SQL.
135+
129136
:raises: :class:`ValueError <exceptions.ValueError>` if both ``read_only``
130137
and ``admin`` are :data:`True`
131138
"""
@@ -146,6 +153,7 @@ def __init__(
146153
query_options=None,
147154
route_to_leader_enabled=True,
148155
directed_read_options=None,
156+
observability_options=None,
149157
):
150158
self._emulator_host = _get_spanner_emulator_host()
151159

@@ -187,6 +195,7 @@ def __init__(
187195

188196
self._route_to_leader_enabled = route_to_leader_enabled
189197
self._directed_read_options = directed_read_options
198+
self._observability_options = observability_options
190199

191200
@property
192201
def credentials(self):
@@ -371,6 +380,7 @@ def instance(
371380
self._emulator_host,
372381
labels,
373382
processing_units,
383+
observability_options=self._observability_options,
374384
)
375385

376386
def list_instances(self, filter_="", page_size=None):

google/cloud/spanner_v1/database.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def __init__(
156156
database_role=None,
157157
enable_drop_protection=False,
158158
proto_descriptors=None,
159+
observability_options=None,
159160
):
160161
self.database_id = database_id
161162
self._instance = instance
@@ -178,11 +179,16 @@ def __init__(
178179
self._reconciling = False
179180
self._directed_read_options = self._instance._client.directed_read_options
180181
self._proto_descriptors = proto_descriptors
182+
self._observability_options = observability_options
181183

182184
if pool is None:
183-
pool = BurstyPool(database_role=database_role)
185+
pool = BurstyPool(
186+
database_role=database_role,
187+
observability_options=self._observability_options,
188+
)
184189

185190
self._pool = pool
191+
self._pool._observability_options = observability_options
186192
pool.bind(self)
187193

188194
@classmethod
@@ -742,7 +748,12 @@ def session(self, labels=None, database_role=None):
742748
# If role is specified in param, then that role is used
743749
# instead.
744750
role = database_role or self._database_role
745-
return Session(self, labels=labels, database_role=role)
751+
return Session(
752+
self,
753+
labels=labels,
754+
database_role=role,
755+
observability_options=self._observability_options,
756+
)
746757

747758
def snapshot(self, **kw):
748759
"""Return an object which wraps a snapshot.

google/cloud/spanner_v1/instance.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ class Instance(object):
110110
111111
:type labels: dict (str -> str) or None
112112
:param labels: (Optional) User-assigned labels for this instance.
113+
114+
:type labels: dict (str -> any) or None
115+
:param observability_options: (Optional) the configuration to control
116+
the tracer's behavior.
117+
tracer_provider is the injected tracer provider
118+
enable_extended_tracing: :type:boolean when set to true will allow for
119+
spans that issue SQL statements to be annotated with SQL.
113120
"""
114121

115122
def __init__(
@@ -122,6 +129,7 @@ def __init__(
122129
emulator_host=None,
123130
labels=None,
124131
processing_units=None,
132+
observability_options=None,
125133
):
126134
self.instance_id = instance_id
127135
self._client = client
@@ -145,6 +153,7 @@ def __init__(
145153
if labels is None:
146154
labels = {}
147155
self.labels = labels
156+
self._observability_options = observability_options
148157

149158
def _update_from_pb(self, instance_pb):
150159
"""Refresh self from the server-provided protobuf.
@@ -499,6 +508,7 @@ def database(
499508
database_role=database_role,
500509
enable_drop_protection=enable_drop_protection,
501510
proto_descriptors=proto_descriptors,
511+
observability_options=self._observability_options,
502512
)
503513
else:
504514
return TestDatabase(

google/cloud/spanner_v1/pool.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ class AbstractSessionPool(object):
4242

4343
_database = None
4444

45-
def __init__(self, labels=None, database_role=None):
45+
def __init__(self, labels=None, database_role=None, observability_options=None):
4646
if labels is None:
4747
labels = {}
4848
self._labels = labels
4949
self._database_role = database_role
50+
self._observability_options = observability_options
5051

5152
@property
5253
def labels(self):
@@ -178,8 +179,13 @@ def __init__(
178179
default_timeout=DEFAULT_TIMEOUT,
179180
labels=None,
180181
database_role=None,
182+
observability_options=None,
181183
):
182-
super(FixedSizePool, self).__init__(labels=labels, database_role=database_role)
184+
super(FixedSizePool, self).__init__(
185+
labels=labels,
186+
database_role=database_role,
187+
observability_options=observability_options,
188+
)
183189
self.size = size
184190
self.default_timeout = default_timeout
185191
self._sessions = queue.LifoQueue(size)
@@ -284,8 +290,18 @@ class BurstyPool(AbstractSessionPool):
284290
:param database_role: (Optional) user-assigned database_role for the session.
285291
"""
286292

287-
def __init__(self, target_size=10, labels=None, database_role=None):
288-
super(BurstyPool, self).__init__(labels=labels, database_role=database_role)
293+
def __init__(
294+
self,
295+
target_size=10,
296+
labels=None,
297+
database_role=None,
298+
observability_options=None,
299+
):
300+
super(BurstyPool, self).__init__(
301+
labels=labels,
302+
database_role=database_role,
303+
observability_options=observability_options,
304+
)
289305
self.target_size = target_size
290306
self._database = None
291307
self._sessions = queue.LifoQueue(target_size)
@@ -392,8 +408,13 @@ def __init__(
392408
ping_interval=3000,
393409
labels=None,
394410
database_role=None,
411+
observability_options=None,
395412
):
396-
super(PingingPool, self).__init__(labels=labels, database_role=database_role)
413+
super(PingingPool, self).__init__(
414+
labels=labels,
415+
database_role=database_role,
416+
observability_options=observability_options,
417+
)
397418
self.size = size
398419
self.default_timeout = default_timeout
399420
self._delta = datetime.timedelta(seconds=ping_interval)
@@ -546,6 +567,7 @@ def __init__(
546567
ping_interval=3000,
547568
labels=None,
548569
database_role=None,
570+
observability_options=None,
549571
):
550572
"""This throws a deprecation warning on initialization."""
551573
warn(
@@ -561,6 +583,7 @@ def __init__(
561583
ping_interval,
562584
labels=labels,
563585
database_role=database_role,
586+
observability_options=observability_options,
564587
)
565588

566589
self.begin_pending_transactions()

google/cloud/spanner_v1/session.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,15 @@ class Session(object):
6363
_session_id = None
6464
_transaction = None
6565

66-
def __init__(self, database, labels=None, database_role=None):
66+
def __init__(
67+
self, database, labels=None, database_role=None, observability_options=None
68+
):
6769
self._database = database
6870
if labels is None:
6971
labels = {}
7072
self._labels = labels
7173
self._database_role = database_role
74+
self._observability_options = observability_options
7275

7376
def __lt__(self, other):
7477
return self._session_id < other._session_id

tests/unit/test__opentelemetry_tracing.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ def _make_rpc_error(error_cls, trailing_metadata=None):
3030
def _make_session():
3131
from google.cloud.spanner_v1.session import Session
3232

33-
return mock.Mock(autospec=Session, instance=True)
33+
session = mock.Mock(autospec=Session, instance=True)
34+
# Setting _tracer_provider to None is to avoid the nasty spill-over
35+
# of mock._tracer_provider spuriously failing tests, because per
36+
# unittest.mock.Mock's definition invoking any attribute or method
37+
# returns another mock.
38+
setattr(session, "_observability_options", None)
39+
return session
3440

3541

3642
# Skip all of these tests if we don't have OpenTelemetry

0 commit comments

Comments
 (0)