Skip to content

Commit 41aa56e

Browse files
Merge pull request #2 from carlosalberto/orm_integration
Implement initial support for ORM tracing.
2 parents ed1b8eb + bb139e3 commit 41aa56e

5 files changed

Lines changed: 201 additions & 30 deletions

File tree

examples/orm-childspan.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import os
2+
from sqlalchemy import MetaData, Table, Integer, String, Column, create_engine
3+
from sqlalchemy.ext.declarative import declarative_base
4+
from sqlalchemy.orm import sessionmaker
5+
6+
import lightstep
7+
import sqlalchemy_opentracing
8+
9+
DB_LOCATION = '/tmp/simple.db'
10+
11+
tracer = lightstep.Tracer(
12+
component_name='sqlalchemy-orm-childspan',
13+
access_token='{your_lightstep_token}'
14+
)
15+
16+
Base = declarative_base()
17+
18+
class User(Base):
19+
__tablename__ = 'users'
20+
21+
id = Column(Integer, primary_key=True)
22+
name = Column(String)
23+
24+
if __name__ == '__main__':
25+
if os.path.exists(DB_LOCATION):
26+
os.remove(DB_LOCATION) # cleanup
27+
28+
engine = create_engine('sqlite:///%s' % DB_LOCATION)
29+
session = sessionmaker(bind=engine)()
30+
31+
sqlalchemy_opentracing.init_tracing(tracer)
32+
sqlalchemy_opentracing.register_connectable(engine)
33+
34+
User.metadata.create_all(engine)
35+
36+
# Create a parent span
37+
span = tracer.start_span('parent span')
38+
39+
# Register the session for the current transaction.
40+
sqlalchemy_opentracing.set_parent_span(session, span)
41+
42+
session.add(User(name='John Doe'))
43+
session.add(User(name='Jason Bourne'))
44+
45+
users = session.query(User).all()
46+
for user in users:
47+
print user.name
48+
49+
session.add(User(name='Who'))
50+
51+
# Commit the session and close the parent span.
52+
session.commit()
53+
span.finish()
54+

examples/orm-simple.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
from sqlalchemy import MetaData, Table, Integer, String, Column, create_engine
3+
from sqlalchemy.ext.declarative import declarative_base
4+
from sqlalchemy.orm import sessionmaker
5+
6+
import lightstep
7+
import sqlalchemy_opentracing
8+
9+
DB_LOCATION = '/tmp/simple.db'
10+
11+
tracer = lightstep.Tracer(
12+
component_name='sqlalchemy-orm-simple',
13+
access_token='{your_lightstep_token}'
14+
)
15+
16+
Base = declarative_base()
17+
18+
class User(Base):
19+
__tablename__ = 'users'
20+
21+
id = Column(Integer, primary_key=True)
22+
name = Column(String)
23+
24+
if __name__ == '__main__':
25+
if os.path.exists(DB_LOCATION):
26+
os.remove(DB_LOCATION) # cleanup
27+
28+
engine = create_engine('sqlite:///%s' % DB_LOCATION)
29+
session = sessionmaker(bind=engine)()
30+
31+
sqlalchemy_opentracing.init_tracing(tracer)
32+
sqlalchemy_opentracing.register_connectable(engine)
33+
34+
User.metadata.create_all(engine)
35+
36+
# Register the session for the current transaction.
37+
sqlalchemy_opentracing.set_traced(session)
38+
39+
# Insert a new row.
40+
session.add(User(name='John Doe'))
41+
42+
# Finish the commit.
43+
# This will stop tracing the session.
44+
session.commit()
45+

sqlalchemy_opentracing/__init__.py

Lines changed: 102 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from sqlalchemy.event import listen, remove
2+
from sqlalchemy.orm import Session
23

34
g_tracer = None
45
g_trace_all = True
@@ -17,92 +18,138 @@ def init_tracing(tracer, trace_all=False):
1718

1819
g_trace_all = trace_all
1920

