From 2ba8794881a094183757363d0df3deb74a233c9c Mon Sep 17 00:00:00 2001 From: Subham Sinha Date: Tue, 9 Sep 2025 14:52:18 +0530 Subject: [PATCH] Make OpenTelemetry default --- .../spanner_v1/_opentelemetry_tracing.py | 27 +- google/cloud/spanner_v1/request_id_header.py | 3 +- google/cloud/spanner_v1/session.py | 81 ++-- setup.py | 19 +- tests/unit/test__opentelemetry_tracing.py | 353 ++++++++---------- tests/unit/test_session.py | 89 ++--- 6 files changed, 250 insertions(+), 322 deletions(-) diff --git a/google/cloud/spanner_v1/_opentelemetry_tracing.py b/google/cloud/spanner_v1/_opentelemetry_tracing.py index eafc983850..8abdb28ffb 100644 --- a/google/cloud/spanner_v1/_opentelemetry_tracing.py +++ b/google/cloud/spanner_v1/_opentelemetry_tracing.py @@ -24,17 +24,12 @@ _metadata_with_span_context, ) -try: - from opentelemetry import trace - from opentelemetry.trace.status import Status, StatusCode - from opentelemetry.semconv.attributes.otel_attributes import ( - OTEL_SCOPE_NAME, - OTEL_SCOPE_VERSION, - ) - - HAS_OPENTELEMETRY_INSTALLED = True -except ImportError: - HAS_OPENTELEMETRY_INSTALLED = False +from opentelemetry import trace +from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.semconv.attributes.otel_attributes import ( + OTEL_SCOPE_NAME, + OTEL_SCOPE_VERSION, +) from google.cloud.spanner_v1.metrics.metrics_capture import MetricsCapture @@ -70,11 +65,6 @@ def trace_call( if session: session._last_use_time = datetime.now() - if not (HAS_OPENTELEMETRY_INSTALLED and name): - # Empty context manager. Users will have to check if the generated value is None or a span - yield None - return - tracer_provider = None # By default enable_extended_tracing=True because in a bid to minimize @@ -155,11 +145,8 @@ def trace_call( def get_current_span(): - if not HAS_OPENTELEMETRY_INSTALLED: - return None return trace.get_current_span() def add_span_event(span, event_name, event_attributes=None): - if span: - span.add_event(event_name, event_attributes) + span.add_event(event_name, event_attributes) diff --git a/google/cloud/spanner_v1/request_id_header.py b/google/cloud/spanner_v1/request_id_header.py index b540b725f5..69b70539a0 100644 --- a/google/cloud/spanner_v1/request_id_header.py +++ b/google/cloud/spanner_v1/request_id_header.py @@ -43,8 +43,7 @@ def with_request_id( all_metadata = (other_metadata or []).copy() all_metadata.append((REQ_ID_HEADER_KEY, req_id)) - if span is not None: - span.set_attribute(X_GOOG_SPANNER_REQUEST_ID_SPAN_ATTR, req_id) + span.set_attribute(X_GOOG_SPANNER_REQUEST_ID_SPAN_ATTR, req_id) return all_metadata diff --git a/google/cloud/spanner_v1/session.py b/google/cloud/spanner_v1/session.py index 1a9313d0d3..0f99c213c0 100644 --- a/google/cloud/spanner_v1/session.py +++ b/google/cloud/spanner_v1/session.py @@ -251,11 +251,9 @@ def exists(self): span, ), ) - if span: - span.set_attribute("session_found", True) + span.set_attribute("session_found", True) except NotFound: - if span: - span.set_attribute("session_found", False) + span.set_attribute("session_found", False) return False return True @@ -311,18 +309,21 @@ def ping(self): """ if self._session_id is None: raise ValueError("Session ID not set by back-end") + database = self._database api = database.spanner_api - request = ExecuteSqlRequest(session=self.name, sql="SELECT 1") - api.execute_sql( - request=request, - metadata=database.metadata_with_request_id( - database._next_nth_request, - 1, - _metadata_with_prefix(database.name), - ), - ) - self._last_use_time = datetime.now() + + with trace_call("CloudSpanner.Session.ping", self) as span: + request = ExecuteSqlRequest(session=self.name, sql="SELECT 1") + api.execute_sql( + request=request, + metadata=database.metadata_with_request_id( + database._next_nth_request, + 1, + _metadata_with_prefix(database.name), + span, + ), + ) def snapshot(self, **kw): """Create a snapshot to perform a set of reads with shared staleness. @@ -557,20 +558,18 @@ def run_in_transaction(self, func, *args, **kw): except Aborted as exc: previous_transaction_id = txn._transaction_id - if span: - delay_seconds = _get_retry_delay( - exc.errors[0], - attempts, - default_retry_delay=default_retry_delay, - ) - attributes = dict(delay_seconds=delay_seconds, cause=str(exc)) - attributes.update(span_attributes) - add_span_event( - span, - "Transaction was aborted in user operation, retrying", - attributes, - ) - + delay_seconds = _get_retry_delay( + exc.errors[0], + attempts, + default_retry_delay=default_retry_delay, + ) + attributes = dict(delay_seconds=delay_seconds, cause=str(exc)) + attributes.update(span_attributes) + add_span_event( + span, + "Transaction was aborted in user operation, retrying", + attributes, + ) _delay_until_retry( exc, deadline, attempts, default_retry_delay=default_retry_delay ) @@ -602,20 +601,18 @@ def run_in_transaction(self, func, *args, **kw): except Aborted as exc: previous_transaction_id = txn._transaction_id - if span: - delay_seconds = _get_retry_delay( - exc.errors[0], - attempts, - default_retry_delay=default_retry_delay, - ) - attributes = dict(delay_seconds=delay_seconds) - attributes.update(span_attributes) - add_span_event( - span, - "Transaction was aborted during commit, retrying", - attributes, - ) - + delay_seconds = _get_retry_delay( + exc.errors[0], + attempts, + default_retry_delay=default_retry_delay, + ) + attributes = dict(delay_seconds=delay_seconds) + attributes.update(span_attributes) + add_span_event( + span, + "Transaction was aborted during commit, retrying", + attributes, + ) _delay_until_retry( exc, deadline, attempts, default_retry_delay=default_retry_delay ) diff --git a/setup.py b/setup.py index a32883075b..02f8d09d5f 100644 --- a/setup.py +++ b/setup.py @@ -44,18 +44,15 @@ "proto-plus >= 1.22.2, <2.0.0; python_version>='3.11'", "protobuf>=3.20.2,<7.0.0,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5", "grpc-interceptor >= 0.15.4", + # Make OpenTelemetry a core dependency + "opentelemetry-api >= 1.22.0", + "opentelemetry-sdk >= 1.22.0", + "opentelemetry-semantic-conventions >= 0.43b0", + "opentelemetry-resourcedetector-gcp >= 1.8.0a0", + "google-cloud-monitoring >= 2.16.0", + "mmh3 >= 4.1.0 ", ] -extras = { - "tracing": [ - "opentelemetry-api >= 1.22.0", - "opentelemetry-sdk >= 1.22.0", - "opentelemetry-semantic-conventions >= 0.43b0", - "opentelemetry-resourcedetector-gcp >= 1.8.0a0", - "google-cloud-monitoring >= 2.16.0", - "mmh3 >= 4.1.0 ", - ], - "libcst": "libcst >= 0.2.5", -} +extras = {"libcst": "libcst >= 0.2.5"} url = "https://github.com/googleapis/python-spanner" diff --git a/tests/unit/test__opentelemetry_tracing.py b/tests/unit/test__opentelemetry_tracing.py index b3d49355c0..9b6933f9f7 100644 --- a/tests/unit/test__opentelemetry_tracing.py +++ b/tests/unit/test__opentelemetry_tracing.py @@ -1,7 +1,5 @@ -import importlib import mock import unittest -import sys try: from opentelemetry import trace as trace_api @@ -15,7 +13,6 @@ from tests._helpers import ( OpenTelemetryBase, LIB_VERSION, - HAS_OPENTELEMETRY_INSTALLED, enrich_with_otel_scope, ) @@ -34,200 +31,182 @@ def _make_session(): return mock.Mock(autospec=Session, instance=True) -# Skip all of these tests if we don't have OpenTelemetry -if HAS_OPENTELEMETRY_INSTALLED: - - class TestNoTracing(unittest.TestCase): - def setUp(self): - self._temp_opentelemetry = sys.modules["opentelemetry"] - - sys.modules["opentelemetry"] = None - importlib.reload(_opentelemetry_tracing) - - def tearDown(self): - sys.modules["opentelemetry"] = self._temp_opentelemetry - importlib.reload(_opentelemetry_tracing) - - def test_no_trace_call(self): - with _opentelemetry_tracing.trace_call("Test", _make_session()) as no_span: - self.assertIsNone(no_span) - - class TestTracing(OpenTelemetryBase): - def test_trace_call(self): - extra_attributes = { - "attribute1": "value1", - # Since our database is mocked, we have to override the db.instance parameter so it is a string - "db.instance": "database_name", +class TestTracing(OpenTelemetryBase): + def test_trace_call(self): + extra_attributes = { + "attribute1": "value1", + # Since our database is mocked, we have to override the db.instance parameter so it is a string + "db.instance": "database_name", + } + + expected_attributes = enrich_with_otel_scope( + { + "db.type": "spanner", + "db.url": "spanner.googleapis.com", + "net.host.name": "spanner.googleapis.com", + "gcp.client.service": "spanner", + "gcp.client.version": LIB_VERSION, + "gcp.client.repo": "googleapis/python-spanner", } + ) + expected_attributes.update(extra_attributes) + + with _opentelemetry_tracing.trace_call( + "CloudSpanner.Test", _make_session(), extra_attributes + ) as span: + span.set_attribute("after_setup_attribute", 1) + + expected_attributes["after_setup_attribute"] = 1 + + span_list = self.ot_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + self.assertEqual(span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(span.attributes, expected_attributes) + self.assertEqual(span.name, "CloudSpanner.Test") + self.assertEqual(span.status.status_code, StatusCode.OK) + + def test_trace_error(self): + extra_attributes = {"db.instance": "database_name"} + + expected_attributes = enrich_with_otel_scope( + { + "db.type": "spanner", + "db.url": "spanner.googleapis.com", + "net.host.name": "spanner.googleapis.com", + "gcp.client.service": "spanner", + "gcp.client.version": LIB_VERSION, + "gcp.client.repo": "googleapis/python-spanner", + } + ) + expected_attributes.update(extra_attributes) - expected_attributes = enrich_with_otel_scope( - { - "db.type": "spanner", - "db.url": "spanner.googleapis.com", - "net.host.name": "spanner.googleapis.com", - "gcp.client.service": "spanner", - "gcp.client.version": LIB_VERSION, - "gcp.client.repo": "googleapis/python-spanner", - } - ) - expected_attributes.update(extra_attributes) - + with self.assertRaises(GoogleAPICallError): with _opentelemetry_tracing.trace_call( "CloudSpanner.Test", _make_session(), extra_attributes ) as span: - span.set_attribute("after_setup_attribute", 1) - - expected_attributes["after_setup_attribute"] = 1 - - span_list = self.ot_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - self.assertEqual(span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual(span.attributes, expected_attributes) - self.assertEqual(span.name, "CloudSpanner.Test") - self.assertEqual(span.status.status_code, StatusCode.OK) - - def test_trace_error(self): - extra_attributes = {"db.instance": "database_name"} - - expected_attributes = enrich_with_otel_scope( - { - "db.type": "spanner", - "db.url": "spanner.googleapis.com", - "net.host.name": "spanner.googleapis.com", - "gcp.client.service": "spanner", - "gcp.client.version": LIB_VERSION, - "gcp.client.repo": "googleapis/python-spanner", - } - ) - expected_attributes.update(extra_attributes) - - with self.assertRaises(GoogleAPICallError): - with _opentelemetry_tracing.trace_call( - "CloudSpanner.Test", _make_session(), extra_attributes - ) as span: - from google.api_core.exceptions import InvalidArgument - - raise _make_rpc_error(InvalidArgument) - - span_list = self.ot_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - self.assertEqual(span.kind, trace_api.SpanKind.CLIENT) - self.assertEqual(dict(span.attributes), expected_attributes) - self.assertEqual(span.name, "CloudSpanner.Test") - self.assertEqual(span.status.status_code, StatusCode.ERROR) - - def test_trace_grpc_error(self): - extra_attributes = {"db.instance": "database_name"} - - expected_attributes = enrich_with_otel_scope( - { - "db.type": "spanner", - "db.url": "spanner.googleapis.com:443", - "net.host.name": "spanner.googleapis.com:443", - } - ) - expected_attributes.update(extra_attributes) - - with self.assertRaises(GoogleAPICallError): - with _opentelemetry_tracing.trace_call( - "CloudSpanner.Test", _make_session(), extra_attributes - ) as span: - from google.api_core.exceptions import DataLoss - - raise DataLoss("error") - - span_list = self.ot_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - self.assertEqual(span.status.status_code, StatusCode.ERROR) - - def test_trace_codeless_error(self): - extra_attributes = {"db.instance": "database_name"} - - expected_attributes = enrich_with_otel_scope( - { - "db.type": "spanner", - "db.url": "spanner.googleapis.com:443", - "net.host.name": "spanner.googleapis.com:443", - } - ) - expected_attributes.update(extra_attributes) - - with self.assertRaises(GoogleAPICallError): - with _opentelemetry_tracing.trace_call( - "CloudSpanner.Test", _make_session(), extra_attributes - ) as span: - raise GoogleAPICallError("error") - - span_list = self.ot_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - self.assertEqual(span.status.status_code, StatusCode.ERROR) - - def test_trace_call_terminal_span_status_ALWAYS_ON_sampler(self): - # Verify that we don't unconditionally set the terminal span status to - # SpanStatus.OK per https://github.com/googleapis/python-spanner/issues/1246 - from opentelemetry.sdk.trace.export import SimpleSpanProcessor - from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, - ) - from opentelemetry.trace.status import Status, StatusCode - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.sampling import ALWAYS_ON - - tracer_provider = TracerProvider(sampler=ALWAYS_ON) - trace_exporter = InMemorySpanExporter() - tracer_provider.add_span_processor(SimpleSpanProcessor(trace_exporter)) - observability_options = dict(tracer_provider=tracer_provider) + from google.api_core.exceptions import InvalidArgument + + raise _make_rpc_error(InvalidArgument) + + span_list = self.ot_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + self.assertEqual(span.kind, trace_api.SpanKind.CLIENT) + self.assertEqual(dict(span.attributes), expected_attributes) + self.assertEqual(span.name, "CloudSpanner.Test") + self.assertEqual(span.status.status_code, StatusCode.ERROR) + + def test_trace_grpc_error(self): + extra_attributes = {"db.instance": "database_name"} + + expected_attributes = enrich_with_otel_scope( + { + "db.type": "spanner", + "db.url": "spanner.googleapis.com:443", + "net.host.name": "spanner.googleapis.com:443", + } + ) + expected_attributes.update(extra_attributes) - session = _make_session() + with self.assertRaises(GoogleAPICallError): with _opentelemetry_tracing.trace_call( - "VerifyTerminalSpanStatus", - session, - observability_options=observability_options, + "CloudSpanner.Test", _make_session(), extra_attributes ) as span: - span.set_status(Status(StatusCode.ERROR, "Our error exhibit")) - - span_list = trace_exporter.get_finished_spans() - got_statuses = [] - - for span in span_list: - got_statuses.append( - (span.name, span.status.status_code, span.status.description) - ) - - want_statuses = [ - ("VerifyTerminalSpanStatus", StatusCode.ERROR, "Our error exhibit"), - ] - assert got_statuses == want_statuses - - def test_trace_call_terminal_span_status_ALWAYS_OFF_sampler(self): - # Verify that we get the correct status even when using the ALWAYS_OFF - # sampler which produces the NonRecordingSpan per - # https://github.com/googleapis/python-spanner/issues/1286 - from opentelemetry.sdk.trace.export import SimpleSpanProcessor - from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, - ) - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.sampling import ALWAYS_OFF + from google.api_core.exceptions import DataLoss + + raise DataLoss("error") - tracer_provider = TracerProvider(sampler=ALWAYS_OFF) - trace_exporter = InMemorySpanExporter() - tracer_provider.add_span_processor(SimpleSpanProcessor(trace_exporter)) - observability_options = dict(tracer_provider=tracer_provider) + span_list = self.ot_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + self.assertEqual(span.status.status_code, StatusCode.ERROR) - session = _make_session() - used_span = None + def test_trace_codeless_error(self): + extra_attributes = {"db.instance": "database_name"} + + expected_attributes = enrich_with_otel_scope( + { + "db.type": "spanner", + "db.url": "spanner.googleapis.com:443", + "net.host.name": "spanner.googleapis.com:443", + } + ) + expected_attributes.update(extra_attributes) + + with self.assertRaises(GoogleAPICallError): with _opentelemetry_tracing.trace_call( - "VerifyWithNonRecordingSpan", - session, - observability_options=observability_options, + "CloudSpanner.Test", _make_session(), extra_attributes ) as span: - used_span = span + raise GoogleAPICallError("error") + + span_list = self.ot_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + self.assertEqual(span.status.status_code, StatusCode.ERROR) + + def test_trace_call_terminal_span_status_ALWAYS_ON_sampler(self): + # Verify that we don't unconditionally set the terminal span status to + # SpanStatus.OK per https://github.com/googleapis/python-spanner/issues/1246 + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, + ) + from opentelemetry.trace.status import Status, StatusCode + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.sampling import ALWAYS_ON + + tracer_provider = TracerProvider(sampler=ALWAYS_ON) + trace_exporter = InMemorySpanExporter() + tracer_provider.add_span_processor(SimpleSpanProcessor(trace_exporter)) + observability_options = dict(tracer_provider=tracer_provider) + + session = _make_session() + with _opentelemetry_tracing.trace_call( + "VerifyTerminalSpanStatus", + session, + observability_options=observability_options, + ) as span: + span.set_status(Status(StatusCode.ERROR, "Our error exhibit")) + + span_list = trace_exporter.get_finished_spans() + got_statuses = [] + + for span in span_list: + got_statuses.append( + (span.name, span.status.status_code, span.status.description) + ) - assert type(used_span).__name__ == "NonRecordingSpan" - span_list = list(trace_exporter.get_finished_spans()) - assert span_list == [] + want_statuses = [ + ("VerifyTerminalSpanStatus", StatusCode.ERROR, "Our error exhibit"), + ] + assert got_statuses == want_statuses + + def test_trace_call_terminal_span_status_ALWAYS_OFF_sampler(self): + # Verify that we get the correct status even when using the ALWAYS_OFF + # sampler which produces the NonRecordingSpan per + # https://github.com/googleapis/python-spanner/issues/1286 + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, + ) + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.sampling import ALWAYS_OFF + + tracer_provider = TracerProvider(sampler=ALWAYS_OFF) + trace_exporter = InMemorySpanExporter() + tracer_provider.add_span_processor(SimpleSpanProcessor(trace_exporter)) + observability_options = dict(tracer_provider=tracer_provider) + + session = _make_session() + used_span = None + with _opentelemetry_tracing.trace_call( + "VerifyWithNonRecordingSpan", + session, + observability_options=observability_options, + ) as span: + used_span = span + + assert type(used_span).__name__ == "NonRecordingSpan" + span_list = list(trace_exporter.get_finished_spans()) + assert span_list == [] diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index d5b9b83478..f081453c11 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -470,35 +470,6 @@ def test_exists_hit(self): ), ) - @mock.patch( - "google.cloud.spanner_v1._opentelemetry_tracing.HAS_OPENTELEMETRY_INSTALLED", - False, - ) - def test_exists_hit_wo_span(self): - session_pb = self._make_session_pb(self.SESSION_NAME) - gax_api = self._make_spanner_api() - gax_api.get_session.return_value = session_pb - database = self._make_database() - database.spanner_api = gax_api - session = self._make_one(database) - session._session_id = self.SESSION_ID - - self.assertTrue(session.exists()) - - gax_api.get_session.assert_called_once_with( - name=self.SESSION_NAME, - metadata=[ - ("google-cloud-resource-prefix", database.name), - ("x-goog-spanner-route-to-leader", "true"), - ( - "x-goog-spanner-request-id", - f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", - ), - ], - ) - - self.assertNoSpans() - def test_exists_miss(self): gax_api = self._make_spanner_api() gax_api.get_session.side_effect = NotFound("testing") @@ -531,34 +502,6 @@ def test_exists_miss(self): ), ) - @mock.patch( - "google.cloud.spanner_v1._opentelemetry_tracing.HAS_OPENTELEMETRY_INSTALLED", - False, - ) - def test_exists_miss_wo_span(self): - gax_api = self._make_spanner_api() - gax_api.get_session.side_effect = NotFound("testing") - database = self._make_database() - database.spanner_api = gax_api - session = self._make_one(database) - session._session_id = self.SESSION_ID - - self.assertFalse(session.exists()) - - gax_api.get_session.assert_called_once_with( - name=self.SESSION_NAME, - metadata=[ - ("google-cloud-resource-prefix", database.name), - ("x-goog-spanner-route-to-leader", "true"), - ( - "x-goog-spanner-request-id", - f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", - ), - ], - ) - - self.assertNoSpans() - def test_exists_error(self): gax_api = self._make_spanner_api() gax_api.get_session.side_effect = Unknown("testing") @@ -612,17 +555,25 @@ def test_ping_hit(self): sql="SELECT 1", ) + req_id = f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1" gax_api.execute_sql.assert_called_once_with( request=request, metadata=[ ("google-cloud-resource-prefix", database.name), ( "x-goog-spanner-request-id", - f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + req_id, ), ], ) + self.assertSpanAttributes( + "CloudSpanner.Session.ping", + attributes=dict( + self.BASE_ATTRIBUTES, x_goog_spanner_request_id=req_id + ), + ) + def test_ping_miss(self): gax_api = self._make_spanner_api() gax_api.execute_sql.side_effect = NotFound("testing") @@ -639,17 +590,26 @@ def test_ping_miss(self): sql="SELECT 1", ) + req_id = f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1" gax_api.execute_sql.assert_called_once_with( request=request, metadata=[ ("google-cloud-resource-prefix", database.name), ( "x-goog-spanner-request-id", - f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + req_id, ), ], ) + self.assertSpanAttributes( + "CloudSpanner.Session.ping", + status=StatusCode.ERROR, + attributes=dict( + self.BASE_ATTRIBUTES, x_goog_spanner_request_id=req_id + ), + ) + def test_ping_error(self): gax_api = self._make_spanner_api() gax_api.execute_sql.side_effect = Unknown("testing") @@ -666,17 +626,26 @@ def test_ping_error(self): sql="SELECT 1", ) + req_id = f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1" gax_api.execute_sql.assert_called_once_with( request=request, metadata=[ ("google-cloud-resource-prefix", database.name), ( "x-goog-spanner-request-id", - f"1.{REQ_RAND_PROCESS_ID}.{database._nth_client_id}.{database._channel_id}.1.1", + req_id, ), ], ) + self.assertSpanAttributes( + "CloudSpanner.Session.ping", + status=StatusCode.ERROR, + attributes=dict( + self.BASE_ATTRIBUTES, x_goog_spanner_request_id=req_id + ), + ) + def test_delete_wo_session_id(self): database = self._make_database() session = self._make_one(database)