Skip to content
This repository was archived by the owner on Mar 13, 2026. It is now read-only.

Commit c0d39cc

Browse files
authored
fix: for open-telemetry status code spec change (#88)
* fix: for open-telemetry status code spec change * fix: updated open telemetry version * refactor: lint fixes * refactor: removed commented and unused line * refactor: change OpenTelemetryBase to inheri unittest.TestCase * refactor: lint corrections * refactor: lint correction * refactor: lint fixes * test: add separate unit test session in noxfile * refactor: move open telemetry test inside test/unit tests
1 parent f34da81 commit c0d39cc

File tree

6 files changed

+217
-23
lines changed

6 files changed

+217
-23
lines changed

google/cloud/sqlalchemy_spanner/_opentelemetry_tracing.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,10 @@
1818

1919
from google.api_core.exceptions import GoogleAPICallError
2020
from google.cloud.spanner_v1 import SpannerClient
21-
from google.cloud.spanner_dbapi.exceptions import IntegrityError
22-
from google.cloud.spanner_dbapi.exceptions import InterfaceError
23-
from google.cloud.spanner_dbapi.exceptions import OperationalError
24-
from google.cloud.spanner_dbapi.exceptions import ProgrammingError
2521

2622
try:
2723
from opentelemetry import trace
28-
from opentelemetry.trace.status import Status, StatusCanonicalCode
29-
from opentelemetry.instrumentation.utils import http_status_to_canonical_code
24+
from opentelemetry.trace.status import Status, StatusCode
3025

3126
HAS_OPENTELEMETRY_INSTALLED = True
3227
except ImportError:
@@ -45,6 +40,7 @@ def trace_call(name, extra_attributes=None):
4540
# Set base attributes that we know for every trace created
4641
attributes = {
4742
"db.type": "spanner",
43+
"db.engine": "sqlalchemy_spanner",
4844
"db.url": SpannerClient.DEFAULT_ENDPOINT,
4945
"net.host.name": SpannerClient.DEFAULT_ENDPOINT,
5046
}
@@ -56,20 +52,9 @@ def trace_call(name, extra_attributes=None):
5652
name, kind=trace.SpanKind.CLIENT, attributes=attributes
5753
) as span:
5854
try:
55+
span.set_status(Status(StatusCode.OK))
5956
yield span
60-
except (ValueError, InterfaceError) as e:
61-
span.set_status(Status(StatusCanonicalCode.UNKNOWN, e.args[0]))
6257
except GoogleAPICallError as error:
63-
if error.code is not None:
64-
span.set_status(Status(http_status_to_canonical_code(error.code)))
65-
elif error.grpc_status_code is not None:
66-
span.set_status(
67-
# OpenTelemetry's StatusCanonicalCode maps 1-1 with grpc status
68-
# codes
69-
Status(StatusCanonicalCode(error.grpc_status_code.value[0]))
70-
)
58+
span.set_status(Status(StatusCode.ERROR))
59+
span.record_exception(error)
7160
raise
72-
except (IntegrityError, ProgrammingError, OperationalError) as e:
73-
span.set_status(
74-
Status(http_status_to_canonical_code(e.args[0].code), e.args[0].message)
75-
)

noxfile.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import configparser
2020
import nox
21+
import os
2122