20-
def get_traced(stmt_obj):
21+
def get_traced(obj):
2122
'''
2223
Gets a bool indicating whether or not this
23-
statement is marked for tracing.
24+
object is marked for tracing.
2425
'''
25-
return getattr(stmt_obj, '_traced', False)
26+
return getattr(obj, '_traced', False)
2627

27-
def set_traced(stmt_obj):
28+
def set_traced(obj):
2829
'''
29-
Mark a statement to be traced.
30+
Mark a statement/session to be traced.
3031
'''
31-
stmt_obj._traced = True
32+
obj._traced = True
3233

33-
def get_parent_span(stmt_obj):
34+
# Session needs to have its connection/statements
35+
# decorated as soon as a connection is acquired.
36+
if isinstance(obj, Session):
37+
_register_session_connection_event(obj)
38+
39+
def get_parent_span(obj):
3440
'''
35-
Gets a parent span for this statement, if any.
41+
Gets a parent span for this object, if any.
3642
'''
37-
return getattr(stmt_obj, '_parent_span', None)
43+
return getattr(obj, '_parent_span', None)
3844

39-
def set_parent_span(stmt_obj, parent_span):
45+
def set_parent_span(obj, parent_span):
4046
'''
41-
Marks a statement as a child of a span.
47+
Marks an object as a child of a span.
4248
It gets marked to be traced if it wasn't before.
4349
'''
44-
stmt_obj._parent_span = parent_span
45-
stmt_obj._traced = True
50+
obj._parent_span = parent_span
51+
set_traced(obj)
4652

47-
def has_parent_span(stmt_obj):
53+
def has_parent_span(obj):
4854
'''
4955
Get whether or not the statement has
5056
a parent span.
5157
'''
52-
return hasattr(stmt_obj, '_parent_span')
58+
return hasattr(obj, '_parent_span')
5359

54-
def get_span(stmt_obj):
60+
def get_span(obj):
5561
'''
5662
Get the span of a statement object, if any.
5763
'''
58-
return getattr(stmt_obj, '_span', None)
64+
return getattr(obj, '_span', None)
5965

6066
def register_connectable(obj):
6167
'''
6268
Register an object to have its events be traced.
6369
Any Connectable object is accepted, which
6470
includes Connection and Engine.
6571
'''
66-
listen(obj, 'before_cursor_execute', _before_cursor_handler)
67-
listen(obj, 'after_cursor_execute', _after_cursor_handler)
68-
listen(obj, 'handle_error', _error_handler)
72+
listen(obj, 'before_cursor_execute', _connectable_before_cursor_handler)
73+
listen(obj, 'after_cursor_execute', _connectable_after_cursor_handler)
74+
listen(obj, 'handle_error', _connectable_error_handler)
6975

7076
def unregister_connectable(obj):
7177
'''
7278
Remove a connectable from having its events being
7379
traced.
7480
'''
75-
remove(obj, 'before_cursor_execute', _before_cursor_handler)
76-
remove(obj, 'after_cursor_execute', _after_cursor_handler)
77-
remove(obj, 'handle_error', _error_handler)
81+
remove(obj, 'before_cursor_execute', _connectable_before_cursor_handler)
82+
remove(obj, 'after_cursor_execute', _connectable_after_cursor_handler)
83+
remove(obj, 'handle_error', _connectable_error_handler)
84+
85+
def _can_operation_be_traced(conn, stmt_obj):
86+
'''
87+
Get whether an operation can be traced, depending on its
88+
connection or the statement being executed, having the latter
89+
the priority.
90+
'''
91+
if hasattr(stmt_obj, '_traced'):
92+
return stmt_obj._traced
93+
if hasattr(conn, '_traced'):
94+
return conn._traced
95+
96+
return False
97+
98+
def _clear_traced(obj):
99+
'''
100+
Clear an object's decorated tracing fields,
101+
to prevent unintended further tracing.
102+
'''
103+
if hasattr(obj, '_parent_span'):
104+
del obj._parent_span
105+
if hasattr(obj, '_traced'):
106+
del obj._traced
107+
108+
def _set_traced_with_session(conn, session):
109+
'''
110+
Mark a connection to be traced with a session tracing information.
111+
'''
112+
conn._traced = True
113+
parent_span = get_parent_span(session)
114+
if parent_span is not None:
115+
conn._parent_span = parent_span
78116

