Skip to content

Commit 40dc11b

Browse files
committed
Trace raw SQL statements.
This is done using the ExecutionContext as storage place (since it is an unique object shared for a single instance). It is an ideal place as raw statement simply come as a string, so no way to store the current span there. We still try to ignore PRAGMA statements, though.
1 parent 5477334 commit 40dc11b

3 files changed

Lines changed: 102 additions & 19 deletions

File tree

sqlalchemy_opentracing/__init__.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,6 @@ def _can_operation_be_traced(conn, stmt_obj):
101101

102102
return False
103103

104-
def _get_span(obj):
105-
'''
106-
Get the span of a statement object, if any.
107-
'''
108-
return getattr(obj, '_span', None)
109-
110104
def _set_traced_with_session(conn, session):
111105
'''
112106
Mark a connection to be traced with a session tracing information.
@@ -117,6 +111,11 @@ def _set_traced_with_session(conn, session):
117111
conn._parent_span = parent_span
118112

119113
def _get_operation_name(stmt_obj):
114+
if stmt_obj is None:
115+
# Match what the ORM shows when raw SQL
116+
# statements are invoked.
117+
return 'textclause'
118+
120119
return stmt_obj.__visit_name__
121120

122121
def _normalize_stmt(statement):
@@ -125,15 +124,19 @@ def _normalize_stmt(statement):
125124
def _engine_before_cursor_handler(conn, cursor,
126125
statement, parameters,
127126
context, executemany):
128-
if context.compiled is None: # PRAGMA
129-
return
127+
stmt_obj = None
128+
if context.compiled is not None:
129+
stmt_obj = context.compiled.statement
130130

131131
# Don't trace if trace_all is disabled
132132
# and the connection/statement wasn't marked explicitly.
133-
stmt_obj = context.compiled.statement
134133
if not (g_trace_all or _can_operation_be_traced(conn, stmt_obj)):
135134
return
136135

136+
# Don't trace PRAGMA statements coming from SQLite
137+
if stmt_obj is None and statement.startswith('PRAGMA'):
138+
return
139+
137140
# Retrieve the parent span, if any,
138141
# either from the statement or inherited from the connection.
139142
parent_span = get_parent_span(stmt_obj)
@@ -148,26 +151,23 @@ def _engine_before_cursor_handler(conn, cursor,
148151
span.set_tag('db.statement', _normalize_stmt(statement))
149152
span.set_tag('sqlalchemy.dialect', context.dialect.name)
150153

151-
stmt_obj._span = span
154+
context._span = span
152155

153156
def _engine_after_cursor_handler(conn, cursor,
154157
statement, parameters,
155158
context, executemany):
156-
if context.compiled is None: # PRAGMA
157-
return
158-
159-
stmt_obj = context.compiled.statement
160-
span = _get_span(stmt_obj)
159+
span = getattr(context, '_span', None)
161160
if span is None:
162161
return
163162

164163
span.finish()
165-
clear_traced(stmt_obj)
164+
165+
if context.compiled is not None:
166+
clear_traced(context.compiled.statement)
166167

167168
def _engine_error_handler(exception_context):
168169
execution_context = exception_context.execution_context
169-
stmt_obj = execution_context.compiled.statement
170-
span = _get_span(stmt_obj)
170+
span = getattr(execution_context, '_span', None)
171171
if span is None:
172172
return
173173

tests/test_core.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,44 @@ def test_traced_error(self):
7878
'error': 'true',
7979
})
8080

81+
def test_trace_text(self):
82+
tracer = DummyTracer()
83+
creat = CreateTable(self.users_table)
84+
self.engine.execute(creat)
85+
86+
sqlalchemy_opentracing.init_tracing(tracer, trace_all=True)
87+
self.engine.execute('SELECT name FROM users')
88+
self.assertEqual(1, len(tracer.spans))
89+
self.assertEqual(tracer.spans[0].operation_name, 'textclause')
90+
self.assertEqual(tracer.spans[0].is_finished, True)
91+
self.assertEqual(tracer.spans[0].tags, {
92+
'component': 'sqlalchemy',
93+
'db.statement': 'SELECT name FROM users',
94+
'db.type': 'sql',
95+
'sqlalchemy.dialect': 'sqlite',
96+
})
97+
98+
def test_trace_text_error(self):
99+
tracer = DummyTracer()
100+
101+
sqlalchemy_opentracing.init_tracing(tracer, trace_all=True)
102+
try:
103+
self.engine.execute('SELECT name FROM users')
104+
except OperationalError:
105+
pass
106+
107+
self.assertEqual(1, len(tracer.spans))
108+
self.assertEqual(tracer.spans[0].operation_name, 'textclause')
109+
self.assertEqual(tracer.spans[0].is_finished, True)
110+
self.assertEqual(tracer.spans[0].tags, {
111+
'component': 'sqlalchemy',
112+
'db.statement': 'SELECT name FROM users',
113+
'db.type': 'sql',
114+
'sqlalchemy.dialect': 'sqlite',
115+
'sqlalchemy.exception': 'no such table: users',
116+
'error': 'true',
117+
})
118+
81119
def test_traced_transaction(self):
82120
tracer = DummyTracer()
83121
creat = CreateTable(self.users_table)

tests/test_orm.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import unittest
22
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String
33
from sqlalchemy.ext.declarative import declarative_base
4-
from sqlalchemy.exc import IntegrityError
4+
from sqlalchemy.exc import IntegrityError, OperationalError
55
from sqlalchemy.orm import sessionmaker
66

77
import sqlalchemy_opentracing
@@ -113,6 +113,51 @@ def test_traced_parent(self):
113113
self.assertEqual(True, all(map(lambda x: x.is_finished, tracer.spans)))
114114
self.assertEqual(True, all(map(lambda x: x.child_of == parent_span, tracer.spans)))
115115

116+
def test_traced_text(self):
117+
tracer = DummyTracer()
118+
sqlalchemy_opentracing.init_tracing(tracer)
119+
120+
session = self.session
121+
span = DummySpan('parent span')
122+
sqlalchemy_opentracing.set_parent_span(session, span)
123+
session.execute('SELECT name FROM users')
124+
125+
self.assertEqual(1, len(tracer.spans))
126+
self.assertEqual(tracer.spans[0].operation_name, 'textclause')
127+
self.assertEqual(tracer.spans[0].is_finished, True)
128+
self.assertEqual(tracer.spans[0].child_of, span)
129+
self.assertEqual(tracer.spans[0].tags, {
130+
'component': 'sqlalchemy',
131+
'db.statement': 'SELECT name FROM users',
132+
'db.type': 'sql',
133+
'sqlalchemy.dialect': 'sqlite',
134+
})
135+
136+
def test_traced_text_error(self):
137+
tracer = DummyTracer()
138+
sqlalchemy_opentracing.init_tracing(tracer)
139+
140+
session = self.session
141+
span = DummySpan('parent span')
142+
sqlalchemy_opentracing.set_parent_span(session, span)
143+
try:
144+
session.execute('SELECT zipcode FROM addresses')
145+
except OperationalError:
146+
pass
147+
148+
self.assertEqual(1, len(tracer.spans))
149+
self.assertEqual(tracer.spans[0].operation_name, 'textclause')
150+
self.assertEqual(tracer.spans[0].is_finished, True)
151+
self.assertEqual(tracer.spans[0].child_of, span)
152+
self.assertEqual(tracer.spans[0].tags, {
153+
'component': 'sqlalchemy',
154+
'db.statement': 'SELECT zipcode FROM addresses',
155+
'db.type': 'sql',
156+
'sqlalchemy.dialect': 'sqlite',
157+
'sqlalchemy.exception': 'no such table: addresses',
158+
'error': 'true',
159+
})
160+
116161
def test_traced_after_commit(self):
117162
tracer = DummyTracer()
118163
sqlalchemy_opentracing.init_tracing(tracer)

0 commit comments

Comments
 (0)