2223
ALEMBIC_CONF = """
2324
[alembic]
@@ -110,11 +111,25 @@ def lint_setup_py(session):
110111
def compliance_test(session):
111112
"""Run SQLAlchemy dialect compliance test suite."""
112113
session.install("pytest")
114+
session.install("mock")
113115
session.install("-e", ".")
114116
session.run("python", "create_test_database.py")
115117
session.run("pytest", "-v")
116118

117119

120+
@nox.session(python=DEFAULT_PYTHON_VERSION)
121+
def unit(session):
122+
"""Run unit tests."""
123+
# Run SQLAlchemy dialect compliance test suite with OpenTelemetry.
124+
session.install("pytest")
125+
session.install("mock")
126+
session.install("-e", ".")
127+
session.install("opentelemetry-api==1.1.0")
128+
session.install("opentelemetry-sdk==1.1.0")
129+
session.install("opentelemetry-instrumentation==0.20b0")
130+
session.run("py.test", "--quiet", os.path.join("test/unit"), *session.posargs)
131+
132+
118133
@nox.session(python=DEFAULT_PYTHON_VERSION)
119134
def migration_test(session):
120135
"""Migrate with SQLAlchemy and Alembic and check the result."""

setup.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
dependencies = ["sqlalchemy>=1.1.13, <=1.3.23", "google-cloud-spanner>=3.3.0"]
2323
extras = {
2424
"tracing": [
25-
"opentelemetry-api==0.11b0",
26-
"opentelemetry-sdk==0.11b0",
27-
"opentelemetry-instrumentation==0.11b0",
25+
"opentelemetry-api >= 1.1.0",
26+
"opentelemetry-sdk >= 1.1.0",
27+
"opentelemetry-instrumentation >= 0.20b0",
2828
]
2929
}
3030

test/__init__.py

Whitespace-only changes.

test/_helpers.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
7+
8+
import mock
9+
from sqlalchemy.testing import fixtures
10+
11+
try:
12+
from opentelemetry import trace
13+
from opentelemetry.sdk.trace import TracerProvider
14+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
15+
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
16+
InMemorySpanExporter,
17+
)
18+
from opentelemetry.trace.status import StatusCode
19+
20+
trace.set_tracer_provider(TracerProvider())
21+
22+
HAS_OPENTELEMETRY_INSTALLED = True
23+
except ImportError:
24+
HAS_OPENTELEMETRY_INSTALLED = False
25+
26+
StatusCode = mock.Mock()
27+
28+
_TEST_OT_EXPORTER = None
29+
_TEST_OT_PROVIDER_INITIALIZED = False
30+
31+
32+
def get_test_ot_exporter():
33+
global _TEST_OT_EXPORTER
34+
35+
if _TEST_OT_EXPORTER is None:
36+
_TEST_OT_EXPORTER = InMemorySpanExporter()
37+
return _TEST_OT_EXPORTER
38+
39+
40+
def use_test_ot_exporter():
41+
global _TEST_OT_PROVIDER_INITIALIZED
42+
43+
if _TEST_OT_PROVIDER_INITIALIZED:
44+
return
45+
46+
provider = trace.get_tracer_provider()
47+
if not hasattr(provider, "add_span_processor"):
48+
return
49+
provider.add_span_processor(SimpleSpanProcessor(get_test_ot_exporter()))
50+
_TEST_OT_PROVIDER_INITIALIZED = True
51+
52+
53+
class OpenTelemetryBase(fixtures.TestBase):
54+
@classmethod
55+
def setup_class(cls):
56+
if HAS_OPENTELEMETRY_INSTALLED:
57+
use_test_ot_exporter()
58+
cls.ot_exporter = get_test_ot_exporter()
59+
60+
def teardown(self):
61+
if HAS_OPENTELEMETRY_INSTALLED:
62+
self.ot_exporter.clear()
63+
64+
def assertNoSpans(self):
65+
if HAS_OPENTELEMETRY_INSTALLED:
66+
span_list = self.ot_exporter.get_finished_spans()
67+
self.assertEqual(len(span_list), 0)
68+
69+
def assertSpanAttributes(
70+
self, name, status=StatusCode.OK, attributes=None, span=None
71+
):
72+
if HAS_OPENTELEMETRY_INSTALLED:
73+
if not span:
74+
span_list = self.ot_exporter.get_finished_spans()
75+
self.assertEqual(len(span_list), 1)
76+
span = span_list[0]
77+
78+
self.assertEqual(span.name, name)
79+
self.assertEqual(span.status.status_code, status)
80+
self.assertEqual(dict(span.attributes), attributes)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright 2021 Google LLC
2+
#
3+
# Use of this source code is governed by a BSD-style
4+
# license that can be found in the LICENSE file or at
5+
# https://developers.google.com/open-source/licenses/bsd
6+
7+
import importlib
8+
import mock
9+
import pytest
10+
import sys
11+
12+
try:
13+
from opentelemetry import trace as trace_api
14+
from opentelemetry.trace.status import StatusCode
15+
except ImportError:
16+
pass
17+
18+
from google.api_core.exceptions import GoogleAPICallError
19+
from google.cloud.spanner_v1 import SpannerClient
20+
from google.cloud.sqlalchemy_spanner import _opentelemetry_tracing
21+
from test._helpers import OpenTelemetryBase, HAS_OPENTELEMETRY_INSTALLED
22+
23+
24+
def _make_rpc_error(error_cls, trailing_metadata=None):
25+
import grpc
26+
27+
grpc_error = mock.create_autospec(grpc.Call, instance=True)
28+
grpc_error.trailing_metadata.return_value = trailing_metadata
29+
return error_cls("error", errors=(grpc_error,))
30+
31+
32+
# Skip all of these tests if we don't have OpenTelemetry
33+
if HAS_OPENTELEMETRY_INSTALLED:
34+
35+
class TestNoTracing(OpenTelemetryBase):
36+
def setup(self):
37+
self._temp_opentelemetry = sys.modules["opentelemetry"]
38+
39+
sys.modules["opentelemetry"] = None
40+
importlib.reload(_opentelemetry_tracing)
41+
42+
def teardown(self):
43+
sys.modules["opentelemetry"] = self._temp_opentelemetry
44+
importlib.reload(_opentelemetry_tracing)
45+
46+
def test_no_trace_call(self):
47+
with _opentelemetry_tracing.trace_call("Test") as no_span:
48+
assert no_span is None
49+
50+
class TestTracing(OpenTelemetryBase):
51+
def test_trace_call(self):
52+
extra_attributes = {
53+
"attribute1": "value1",
54+
# Since our database is mocked, we have to override the
55+
# db.instance parameter so it is a string.
56+
"db.instance": "database_name",
57+
}
58+
59+
expected_attributes = {
60+
"db.type": "spanner",
61+
"db.engine": "sqlalchemy_spanner",
62+
"db.url": SpannerClient.DEFAULT_ENDPOINT,
63+
"net.host.name": SpannerClient.DEFAULT_ENDPOINT,
64+
}
65+
expected_attributes.update(extra_attributes)
66+
67+
with _opentelemetry_tracing.trace_call(
68+
"CloudSpannerSqlAlchemy.Test", extra_attributes
69+
) as span:
70+
span.set_attribute("after_setup_attribute", 1)
71+
72+
expected_attributes["after_setup_attribute"] = 1
73+
74+
span_list = self.ot_exporter.get_finished_spans()
75+
assert len(span_list) == 1
76+
77+
span = span_list[0]
78+
assert span.kind == trace_api.SpanKind.CLIENT
79+
span_attr = dict(span.attributes)
80+
for key in expected_attributes:
81+
assert key in span_attr
82+
assert span_attr[key] == expected_attributes[key]
83+
assert span.name == "CloudSpannerSqlAlchemy.Test"
84+
assert span.status.status_code == StatusCode.OK
85+
86+
def test_trace_error(self):
87+
extra_attributes = {"db.instance": "database_name"}
88+
89+
expected_attributes = {
90+
"db.type": "spanner",
91+
"db.engine": "sqlalchemy_spanner",
92+
"db.url": SpannerClient.DEFAULT_ENDPOINT,
93+
"net.host.name": SpannerClient.DEFAULT_ENDPOINT,
94+
}
95+
expected_attributes.update(extra_attributes)
96+
97+
with pytest.raises(GoogleAPICallError):
98+
with _opentelemetry_tracing.trace_call(
99+
"CloudSpannerSqlAlchemy.Test", extra_attributes,
100+
) as span:
101+
from google.api_core.exceptions import InvalidArgument
102+
103+
raise _make_rpc_error(InvalidArgument)
104+
105+
span_list = self.ot_exporter.get_finished_spans()
106+
assert len(span_list) == 1
107+
span = span_list[0]
108+
assert span.kind == trace_api.SpanKind.CLIENT
109+
span_attr = dict(span.attributes)
110+
for key in expected_attributes:
111+
assert key in span_attr
112+
assert span_attr[key] == expected_attributes[key]
113+
assert span.name == "CloudSpannerSqlAlchemy.Test"
114+
assert span.status.status_code == StatusCode.ERROR

0 commit comments

Comments
 (0)