Skip to content

Commit f28ed47

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 ad69c48 commit f28ed47

14 files changed

+757
-189
lines changed

google/cloud/spanner_v1/_opentelemetry_tracing.py

Lines changed: 87 additions & 10 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=None, extra_attributes=None, observability_options=None):
60-
if session:
61-
session._last_use_time = datetime.now()
62-
63-
if not (HAS_OPENTELEMETRY_INSTALLED and name):
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

@@ -103,9 +99,77 @@ def trace_call(name, session=None, extra_attributes=None, observability_options=
10399

104100
if not enable_extended_tracing:
105101
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
106170

107171
with tracer.start_as_current_span(
108-
name, kind=trace.SpanKind.CLIENT, attributes=attributes
172+
name, kind=trace.SpanKind.CLIENT, attributes=span_attributes
109173
) as span:
110174
try:
111175
yield span
@@ -135,3 +199,16 @@ def get_current_span():
135199
def add_span_event(span, event_name, event_attributes=None):
136200
if span:
137201
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: 40 additions & 2 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
# TODO: Decide if we should add a span event per mutation:
7488
# https://github.com/googleapis/python-spanner/issues/1269
@@ -137,6 +151,17 @@ def delete(self, table, keyset):
137151
# TODO: Decide if we should add a span event per mutation:
138152
# https://github.com/googleapis/python-spanner/issues/1269
139153

154+
def _discard_on_end(self, exc_type=None, exc_val=None, exc_traceback=None):
155+
if self.__base_discard_span:
156+
self.__base_discard_span(exc_type, exc_val, exc_traceback)
157+
self.__base_discard_span = None
158+
159+
def __exit__(self, exc_type=None, exc_value=None, exc_traceback=None):
160+
self._discard_on_end(exc_type, exc_val, exc_traceback)
161+
162+
def __enter__(self):
163+
return self
164+
140165

141166
class Batch(_BatchBase):
142167
"""Accumulate mutations for transmission during :meth:`commit`."""
@@ -233,18 +258,31 @@ def commit(
233258
)
234259
self.committed = response.commit_timestamp
235260
self.commit_stats = response.commit_stats
261+
self._discard_on_end()
236262
return self.committed
237263

238264
def __enter__(self):
239265
"""Begin ``with`` block."""
240266
self._check_state()
267+
observability_options = getattr(
268+
self._session._database, "observability_options", None
269+
)
270+
self.__discard_span = trace_call_end_lazily(
271+
"CloudSpanner.Batch",
272+
self._session,
273+
observability_options=observability_options,
274+
)
241275

242276
return self
243277

244278
def __exit__(self, exc_type, exc_val, exc_tb):
245279
"""End ``with`` block."""
246280
if exc_type is None:
247281
self.commit()
282+
if self.__discard_span:
283+
self.__discard_span(exc_type, exc_val, exc_tb)
284+
self.__discard_span = None
285+
self._discard_on_end()
248286

249287

250288
class MutationGroup(_BatchBase):
@@ -336,7 +374,7 @@ def batch_write(self, request_options=None, exclude_txn_from_change_streams=Fals
336374
)
337375
observability_options = getattr(database, "observability_options", None)
338376
with trace_call(
339-
"CloudSpanner.BatchWrite",
377+
"CloudSpanner.batch_write",
340378
self._session,
341379
trace_attributes,
342380
observability_options=observability_options,

0 commit comments

Comments
 (0)