79117
def _get_operation_name(stmt_obj):
80118
return stmt_obj.__visit_name__
81119

82120
def _normalize_stmt(statement):
83121
return statement.strip().replace('\n', '').replace('\t', '')
84122

85-
def _before_cursor_handler(conn, cursor, statement, parameters, context, executemany):
123+
def _connectable_before_cursor_handler(conn, cursor,
124+
statement, parameters,
125+
context, executemany):
86126
if context.compiled is None: # PRAGMA
87127
return
88128

89-
# Don't trace if trace_all is disabled and the statement wasn't marked
129+
# Don't trace if trace_all is disabled
130+
# and the connection/statement wasn't marked explicitly.
90131
stmt_obj = context.compiled.statement
91-
if not (g_trace_all or get_traced(stmt_obj)):
132+
if not (g_trace_all or _can_operation_be_traced(conn, stmt_obj)):
92133
return
93134

135+
# Retrieve the parent span, if any,
136+
# either from the statement or inherited from the connection.
94137
parent_span = get_parent_span(stmt_obj)
95-
operation_name = _get_operation_name(stmt_obj)
138+
if parent_span is None:
139+
parent_span = get_parent_span(conn)
96140

97141
# Start a new span for this query.
98-
span = g_tracer.start_span(operation_name=operation_name, child_of=parent_span)
142+
name = _get_operation_name(stmt_obj)
143+
span = g_tracer.start_span(operation_name=name, child_of=parent_span)
99144
span.set_tag('component', 'sqlalchemy')
100145
span.set_tag('db.type', 'sql')
101146
span.set_tag('db.statement', _normalize_stmt(statement))
102147

103148
stmt_obj._span = span
104149

105-
def _after_cursor_handler(conn, cursor, statement, parameters, context, executemany):
150+
def _connectable_after_cursor_handler(conn, cursor,
151+
statement, parameters,
152+
context, executemany):
106153
if context.compiled is None: # PRAGMA
107154
return
108155

@@ -113,7 +160,7 @@ def _after_cursor_handler(conn, cursor, statement, parameters, context, executem
113160

114161
span.finish()
115162

116-
def _error_handler(exception_context):
163+
def _connectable_error_handler(exception_context):
117164
execution_context = exception_context.execution_context
118165
stmt_obj = execution_context.compiled.statement
119166
span = get_span(stmt_obj)
@@ -123,3 +170,28 @@ def _error_handler(exception_context):
123170
span.set_tag('error', 'true')
124171
span.finish()
125172

173+
def _register_session_connection_event(session):
174+
listen(session, 'after_begin', _session_after_begin_handler)
175+
176+
def _session_after_begin_handler(session, transaction, conn):
177+
# Have the connections inherit the tracing info
178+
# from the session (including parent span, if any).
179+
_set_traced_with_session(conn, session)
180+
181+
# Plug post-operation clean up handlers.
182+
# The actual session commit/rollback is not traced by us.
183+
listen(session, 'before_commit', _session_before_commit_handler, once=True)
184+
listen(session, 'after_rollback', _session_rollback_handler, once=True)
185+
186+
# Event handlers can't remove themselves
187+
# Fine, as long as we only fire one of them *once*.
188+
def _session_before_commit_handler(session):
189+
_clear_traced(session)
190+
remove(session, 'after_begin', _session_after_begin_handler)
191+
remove(session, 'after_rollback', _session_rollback_handler)
192+
193+
def _session_rollback_handler(session):
194+
_clear_traced(session)
195+
remove(session, 'after_begin', _session_after_begin_handler)
196+
remove(session, 'before_commit', _session_before_commit_handler)
197+

0 commit comments

Comments
 (0)