Skip to content

Commit 8c8f278

Browse files
authored
SQLAlchemy: Use SQL operation and DB name as the Span name (#254)
Current instrumentation uses the entire SQL query as the operation name which makes traces very hard to read and understand in addition to introducing high-cardinality issues. This commit fixes the problem by using only the SQL operation name and the DB name instead of the entire query.
1 parent d12f67f commit 8c8f278

File tree

8 files changed

+59
-41
lines changed

8 files changed

+59
-41
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4242

4343
### Changed
4444
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-wsgi` Return `None` for `CarrierGetter` if key not found
45-
([#1374](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/233))
45+
([#233](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/233))
4646
- `opentelemetry-instrumentation-grpc` Comply with updated spec, rework tests
4747
([#236](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/236))
4848
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-falcon`, `opentelemetry-instrumentation-flask`, `opentelemetry-instrumentation-pyramid`, `opentelemetry-instrumentation-wsgi` Renamed `host.port` attribute to `net.host.port`
@@ -57,6 +57,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
5757
([#235](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/235))
5858
- `opentelemetry-exporter-datadog` `opentelemetry-sdk-extension-aws` Fix reference to ids_generator in sdk
5959
([#235](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/235))
60+
- `opentelemetry-instrumentation-sqlalchemy` Use SQL operation and DB name as span name.
61+
([#254](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/254))
6062

6163
## [0.16b1](https://github.com/open-telemetry/opentelemetry-python-contrib/releases/tag/v0.16b1) - 2020-11-26
6264

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

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from sqlalchemy.event import listen
15+
from sqlalchemy.event import listen # pylint: disable=no-name-in-module
1616

1717
from opentelemetry import trace
1818
from opentelemetry.instrumentation.sqlalchemy.version import __version__
@@ -72,22 +72,38 @@ def __init__(self, tracer, engine):
7272
listen(engine, "after_cursor_execute", self._after_cur_exec)
7373
listen(engine, "handle_error", self._handle_error)
7474

75+
def _operation_name(self, db_name, statement):
76+
parts = []
77+
if isinstance(statement, str):
78+
# otel spec recommends against parsing SQL queries. We are not trying to parse SQL
79+
# but simply truncating the statement to the first word. This covers probably >95%
80+
# use cases and uses the SQL statement in span name correctly as per the spec.
81+
# For some very special cases it might not record the correct statement if the SQL
82+
# dialect is too weird but in any case it shouldn't break anything.
83+
parts.append(statement.split()[0])
84+
if db_name:
85+
parts.append(db_name)
86+
if not parts:
87+
return self.vendor
88+
return " ".join(parts)
89+
7590
# pylint: disable=unused-argument
7691
def _before_cur_exec(self, conn, cursor, statement, *args):
92+
attrs, found = _get_attributes_from_url(conn.engine.url)
93+
if not found:
94+
attrs = _get_attributes_from_cursor(self.vendor, cursor, attrs)
95+
96+
db_name = attrs.get(_DB, "")
7797
self.current_span = self.tracer.start_span(
78-
statement, kind=trace.SpanKind.CLIENT
98+
self._operation_name(db_name, statement),
99+
kind=trace.SpanKind.CLIENT,
79100
)
80101
with self.tracer.use_span(self.current_span, end_on_exit=False):
81102
if self.current_span.is_recording():
82103
self.current_span.set_attribute(_STMT, statement)
83104
self.current_span.set_attribute("db.system", self.vendor)
84-
85-
if not _set_attributes_from_url(
86-
self.current_span, conn.engine.url
87-
):
88-
_set_attributes_from_cursor(
89-
self.current_span, self.vendor, cursor
90-
)
105+
for key, value in attrs.items():
106+
self.current_span.set_attribute(key, value)
91107

92108
# pylint: disable=unused-argument
93109
def _after_cur_exec(self, conn, cursor, statement, *args):
@@ -108,25 +124,22 @@ def _handle_error(self, context):
108124
self.current_span.end()
109125

110126

111-
def _set_attributes_from_url(span: trace.Span, url):
127+
def _get_attributes_from_url(url):
112128
"""Set connection tags from the url. return true if successful."""
113-
if span.is_recording():
114-
if url.host:
115-
span.set_attribute(_HOST, url.host)
116-
if url.port:
117-
span.set_attribute(_PORT, url.port)
118-
if url.database:
119-
span.set_attribute(_DB, url.database)
120-
if url.username:
121-
span.set_attribute(_USER, url.username)
122-
123-
return bool(url.host)
124-
125-
126-
def _set_attributes_from_cursor(span: trace.Span, vendor, cursor):
129+
attrs = {}
130+
if url.host:
131+
attrs[_HOST] = url.host
132+
if url.port:
133+
attrs[_PORT] = url.port
134+
if url.database:
135+
attrs[_DB] = url.database
136+
if url.username:
137+
attrs[_USER] = url.username
138+
return attrs, bool(url.host)
139+
140+
141+
def _get_attributes_from_cursor(vendor, cursor, attrs):
127142
"""Attempt to set db connection attributes by introspecting the cursor."""
128-
if not span.is_recording():
129-
return
130143
if vendor == "postgresql":
131144
# pylint: disable=import-outside-toplevel
132145
from psycopg2.extensions import parse_dsn
@@ -135,6 +148,7 @@ def _set_attributes_from_cursor(span: trace.Span, vendor, cursor):
135148
dsn = getattr(cursor.connection, "dsn", None)
136149
if dsn:
137150
data = parse_dsn(dsn)
138-
span.set_attribute(_DB, data.get("dbname"))
139-
span.set_attribute(_HOST, data.get("host"))
140-
span.set_attribute(_PORT, int(data.get("port")))
151+
attrs[_DB] = data.get("dbname")
152+
attrs[_HOST] = data.get("host")
153+
attrs[_PORT] = int(data.get("port"))
154+
return attrs

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_trace_integration(self):
3535
spans = self.memory_exporter.get_finished_spans()
3636

3737
self.assertEqual(len(spans), 1)
38-
self.assertEqual(spans[0].name, "SELECT 1 + 1;")
38+
self.assertEqual(spans[0].name, "SELECT :memory:")
3939
self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)
4040

4141
def test_not_recording(self):
@@ -68,5 +68,5 @@ def test_create_engine_wrapper(self):
6868
spans = self.memory_exporter.get_finished_spans()
6969

7070
self.assertEqual(len(spans), 1)
71-
self.assertEqual(spans[0].name, "SELECT 1 + 1;")
71+
self.assertEqual(spans[0].name, "SELECT :memory:")
7272
self.assertEqual(spans[0].kind, trace.SpanKind.CLIENT)

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ def tearDown(self):
110110
super().tearDown()
111111

112112
def _check_span(self, span, name):
113+
if self.SQL_DB:
114+
name = "{0} {1}".format(name, self.SQL_DB)
113115
self.assertEqual(span.name, name)
114116
self.assertEqual(span.attributes.get(_DB), self.SQL_DB)
115117
self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET)
@@ -129,7 +131,7 @@ def test_orm_insert(self):
129131
stmt += "(?, ?)"
130132
else:
131133
stmt += "(%(id)s, %(name)s)"
132-
self._check_span(span, stmt)
134+
self._check_span(span, "INSERT")
133135
self.assertIn("INSERT INTO players", span.attributes.get(_STMT))
134136
self.check_meta(span)
135137

@@ -146,7 +148,7 @@ def test_session_query(self):
146148
stmt += "?"
147149
else:
148150
stmt += "%(name_1)s"
149-
self._check_span(span, stmt)
151+
self._check_span(span, "SELECT")
150152
self.assertIn(
151153
"SELECT players.id AS players_id, players.name AS players_name \nFROM players \nWHERE players.name",
152154
span.attributes.get(_STMT),
@@ -163,7 +165,7 @@ def test_engine_connect_execute(self):
163165
spans = self.memory_exporter.get_finished_spans()
164166
self.assertEqual(len(spans), 1)
165167
span = spans[0]
166-
self._check_span(span, stmt)
168+
self._check_span(span, "SELECT")
167169
self.assertEqual(span.attributes.get(_STMT), "SELECT * FROM players")
168170
self.check_meta(span)
169171

@@ -188,4 +190,4 @@ def test_parent(self):
188190
self.assertEqual(parent_span.name, "sqlalch_op")
189191
self.assertEqual(parent_span.instrumentation_info.name, "sqlalch_svc")
190192

191-
self.assertEqual(child_span.name, stmt)
193+
self.assertEqual(child_span.name, "SELECT " + self.SQL_DB)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ def tearDown(self):
5656

5757
def test_engine_traced(self):
5858
# ensures that the engine is traced
59-
rows = self.conn.execute("SELECT 1").fetchall()
59+
rows = self.conn.execute("SELECT").fetchall()
6060
self.assertEqual(len(rows), 1)
6161

6262
traces = self.memory_exporter.get_finished_spans()
6363
# trace composition
6464
self.assertEqual(len(traces), 1)
6565
span = traces[0]
6666
# check subset of span fields
67-
self.assertEqual(span.name, "SELECT 1")
67+
self.assertEqual(span.name, "SELECT opentelemetry-tests")
6868
self.assertIs(span.status.status_code, trace.status.StatusCode.UNSET)
6969
self.assertGreater((span.end_time - span.start_time), 0)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_engine_execute_errors(self):
6767
self.assertEqual(len(spans), 1)
6868
span = spans[0]
6969
# span fields
70-
self.assertEqual(span.name, "SELECT * FROM a_wrong_table")
70+
self.assertEqual(span.name, "SELECT opentelemetry-tests")
7171
self.assertEqual(
7272
span.attributes.get(_STMT), "SELECT * FROM a_wrong_table"
7373
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def test_engine_execute_errors(self):
6565
self.assertEqual(len(spans), 1)
6666
span = spans[0]
6767
# span fields
68-
self.assertEqual(span.name, "SELECT * FROM a_wrong_table")
68+
self.assertEqual(span.name, "SELECT opentelemetry-tests")
6969
self.assertEqual(
7070
span.attributes.get(_STMT), "SELECT * FROM a_wrong_table"
7171
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def test_engine_execute_errors(self):
4343
self.assertEqual(len(spans), 1)
4444
span = spans[0]
4545
# span fields
46-
self.assertEqual(span.name, stmt)
46+
self.assertEqual(span.name, "SELECT :memory:")
4747
self.assertEqual(
4848
span.attributes.get(_STMT), "SELECT * FROM a_wrong_table"
4949
)

0 commit comments

Comments
 (0)