Skip to content

Commit 847d89b

Browse files
committed
feat(observability): more descriptive and value adding spans
This change adds more descriptive and value adding spans to replace the generic CloudSpanner.ReadWriteTransaction. With this change, we add new spans: * CloudSpanner.Database.run_in_transaction * CloudSpanner.execute_pdml * CloudSpanner.execute_sql * CloudSpanner.execute_update
1 parent a6811af commit 847d89b

14 files changed

+977
-294
lines changed

google/cloud/spanner_v1/_opentelemetry_tracing.py

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,11 @@ def get_tracer(tracer_provider=None):
5555
return tracer_provider.get_tracer(TRACER_NAME, TRACER_VERSION)
5656

5757

58-
@contextmanager
59-
def trace_call(name, session, extra_attributes=None, observability_options=None):
60-
if session:
61-
session._last_use_time = datetime.now()
62-
63-
if not HAS_OPENTELEMETRY_INSTALLED or not session:
64-
# Empty context manager. Users will have to check if the generated value is None or a span
65-
yield None
66-
return
58+
def _make_tracer_and_span_attributes(
59+
session=None, extra_attributes=None, observability_options=None
60+
):
61+
if not HAS_OPENTELEMETRY_INSTALLED:
62+
return None, None
6763

6864
tracer_provider = None
6965

@@ -72,20 +68,24 @@ def trace_call(name, session, extra_attributes=None, observability_options=None)
7268
# on by default.
7369
enable_extended_tracing = True
7470

71+
db_name = ""
72+
if session and getattr(session, "_database", None):
73+
db_name = session._database.name
74+
7575
if isinstance(observability_options, dict): # Avoid false positives with mock.Mock
7676
tracer_provider = observability_options.get("tracer_provider", None)
7777
enable_extended_tracing = observability_options.get(
7878
"enable_extended_tracing", enable_extended_tracing
7979
)
80+
db_name = observability_options.get("db_name", db_name)
8081

8182
tracer = get_tracer(tracer_provider)
8283

8384
# Set base attributes that we know for every trace created
84-
db = session._database
8585
attributes = {
8686
"db.type": "spanner",
8787
"db.url": SpannerClient.DEFAULT_ENDPOINT,
88-
"db.instance": "" if not db else db.name,
88+
"db.instance": db_name,
8989
"net.host.name": SpannerClient.DEFAULT_ENDPOINT,
9090
OTEL_SCOPE_NAME: TRACER_NAME,
9191
OTEL_SCOPE_VERSION: TRACER_VERSION,
@@ -99,9 +99,77 @@ def trace_call(name, session, extra_attributes=None, observability_options=None)
9999

100100
if not enable_extended_tracing:
101101
attributes.pop("db.statement", False)
102+
attributes.pop("sql", False)
103+
else:
104+
# Otherwise there are places where the annotated sql was inserted
105+
# directly from the arguments as "sql", and transform those into "db.statement".
106+
db_statement = attributes.get("db.statement", None)
107+
if not db_statement:
108+
sql = attributes.get("sql", None)
109+
if sql:
110+
attributes = attributes.copy()
111+
attributes.pop("sql", False)
112+
attributes["db.statement"] = sql
113+
114+
return tracer, attributes
115+
116+
117+
def trace_call_end_lazily(
118+
name, session=None, extra_attributes=None, observability_options=None
119+
):
120+
"""
121+
trace_call_end_lazily is used in situations where you don't want a context managed
122+
span in a with statement to end as soon as a block exits. This is useful for example
123+
after a Database.batch or Database.snapshot but without a context manager.
124+
If you need to directly invoke tracing with a context manager, please invoke
125+
`trace_call` with which you can invoke
126+
 `with trace_call(...) as span:`
127+
It is the caller's responsibility to explicitly invoke the returned ending function.
128+
"""
129+
if not name:
130+
return None
131+
132+
tracer, span_attributes = _make_tracer_and_span_attributes(
133+
session, extra_attributes, observability_options
134+
)
135+
if not tracer:
136+
return None
137+
138+
span = tracer.start_span(
139+
name, kind=trace.SpanKind.CLIENT, attributes=span_attributes
140+
)
141+
ctx_manager = trace.use_span(span, end_on_exit=True, record_exception=True)
142+
ctx_manager.__enter__()
143+
144+
def discard(exc_type=None, exc_value=None, exc_traceback=None):
145+
if not exc_type:
146+
span.set_status(Status(StatusCode.OK))
147+
148+
ctx_manager.__exit__(exc_type, exc_value, exc_traceback)
149+
150+
return discard
151+
152+
153+
@contextmanager
154+
def trace_call(name, session=None, extra_attributes=None, observability_options=None):
155+
"""
156+
 trace_call is used in situations where you need to end a span with a context manager
157+
 or after a scope is exited. If you need to keep a span alive and lazily end it, please
158+
 invoke `trace_call_end_lazily`.
159+
"""
160+
if not name:
161+
yield None
162+
return
163+
164+
tracer, span_attributes = _make_tracer_and_span_attributes(
165+
session, extra_attributes, observability_options
166+
)
167+
if not tracer:
168+
yield None
169+
return
102170

103171
with tracer.start_as_current_span(
104-
name, kind=trace.SpanKind.CLIENT, attributes=attributes
172+
name, kind=trace.SpanKind.CLIENT, attributes=span_attributes
105173
) as span:
106174
try:
107175
yield span
@@ -131,3 +199,16 @@ def get_current_span():
131199
def add_span_event(span, event_name, event_attributes=None):
132200
if span:
133201
span.add_event(event_name, event_attributes)
202+
203+
204+
def add_event_on_current_span(event_name, event_attributes=None, span=None):
205+
if not span:
206+
span = get_current_span()
207+
208+
add_span_event(span, event_name, event_attributes)
209+
210+
211+
def record_span_exception_and_status(span, exc):
212+
if span:
213+
span.set_status(Status(StatusCode.ERROR, str(exc)))
214+
span.record_exception(exc)

google/cloud/spanner_v1/batch.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@
2626
_metadata_with_prefix,
2727
_metadata_with_leader_aware_routing,
2828
)
29-
from google.cloud.spanner_v1._opentelemetry_tracing import trace_call
29+
from google.cloud.spanner_v1._opentelemetry_tracing import (
30+
add_event_on_current_span,
31+
trace_call,
32+
trace_call_end_lazily,
33+
)
3034
from google.cloud.spanner_v1 import RequestOptions
3135
from google.cloud.spanner_v1._helpers import _retry
3236
from google.cloud.spanner_v1._helpers import _check_rst_stream_error
@@ -46,6 +50,12 @@ class _BatchBase(_SessionWrapper):
4650
def __init__(self, session):
4751
super(_BatchBase, self).__init__(session)
4852
self._mutations = []
53+
self.__base_discard_span = trace_call_end_lazily(
54+
f"CloudSpanner.{type(self).__name__}",
55+
self._session,
56+
None,
57+
getattr(self._session._database, "observability_options", None),
58+
)
4959

