Skip to content

Commit e742cf7

Browse files
authored
Merge pull request #202 from lonewolf3739/sqlalchemy-semantic-conv
2 parents f52c88b + b7f8a5b commit e742cf7

File tree

9 files changed

+51
-65
lines changed

9 files changed

+51
-65
lines changed

instrumentation/opentelemetry-instrumentation-sqlalchemy/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Update sqlalchemy instrumentation to follow semantic conventions
6+
([#202](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/202))
7+
58
## Version 0.13b0
69

710
Released 2020-09-17

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
engine = create_engine("sqlite:///:memory:")
3535
SQLAlchemyInstrumentor().instrument(
3636
engine=engine,
37-
service="service-A",
3837
)
3938
4039
API
@@ -66,7 +65,6 @@ def _instrument(self, **kwargs):
6665
**kwargs: Optional arguments
6766
``engine``: a SQLAlchemy engine instance
6867
``tracer_provider``: a TracerProvider, defaults to global
69-
``service``: the name of the service to trace.
7068
7169
Returns:
7270
An instrumented engine if passed in as an argument, None otherwise.
@@ -78,7 +76,6 @@ def _instrument(self, **kwargs):
7876
_get_tracer(
7977
kwargs.get("engine"), kwargs.get("tracer_provider")
8078
),
81-
kwargs.get("service"),
8279
kwargs.get("engine"),
8380
)
8481
return None

instrumentation/opentelemetry-instrumentation-sqlalchemy/src/opentelemetry/instrumentation/sqlalchemy/engine.py

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,9 @@
2424
_PORT = "net.peer.port"
2525
# Database semantic conventions here:
2626
# https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/database.md
27-
_ROWS = "sql.rows" # number of rows returned by a query
2827
_STMT = "db.statement"
29-
_DB = "db.type"
30-
_URL = "db.url"
28+
_DB = "db.name"
29+
_USER = "db.user"
3130

3231

3332
def _normalize_vendor(vendor):
@@ -39,7 +38,7 @@ def _normalize_vendor(vendor):
3938
return "sqlite"
4039

4140
if "postgres" in vendor or vendor == "psycopg2":
42-
return "postgres"
41+
return "postgresql"
4342

4443
return vendor
4544

@@ -58,17 +57,15 @@ def _wrap_create_engine(func, module, args, kwargs):
5857
object that will listen to SQLAlchemy events.
5958
"""
6059
engine = func(*args, **kwargs)
61-
EngineTracer(_get_tracer(engine), None, engine)
60+
EngineTracer(_get_tracer(engine), engine)
6261
return engine
6362

6463

6564
class EngineTracer:
66-
def __init__(self, tracer, service, engine):
65+
def __init__(self, tracer, engine):
6766
self.tracer = tracer
6867
self.engine = engine
6968
self.vendor = _normalize_vendor(engine.name)
70-
self.service = service or self.vendor
71-
self.name = "%s.query" % self.vendor
7269
self.current_span = None
7370

7471
listen(engine, "before_cursor_execute", self._before_cur_exec)
@@ -77,11 +74,11 @@ def __init__(self, tracer, service, engine):
7774

7875
# pylint: disable=unused-argument
7976
def _before_cur_exec(self, conn, cursor, statement, *args):
80-
self.current_span = self.tracer.start_span(self.name)
77+
self.current_span = self.tracer.start_span(statement)
8178
with self.tracer.use_span(self.current_span, end_on_exit=False):
8279
if self.current_span.is_recording():
83-
self.current_span.set_attribute("service", self.vendor)
8480
self.current_span.set_attribute(_STMT, statement)
81+
self.current_span.set_attribute("db.system", self.vendor)
8582

8683
if not _set_attributes_from_url(
8784
self.current_span, conn.engine.url
@@ -94,16 +91,7 @@ def _before_cur_exec(self, conn, cursor, statement, *args):
9491
def _after_cur_exec(self, conn, cursor, statement, *args):
9592
if self.current_span is None:
9693
return
97-
98-
try:
99-
if (
100-
cursor
101-
and cursor.rowcount >= 0
102-
and self.current_span.is_recording()
103-
):
104-
self.current_span.set_attribute(_ROWS, cursor.rowcount)
105-
finally:
106-
self.current_span.end()
94+
self.current_span.end()
10795

10896
def _handle_error(self, context):
10997
if self.current_span is None:
@@ -127,6 +115,8 @@ def _set_attributes_from_url(span: trace.Span, url):
127115
span.set_attribute(_PORT, url.port)
128116
if url.database:
129117
span.set_attribute(_DB, url.database)
118+
if url.username:
119+
span.set_attribute(_USER, url.username)
130120

131121
return bool(url.host)
132122

@@ -135,7 +125,7 @@ def _set_attributes_from_cursor(span: trace.Span, vendor, cursor):
135125
"""Attempt to set db connection attributes by introspecting the cursor."""
136126
if not span.is_recording():
137127
return
138-
if vendor == "postgres":
128+
if vendor == "postgresql":
139129
# pylint: disable=import-outside-toplevel
140130
from psycopg2.extensions import parse_dsn
141131

instrumentation/opentelemetry-instrumentation-sqlalchemy/tests/test_sqlalchemy.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,14 @@ def tearDown(self):
2727
def test_trace_integration(self):
2828
engine = create_engine("sqlite:///:memory:")
2929
SQLAlchemyInstrumentor().instrument(
30-
engine=engine,
31-
tracer_provider=self.tracer_provider,
32-
service="my-database",
30+
engine=engine, tracer_provider=self.tracer_provider,
3331
)
3432
cnx = engine.connect()
3533
cnx.execute("SELECT 1 + 1;").fetchall()
3634
spans = self.memory_exporter.get_finished_spans()
3735

3836
self.assertEqual(len(spans), 1)
39-
self.assertEqual(spans[0].name, "sqlite.query")
37+
self.assertEqual(spans[0].name, "SELECT 1 + 1;")
4038

4139
def test_not_recording(self):
4240
mock_tracer = mock.Mock()
@@ -49,9 +47,7 @@ def test_not_recording(self):
4947
tracer.return_value = mock_tracer
5048
engine = create_engine("sqlite:///:memory:")
5149
SQLAlchemyInstrumentor().instrument(
52-
engine=engine,
53-
tracer_provider=self.tracer_provider,
54-
service="my-database",
50+
engine=engine, tracer_provider=self.tracer_provider,
5551
)
5652
cnx = engine.connect()
5753
cnx.execute("SELECT 1 + 1;").fetchall()
@@ -70,4 +66,4 @@ def test_create_engine_wrapper(self):
7066
spans = self.memory_exporter.get_finished_spans()
7167

7268
self.assertEqual(len(spans), 1)
73-
self.assertEqual(spans[0].name, "sqlite.query")
69+
self.assertEqual(spans[0].name, "SELECT 1 + 1;")

tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/mixins.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from opentelemetry import trace
2222
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
23-
from opentelemetry.instrumentation.sqlalchemy.engine import _DB, _ROWS, _STMT
23+
from opentelemetry.instrumentation.sqlalchemy.engine import _DB, _STMT
2424
from opentelemetry.test.test_base import TestBase
2525

2626
Base = declarative_base()
@@ -109,9 +109,8 @@ def tearDown(self):
109109
SQLAlchemyInstrumentor().uninstrument()
110110
super().tearDown()
111111

112-
def _check_span(self, span):
113-
self.assertEqual(span.name, "{}.query".format(self.VENDOR))
114-
self.assertEqual(span.attributes.get("service"), self.SERVICE)
112+
def _check_span(self, span, name):
113+
self.assertEqual(span.name, name)
115114
self.assertEqual(span.attributes.get(_DB), self.SQL_DB)
116115
self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET)
117116
self.assertGreater((span.end_time - span.start_time), 0)
@@ -125,9 +124,13 @@ def test_orm_insert(self):
125124
spans = self.memory_exporter.get_finished_spans()
126125
self.assertEqual(len(spans), 1)
127126
span = spans[0]
128-
self._check_span(span)
127+
stmt = "INSERT INTO players (id, name) VALUES "
128+
if span.attributes.get("db.system") == "sqlite":
129+
stmt += "(?, ?)"
130+
else:
131+
stmt += "(%(id)s, %(name)s)"
132+
self._check_span(span, stmt)
129133
self.assertIn("INSERT INTO players", span.attributes.get(_STMT))
130-
self.assertEqual(span.attributes.get(_ROWS), 1)
131134
self.check_meta(span)
132135

133136
def test_session_query(self):
@@ -138,7 +141,12 @@ def test_session_query(self):
138141
spans = self.memory_exporter.get_finished_spans()
139142
self.assertEqual(len(spans), 1)
140143
span = spans[0]
141-
self._check_span(span)
144+
stmt = "SELECT players.id AS players_id, players.name AS players_name \nFROM players \nWHERE players.name = "
145+
if span.attributes.get("db.system") == "sqlite":
146+
stmt += "?"
147+
else:
148+
stmt += "%(name_1)s"
149+
self._check_span(span, stmt)
142150
self.assertIn(
143151
"SELECT players.id AS players_id, players.name AS players_name \nFROM players \nWHERE players.name",
144152
span.attributes.get(_STMT),
@@ -147,24 +155,26 @@ def test_session_query(self):
147155

148156
def test_engine_connect_execute(self):
149157
# ensures that engine.connect() is properly traced
158+
stmt = "SELECT * FROM players"
150159
with self.connection() as conn:
151-
rows = conn.execute("SELECT * FROM players").fetchall()
160+
rows = conn.execute(stmt).fetchall()
152161
self.assertEqual(len(rows), 0)
153162

154163
spans = self.memory_exporter.get_finished_spans()
155164
self.assertEqual(len(spans), 1)
156165
span = spans[0]
157-
self._check_span(span)
166+
self._check_span(span, stmt)
158167
self.assertEqual(span.attributes.get(_STMT), "SELECT * FROM players")
159168
self.check_meta(span)
160169

161170
def test_parent(self):
162171
"""Ensure that sqlalchemy works with opentelemetry."""
172+
stmt = "SELECT * FROM players"
163173
tracer = self.tracer_provider.get_tracer("sqlalch_svc")
164174

165175
with tracer.start_as_current_span("sqlalch_op"):
166176
with self.connection() as conn:
167-
rows = conn.execute("SELECT * FROM players").fetchall()
177+
rows = conn.execute(stmt).fetchall()
168178
self.assertEqual(len(rows), 0)
169179

170180
spans = self.memory_exporter.get_finished_spans()
@@ -178,5 +188,4 @@ def test_parent(self):
178188
self.assertEqual(parent_span.name, "sqlalch_op")
179189
self.assertEqual(parent_span.instrumentation_info.name, "sqlalch_svc")
180190

181-
self.assertEqual(child_span.name, "{}.query".format(self.VENDOR))
182-
self.assertEqual(child_span.attributes.get("service"), self.SERVICE)
191+
self.assertEqual(child_span.name, stmt)

tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_instrument.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ def test_engine_traced(self):
6464
self.assertEqual(len(traces), 1)
6565
span = traces[0]
6666
# check subset of span fields
67-
self.assertEqual(span.name, "postgres.query")
68-
self.assertEqual(span.attributes.get("service"), "postgres")
67+
self.assertEqual(span.name, "SELECT 1")
6968
self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET)
7069
self.assertGreater((span.end_time - span.start_time), 0)

tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_mysql.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
_DB,
2424
_HOST,
2525
_PORT,
26-
_ROWS,
2726
_STMT,
27+
_USER,
2828
)
2929

3030
from .mixins import SQLAlchemyTestMixin
@@ -45,7 +45,6 @@ class MysqlConnectorTestCase(SQLAlchemyTestMixin):
4545

4646
VENDOR = "mysql"
4747
SQL_DB = "opentelemetry-tests"
48-
SERVICE = "mysql"
4948
ENGINE_ARGS = {
5049
"url": "mysql+mysqlconnector://%(user)s:%(password)s@%(host)s:%(port)s/%(database)s"
5150
% MYSQL_CONFIG
@@ -55,6 +54,8 @@ def check_meta(self, span):
5554
# check database connection tags
5655
self.assertEqual(span.attributes.get(_HOST), MYSQL_CONFIG["host"])
5756
self.assertEqual(span.attributes.get(_PORT), MYSQL_CONFIG["port"])
57+
self.assertEqual(span.attributes.get(_DB), MYSQL_CONFIG["database"])
58+
self.assertEqual(span.attributes.get(_USER), MYSQL_CONFIG["user"])
5859

5960
def test_engine_execute_errors(self):
6061
# ensures that SQL errors are reported
@@ -66,13 +67,11 @@ def test_engine_execute_errors(self):
6667
self.assertEqual(len(spans), 1)
6768
span = spans[0]
6869
# span fields
69-
self.assertEqual(span.name, "{}.query".format(self.VENDOR))
70-
self.assertEqual(span.attributes.get("service"), self.SERVICE)
70+
self.assertEqual(span.name, "SELECT * FROM a_wrong_table")
7171
self.assertEqual(
7272
span.attributes.get(_STMT), "SELECT * FROM a_wrong_table"
7373
)
7474
self.assertEqual(span.attributes.get(_DB), self.SQL_DB)
75-
self.assertIsNone(span.attributes.get(_ROWS))
7675
self.check_meta(span)
7776
self.assertTrue(span.end_time - span.start_time > 0)
7877
# check the error

tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_postgres.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
_DB,
2525
_HOST,
2626
_PORT,
27-
_ROWS,
2827
_STMT,
2928
)
3029

@@ -44,9 +43,8 @@ class PostgresTestCase(SQLAlchemyTestMixin):
4443

4544
__test__ = True
4645

47-
VENDOR = "postgres"
46+
VENDOR = "postgresql"
4847
SQL_DB = "opentelemetry-tests"
49-
SERVICE = "postgres"
5048
ENGINE_ARGS = {
5149
"url": "postgresql://%(user)s:%(password)s@%(host)s:%(port)s/%(dbname)s"
5250
% POSTGRES_CONFIG
@@ -67,13 +65,11 @@ def test_engine_execute_errors(self):
6765
self.assertEqual(len(spans), 1)
6866
span = spans[0]
6967
# span fields
70-
self.assertEqual(span.name, "{}.query".format(self.VENDOR))
71-
self.assertEqual(span.attributes.get("service"), self.SERVICE)
68+
self.assertEqual(span.name, "SELECT * FROM a_wrong_table")
7269
self.assertEqual(
7370
span.attributes.get(_STMT), "SELECT * FROM a_wrong_table"
7471
)
7572
self.assertEqual(span.attributes.get(_DB), self.SQL_DB)
76-
self.assertIsNone(span.attributes.get(_ROWS))
7773
self.check_meta(span)
7874
self.assertTrue(span.end_time - span.start_time > 0)
7975
# check the error
@@ -88,9 +84,8 @@ class PostgresCreatorTestCase(PostgresTestCase):
8884
of `PostgresTestCase`, but it uses a specific `creator` function.
8985
"""
9086

91-
VENDOR = "postgres"
87+
VENDOR = "postgresql"
9288
SQL_DB = "opentelemetry-tests"
93-
SERVICE = "postgres"
9489
ENGINE_ARGS = {
9590
"url": "postgresql://",
9691
"creator": lambda: psycopg2.connect(**POSTGRES_CONFIG),

tests/opentelemetry-docker-tests/tests/sqlalchemy_tests/test_sqlite.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from sqlalchemy.exc import OperationalError
1919

2020
from opentelemetry import trace
21-
from opentelemetry.instrumentation.sqlalchemy.engine import _DB, _ROWS, _STMT
21+
from opentelemetry.instrumentation.sqlalchemy.engine import _DB, _STMT
2222

2323
from .mixins import SQLAlchemyTestMixin
2424

@@ -30,26 +30,24 @@ class SQLiteTestCase(SQLAlchemyTestMixin):
3030

3131
VENDOR = "sqlite"
3232
SQL_DB = ":memory:"
33-
SERVICE = "sqlite"
3433
ENGINE_ARGS = {"url": "sqlite:///:memory:"}
3534

3635
def test_engine_execute_errors(self):
3736
# ensures that SQL errors are reported
37+
stmt = "SELECT * FROM a_wrong_table"
3838
with pytest.raises(OperationalError):
3939
with self.connection() as conn:
40-
conn.execute("SELECT * FROM a_wrong_table").fetchall()
40+
conn.execute(stmt).fetchall()
4141

4242
spans = self.memory_exporter.get_finished_spans()
4343
self.assertEqual(len(spans), 1)
4444
span = spans[0]
4545
# span fields
46-
self.assertEqual(span.name, "{}.query".format(self.VENDOR))
47-
self.assertEqual(span.attributes.get("service"), self.SERVICE)
46+
self.assertEqual(span.name, stmt)
4847
self.assertEqual(
4948
span.attributes.get(_STMT), "SELECT * FROM a_wrong_table"
5049
)
5150
self.assertEqual(span.attributes.get(_DB), self.SQL_DB)
52-
self.assertIsNone(span.attributes.get(_ROWS))
5351
self.assertTrue((span.end_time - span.start_time) > 0)
5452
# check the error
5553
self.assertIs(

0 commit comments

Comments
 (0)