diff --git a/google/cloud/spanner_v1/_opentelemetry_tracing.py b/google/cloud/spanner_v1/_opentelemetry_tracing.py index 5c1a487012..86a9fb7c51 100644 --- a/google/cloud/spanner_v1/_opentelemetry_tracing.py +++ b/google/cloud/spanner_v1/_opentelemetry_tracing.py @@ -1,4 +1,4 @@ -# Copyright 2016 Google LLC All rights reserved. +# Copyright 2020 Google LLC All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/google/cloud/spanner_v1/session.py b/google/cloud/spanner_v1/session.py index d05930389b..b3a1b7e6d8 100644 --- a/google/cloud/spanner_v1/session.py +++ b/google/cloud/spanner_v1/session.py @@ -139,9 +139,11 @@ def exists(self): with trace_call("CloudSpanner.GetSession", self) as span: try: api.get_session(self.name, metadata=metadata) - span.set_attribute("session_found", True) + if span: + span.set_attribute("session_found", True) except NotFound: - span.set_attribute("session_found", False) + if span: + span.set_attribute("session_found", False) return False return True diff --git a/noxfile.py b/noxfile.py index aa6f2ef7e8..91de61a9de 100644 --- a/noxfile.py +++ b/noxfile.py @@ -67,10 +67,11 @@ def default(session): # Install all test dependencies, then install this package in-place. session.install("mock", "pytest", "pytest-cov") - # Install opentelemetry dependencies - session.install( - "opentelemetry-api", "opentelemetry-sdk", "opentelemetry-instrumentation" - ) + # Install opentelemetry dependencies if python3+ + if session.python != "2.7": + session.install( + "opentelemetry-api", "opentelemetry-sdk", "opentelemetry-instrumentation" + ) session.install("-e", ".") @@ -89,13 +90,13 @@ def default(session): ) -@nox.session(python=["3.5", "3.6", "3.7", "3.8"]) +@nox.session(python=["2.7", "3.5", "3.6", "3.7", "3.8"]) def unit(session): """Run the unit test suite.""" default(session) -@nox.session(python="3.7") +@nox.session(python=["2.7", "3.7"]) def system(session): """Run the system test suite.""" system_test_path = os.path.join("tests", "system.py") @@ -121,10 +122,11 @@ def system(session): # virtualenv's dist-packages. session.install("mock", "pytest") - # Install opentelemetry dependencies - session.install( - "opentelemetry-api", "opentelemetry-sdk", "opentelemetry-instrumentation" - ) + # Install opentelemetry dependencies if not 2.7 + if session.python != "2.7": + session.install( + "opentelemetry-api", "opentelemetry-sdk", "opentelemetry-instrumentation" + ) session.install("-e", ".") session.install("-e", "test_utils/") diff --git a/tests/_helpers.py b/tests/_helpers.py index f0cf298a2b..2b013d8108 100644 --- a/tests/_helpers.py +++ b/tests/_helpers.py @@ -1,35 +1,47 @@ import unittest -from opentelemetry import trace as trace_api -from opentelemetry.trace.status import StatusCanonicalCode +from unittest import mock -from opentelemetry.sdk.trace import TracerProvider, export -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +try: + from opentelemetry import trace as trace_api + from opentelemetry.trace.status import StatusCanonicalCode + from opentelemetry.sdk.trace import TracerProvider, export + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + + HAS_OPENTELEMETRY_INSTALLED = True +except ImportError: + HAS_OPENTELEMETRY_INSTALLED = False + + StatusCanonicalCode = mock.Mock() class OpenTelemetryBase(unittest.TestCase): def setUp(self): - self.original_tracer_provider = trace_api.get_tracer_provider() - self.tracer_provider = TracerProvider() - self.memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) - self.tracer_provider.add_span_processor(span_processor) - trace_api.set_tracer_provider(self.tracer_provider) + if HAS_OPENTELEMETRY_INSTALLED: + self.original_tracer_provider = trace_api.get_tracer_provider() + self.tracer_provider = TracerProvider() + self.memory_exporter = InMemorySpanExporter() + span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) + self.tracer_provider.add_span_processor(span_processor) + trace_api.set_tracer_provider(self.tracer_provider) def tearDown(self): - trace_api.set_tracer_provider(self.original_tracer_provider) + if HAS_OPENTELEMETRY_INSTALLED: + trace_api.set_tracer_provider(self.original_tracer_provider) def assertNoSpans(self): - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) + if HAS_OPENTELEMETRY_INSTALLED: + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 0) def assertSpanAttributes( self, name, status=StatusCanonicalCode.OK, attributes=None, span=None ): - if not span: - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - print(status, attributes, span.status, span.attributes) - self.assertEqual(span.name, name) - self.assertEqual(span.status.canonical_code, status) - self.assertEqual(span.attributes, attributes) + if HAS_OPENTELEMETRY_INSTALLED: + if not span: + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + + self.assertEqual(span.name, name) + self.assertEqual(span.status.canonical_code, status) + self.assertEqual(span.attributes, attributes) diff --git a/tests/system/test_system.py b/tests/system/test_system.py index 27f55d5308..4855e5a784 100644 --- a/tests/system/test_system.py +++ b/tests/system/test_system.py @@ -1332,8 +1332,10 @@ def test_transaction_batch_update_wo_statements(self): transaction.batch_update([]) def test_transaction_batch_update_w_parent_span(self): - import sys - from opentelemetry import trace + try: + from opentelemetry import trace + except ImportError: + return tracer = trace.get_tracer(__name__) diff --git a/tests/unit/test__opentelemetry_tracing.py b/tests/unit/test__opentelemetry_tracing.py index 83a0cf2221..85d27a3553 100644 --- a/tests/unit/test__opentelemetry_tracing.py +++ b/tests/unit/test__opentelemetry_tracing.py @@ -3,27 +3,18 @@ import unittest import sys -from opentelemetry import trace as trace_api -from opentelemetry.trace.status import StatusCanonicalCode -from opentelemetry.sdk.trace import TracerProvider, export -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +try: + from opentelemetry import trace as trace_api + from opentelemetry.trace.status import StatusCanonicalCode + from opentelemetry.sdk.trace import TracerProvider, export + from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +except ImportError: + pass from google.api_core.exceptions import GoogleAPICallError from google.cloud.spanner_v1 import _opentelemetry_tracing - -class OpenTelemetryBase(unittest.TestCase): - def setUp(self): - self.original_tracer_provider = trace_api.get_tracer_provider() - self.tracer_provider = TracerProvider() - self.memory_exporter = InMemorySpanExporter() - span_processor = export.SimpleExportSpanProcessor(self.memory_exporter) - self.tracer_provider.add_span_processor(span_processor) - trace_api.set_tracer_provider(self.tracer_provider) - - def tearDown(self): - trace_api.set_tracer_provider(self.original_tracer_provider) - +from tests._helpers import OpenTelemetryBase, HAS_OPENTELEMETRY_INSTALLED def _make_rpc_error(error_cls, trailing_metadata=None): import grpc @@ -38,102 +29,103 @@ 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", + } + + expected_attributes = { + "db.type": "spanner", + "db.url": "spanner.googleapis.com:443", + "net.host.name": "spanner.googleapis.com:443", + } + expected_attributes.update(extra_attributes) -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", - } - - expected_attributes = { - "db.type": "spanner", - "db.url": "spanner.googleapis.com:443", - "net.host.name": "spanner.googleapis.com:443", - } - 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.memory_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.canonical_code, trace_api.status.StatusCanonicalCode.OK - ) - - def test_trace_error(self): - extra_attributes = {"db.instance": "database_name"} - - expected_attributes = { - "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 InvalidArgument - - raise _make_rpc_error(InvalidArgument) - - span_list = self.memory_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.canonical_code, StatusCanonicalCode.INVALID_ARGUMENT - ) - - def test_trace_grpc_error(self): - extra_attributes = {"db.instance": "database_name"} - - expected_attributes = { - "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 _make_rpc_error(DataLoss) - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 1) - span = span_list[0] - self.assertEqual(span.status.canonical_code, StatusCanonicalCode.DATA_LOSS) + span.set_attribute("after_setup_attribute", 1) + + expected_attributes["after_setup_attribute"] = 1 + + span_list = self.memory_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.canonical_code, trace_api.status.StatusCanonicalCode.OK + ) + + def test_trace_error(self): + extra_attributes = {"db.instance": "database_name"} + + expected_attributes = { + "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 InvalidArgument + + raise _make_rpc_error(InvalidArgument) + + span_list = self.memory_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.canonical_code, StatusCanonicalCode.INVALID_ARGUMENT + ) + + def test_trace_grpc_error(self): + extra_attributes = {"db.instance": "database_name"} + + expected_attributes = { + "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 _make_rpc_error(DataLoss) + + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 1) + span = span_list[0] + self.assertEqual(span.status.canonical_code, StatusCanonicalCode.DATA_LOSS) diff --git a/tests/unit/test_batch.py b/tests/unit/test_batch.py index 291b8e4c32..9b831f4906 100644 --- a/tests/unit/test_batch.py +++ b/tests/unit/test_batch.py @@ -14,8 +14,7 @@ import unittest -from tests._helpers import OpenTelemetryBase -from opentelemetry.trace.status import StatusCanonicalCode +from tests._helpers import OpenTelemetryBase, StatusCanonicalCode TABLE_NAME = "citizens" COLUMNS = ["email", "first_name", "last_name", "age"] diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index ff0118206b..17b2ce4688 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -15,8 +15,7 @@ import google.api_core.gapic_v1.method import mock -from tests._helpers import OpenTelemetryBase -from opentelemetry.trace.status import StatusCanonicalCode +from tests._helpers import OpenTelemetryBase, StatusCanonicalCode, HAS_OPENTELEMETRY_INSTALLED def _make_rpc_error(error_cls, trailing_metadata=None): @@ -1109,7 +1108,12 @@ def _time(_results=[1, 1.5]): return _results.pop(0) with mock.patch("time.time", _time): - with mock.patch("opentelemetry.util.time", _ConstantTime()): + if HAS_OPENTELEMETRY_INSTALLED: + with mock.patch("opentelemetry.util.time", _ConstantTime()): + with mock.patch("time.sleep") as sleep_mock: + with self.assertRaises(Aborted): + session.run_in_transaction(unit_of_work, "abc", timeout_secs=1) + else: with mock.patch("time.sleep") as sleep_mock: with self.assertRaises(Aborted): session.run_in_transaction(unit_of_work, "abc", timeout_secs=1) @@ -1172,7 +1176,12 @@ def _time(_results=[1, 2, 4, 8]): return _results.pop(0) with mock.patch("time.time", _time): - with mock.patch("opentelemetry.util.time", _ConstantTime()): + if HAS_OPENTELEMETRY_INSTALLED: + with mock.patch("opentelemetry.util.time", _ConstantTime()): + with mock.patch("time.sleep") as sleep_mock: + with self.assertRaises(Aborted): + session.run_in_transaction(unit_of_work, timeout_secs=8) + else: with mock.patch("time.sleep") as sleep_mock: with self.assertRaises(Aborted): session.run_in_transaction(unit_of_work, timeout_secs=8) diff --git a/tests/unit/test_snapshot.py b/tests/unit/test_snapshot.py index 4e1dd80f48..17e10c7a9e 100644 --- a/tests/unit/test_snapshot.py +++ b/tests/unit/test_snapshot.py @@ -15,8 +15,7 @@ import google.api_core.gapic_v1.method import mock -from opentelemetry.trace.status import StatusCanonicalCode -from tests._helpers import OpenTelemetryBase +from tests._helpers import OpenTelemetryBase, StatusCanonicalCode, HAS_OPENTELEMETRY_INSTALLED TABLE_NAME = "citizens" COLUMNS = ["email", "first_name", "last_name", "age"] @@ -133,33 +132,34 @@ def test_iteration_w_span_creation(self): self.assertSpanAttributes(name, attributes=dict(BASE_ATTRIBUTES, test_att=1)) def test_iteration_w_multiple_span_creation(self): - FIRST = (self._make_item(0), self._make_item(1, resume_token=RESUME_TOKEN)) - SECOND = (self._make_item(2),) # discarded after 503 - LAST = (self._make_item(3),) - before = _MockIterator(*(FIRST + SECOND), fail_after=True) - after = _MockIterator(*LAST) - restart = mock.Mock(spec=[], side_effect=[before, after]) - name = "TestSpan" - resumable = self._call_fut(restart, name, _Session(_Database())) - self.assertEqual(list(resumable), list(FIRST + LAST)) - self.assertEqual( - restart.mock_calls, [mock.call(), mock.call(resume_token=RESUME_TOKEN)] - ) - - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 2) - for span in span_list: - self.assertEqual(span.name, name) + if HAS_OPENTELEMETRY_INSTALLED: + FIRST = (self._make_item(0), self._make_item(1, resume_token=RESUME_TOKEN)) + SECOND = (self._make_item(2),) # discarded after 503 + LAST = (self._make_item(3),) + before = _MockIterator(*(FIRST + SECOND), fail_after=True) + after = _MockIterator(*LAST) + restart = mock.Mock(spec=[], side_effect=[before, after]) + name = "TestSpan" + resumable = self._call_fut(restart, name, _Session(_Database())) + self.assertEqual(list(resumable), list(FIRST + LAST)) self.assertEqual( - span.attributes, - { - "db.type": "spanner", - "db.url": "spanner.googleapis.com:443", - "db.instance": "testing", - "net.host.name": "spanner.googleapis.com:443", - }, + restart.mock_calls, [mock.call(), mock.call(resume_token=RESUME_TOKEN)] ) + span_list = self.memory_exporter.get_finished_spans() + self.assertEqual(len(span_list), 2) + for span in span_list: + self.assertEqual(span.name, name) + self.assertEqual( + span.attributes, + { + "db.type": "spanner", + "db.url": "spanner.googleapis.com:443", + "db.instance": "testing", + "net.host.name": "spanner.googleapis.com:443", + }, + ) + class Test_SnapshotBase(OpenTelemetryBase): @@ -215,8 +215,7 @@ def test_ctor(self): self.assertIs(base._session, session) self.assertEqual(base._execute_sql_count, 0) - span_list = self.memory_exporter.get_finished_spans() - self.assertEqual(len(span_list), 0) + self.assertNoSpans() def test__make_txn_selector_virtual(self): session = _Session() diff --git a/tests/unit/test_transaction.py b/tests/unit/test_transaction.py index 939242e6bf..e2ac7c2eec 100644 --- a/tests/unit/test_transaction.py +++ b/tests/unit/test_transaction.py @@ -14,8 +14,7 @@ import mock -from tests._helpers import OpenTelemetryBase -from opentelemetry.trace.status import StatusCanonicalCode +from tests._helpers import OpenTelemetryBase, StatusCanonicalCode TABLE_NAME = "citizens" COLUMNS = ["email", "first_name", "last_name", "age"]