5060
def _check_state(self):
5161
"""Helper for :meth:`commit` et al.
@@ -69,6 +79,10 @@ def insert(self, table, columns, values):
6979
:type values: list of lists
7080
:param values: Values to be modified.
7181
"""
82+
add_event_on_current_span(
83+
"insert mutations added",
84+
dict(table=table, columns=columns),
85+
)
7286
self._mutations.append(Mutation(insert=_make_write_pb(table, columns, values)))
7387

7488
def update(self, table, columns, values):
@@ -84,6 +98,10 @@ def update(self, table, columns, values):
8498
:param values: Values to be modified.
8599
"""
86100
self._mutations.append(Mutation(update=_make_write_pb(table, columns, values)))
101+
add_event_on_current_span(
102+
"update mutations added",
103+
dict(table=table, columns=columns),
104+
)
87105

88106
def insert_or_update(self, table, columns, values):
89107
"""Insert/update one or more table rows.
@@ -100,6 +118,10 @@ def insert_or_update(self, table, columns, values):
100118
self._mutations.append(
101119
Mutation(insert_or_update=_make_write_pb(table, columns, values))
102120
)
121+
add_event_on_current_span(
122+
"insert_or_update mutations added",
123+
dict(table=table, columns=columns),
124+
)
103125

104126
def replace(self, table, columns, values):
105127
"""Replace one or more table rows.
@@ -114,6 +136,10 @@ def replace(self, table, columns, values):
114136
:param values: Values to be modified.
115137
"""
116138
self._mutations.append(Mutation(replace=_make_write_pb(table, columns, values)))
139+
add_event_on_current_span(
140+
"replace mutations added",
141+
dict(table=table, columns=columns),
142+
)
117143

118144
def delete(self, table, keyset):
119145
"""Delete one or more table rows.
@@ -126,6 +152,21 @@ def delete(self, table, keyset):
126152
"""
127153
delete = Mutation.Delete(table=table, key_set=keyset._to_pb())
128154
self._mutations.append(Mutation(delete=delete))
155+
add_event_on_current_span(
156+
"delete mutations added",
157+
dict(table=table),
158+
)
159+
160+
def _discard_on_end(self, exc_type=None, exc_val=None, exc_traceback=None):
161+
if self.__base_discard_span:
162+
self.__base_discard_span(exc_type, exc_val, exc_traceback)
163+
self.__base_discard_span = None
164+
165+
def __exit__(self, exc_type=None, exc_value=None, exc_traceback=None):
166+
self._discard_on_end(exc_type, exc_val, exc_traceback)
167+
168+
def __enter__(self):
169+
return self
129170

130171

131172
class Batch(_BatchBase):
@@ -207,7 +248,7 @@ def commit(
207248
)
208249
observability_options = getattr(database, "observability_options", None)
209250
with trace_call(
210-
"CloudSpanner.Commit",
251+
"CloudSpanner.Batch.commit",
211252
self._session,
212253
trace_attributes,
213254
observability_options=observability_options,
@@ -223,18 +264,31 @@ def commit(
223264
)
224265
self.committed = response.commit_timestamp
225266
self.commit_stats = response.commit_stats
267+
self._discard_on_end()
226268
return self.committed
227269

228270
def __enter__(self):
229271
"""Begin ``with`` block."""
230272
self._check_state()
273+
observability_options = getattr(
274+
self._session._database, "observability_options", None
275+
)
276+
self.__discard_span = trace_call_end_lazily(
277+
"CloudSpanner.Batch",
278+
self._session,
279+
observability_options=observability_options,
280+
)
231281

232282
return self
233283

234284
def __exit__(self, exc_type, exc_val, exc_tb):
235285
"""End ``with`` block."""
236286
if exc_type is None:
237287
self.commit()
288+
if self.__discard_span:
289+
self.__discard_span(exc_type, exc_val, exc_tb)
290+
self.__discard_span = None
291+
self._discard_on_end()
238292

239293

240294
class MutationGroup(_BatchBase):
@@ -326,7 +380,7 @@ def batch_write(self, request_options=None, exclude_txn_from_change_streams=Fals
326380
)
327381
observability_options = getattr(database, "observability_options", None)
328382
with trace_call(
329-
"CloudSpanner.BatchWrite",
383+
"CloudSpanner.batch_write",
330384
self._session,
331385
trace_attributes,
332386
observability_options=observability_options,

0 commit comments

Comments
 (0)