Skip to content

Commit d807884

Browse files
authored
Fixing broken instrumentation for sqlalchemy >= 1.4.0 (#289)
* Drop version pinning sqlalchemy and flask_sqlalchemy * Check obj type instead of callable for functions * testing * Adding patch for Session.execute. Removing session.add test * Remove unused variable * Adding 2.0 style test case. Some refactor. Unwrap session. * Splitting out tests * Typo fix * Refactored some tests files * minor renaming of function
1 parent 894b419 commit d807884

File tree

6 files changed

+110
-81
lines changed

6 files changed

+110
-81
lines changed

aws_xray_sdk/ext/sqlalchemy/util/decorators.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import re
2+
import types
3+
24
from aws_xray_sdk.core import xray_recorder
35
from aws_xray_sdk.ext.util import strip_url
46
from future.standard_library import install_aliases
@@ -13,7 +15,7 @@ def decorator(cls):
1315
for name, obj in vars(c).items():
1416
if name.startswith("_"):
1517
continue
16-
if callable(obj):
18+
if isinstance(obj, types.FunctionType):
1719
try:
1820
obj = obj.__func__ # unwrap Python 2 unbound method
1921
except AttributeError:

aws_xray_sdk/ext/sqlalchemy_core/patch.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import sys
3+
34
if sys.version_info >= (3, 0, 0):
45
from urllib.parse import urlparse, uses_netloc
56
else:
@@ -13,10 +14,10 @@
1314
from aws_xray_sdk.ext.util import unwrap
1415

1516

16-
def _sql_meta(instance, args):
17+
def _sql_meta(engine_instance, args):
1718
try:
1819
metadata = {}
19-
url = urlparse(str(instance.engine.url))
20+
url = urlparse(str(engine_instance.engine.url))
2021
# Add Scheme to uses_netloc or // will be missing from url.
2122
uses_netloc.append(url.scheme)
2223
if url.password is None:
@@ -29,17 +30,20 @@ def _sql_meta(instance, args):
2930
metadata['url'] = parts.geturl()
3031
name = host_info
3132
metadata['user'] = url.username
32-
metadata['database_type'] = instance.engine.name
33+
metadata['database_type'] = engine_instance.engine.name
3334
try:
34-
version = getattr(instance.dialect, '{}_version'.format(instance.engine.driver))
35+
version = getattr(engine_instance.dialect, '{}_version'.format(engine_instance.engine.driver))
3536
version_str = '.'.join(map(str, version))
36-
metadata['driver_version'] = "{}-{}".format(instance.engine.driver, version_str)
37+
metadata['driver_version'] = "{}-{}".format(engine_instance.engine.driver, version_str)
3738
except AttributeError:
38-
metadata['driver_version'] = instance.engine.driver
39-
if instance.dialect.server_version_info is not None:
40-
metadata['database_version'] = '.'.join(map(str, instance.dialect.server_version_info))
39+
metadata['driver_version'] = engine_instance.engine.driver
40+
if engine_instance.dialect.server_version_info is not None:
41+
metadata['database_version'] = '.'.join(map(str, engine_instance.dialect.server_version_info))
4142
if xray_recorder.stream_sql:
42-
metadata['sanitized_query'] = str(args[0])
43+
try:
44+
metadata['sanitized_query'] = str(args[0])
45+
except Exception:
46+
logging.getLogger(__name__).exception('Error getting the sanitized query')
4347
except Exception:
4448
metadata = None
4549
name = None
@@ -48,7 +52,15 @@ def _sql_meta(instance, args):
4852

4953

5054
def _xray_traced_sqlalchemy_execute(wrapped, instance, args, kwargs):
51-
name, sql = _sql_meta(instance, args)
55+
return _process_request(wrapped, instance, args, kwargs)
56+
57+
58+
def _xray_traced_sqlalchemy_session(wrapped, instance, args, kwargs):
59+
return _process_request(wrapped, instance.bind, args, kwargs)
60+
61+
62+
def _process_request(wrapped, engine_instance, args, kwargs):
63+
name, sql = _sql_meta(engine_instance, args)
5264
if sql is not None:
5365
subsegment = xray_recorder.begin_subsegment(name, namespace='remote')
5466
else:
@@ -75,6 +87,12 @@ def patch():
7587
_xray_traced_sqlalchemy_execute
7688
)
7789

90+
wrapt.wrap_function_wrapper(
91+
'sqlalchemy.orm.session',
92+
'Session.execute',
93+
_xray_traced_sqlalchemy_session
94+
)
95+
7896

7997
def unpatch():
8098
"""
@@ -84,3 +102,4 @@ def unpatch():
84102
_PATCHED_MODULES.discard('sqlalchemy_core')
85103
import sqlalchemy
86104
unwrap(sqlalchemy.engine.base.Connection, 'execute')
105+
unwrap(sqlalchemy.orm.session.Session, 'execute')
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from __future__ import absolute_import
2+
3+
import pytest
4+
from sqlalchemy import create_engine, Column, Integer, String
5+
from sqlalchemy.ext.declarative import declarative_base
6+
from sqlalchemy.orm import sessionmaker
7+
8+
from aws_xray_sdk.core import xray_recorder, patch
9+
from aws_xray_sdk.core.context import Context
10+
11+
Base = declarative_base()
12+
13+
14+
class User(Base):
15+
__tablename__ = 'users'
16+
17+
id = Column(Integer, primary_key=True)
18+
name = Column(String)
19+
fullname = Column(String)
20+
password = Column(String)
21+
22+
23+
@pytest.fixture()
24+
def engine():
25+
"""
26+
Clean up context storage on each test run and begin a segment
27+
so that later subsegment can be attached. After each test run
28+
it cleans up context storage again.
29+
"""
30+
from aws_xray_sdk.ext.sqlalchemy_core import unpatch
31+
patch(('sqlalchemy_core',))
32+
engine = create_engine('sqlite:///:memory:')
33+
xray_recorder.configure(service='test', sampling=False, context=Context())
34+
xray_recorder.begin_segment('name')
35+
Base.metadata.create_all(engine)
36+
xray_recorder.clear_trace_entities()
37+
xray_recorder.begin_segment('name')
38+
yield engine
39+
xray_recorder.clear_trace_entities()
40+
unpatch()
41+
42+
43+
@pytest.fixture()
44+
def connection(engine):
45+
return engine.connect()
46+
47+
48+
@pytest.fixture()
49+
def session(engine):
50+
Session = sessionmaker(bind=engine)
51+
return Session()
Lines changed: 4 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,6 @@
1-
from __future__ import absolute_import
2-
3-
import pytest
4-
from sqlalchemy import create_engine, Column, Integer, String
5-
from sqlalchemy.ext.declarative import declarative_base
6-
from sqlalchemy.orm import sessionmaker
1+
from .test_base import User, session, engine, connection
72
from sqlalchemy.sql.expression import Insert, Delete
8-
9-
from aws_xray_sdk.core import xray_recorder, patch
10-
from aws_xray_sdk.core.context import Context
11-
12-
Base = declarative_base()
13-
14-
15-
class User(Base):
16-
__tablename__ = 'users'
17-
18-
id = Column(Integer, primary_key=True)
19-
name = Column(String)
20-
fullname = Column(String)
21-
password = Column(String)
22-
23-
24-
@pytest.fixture()
25-
def engine():
26-
"""
27-
Clean up context storage on each test run and begin a segment
28-
so that later subsegment can be attached. After each test run
29-
it cleans up context storage again.
30-
"""
31-
from aws_xray_sdk.ext.sqlalchemy_core import unpatch
32-
patch(('sqlalchemy_core',))
33-
engine = create_engine('sqlite:///:memory:')
34-
xray_recorder.configure(service='test', sampling=False, context=Context())
35-
xray_recorder.begin_segment('name')
36-
Base.metadata.create_all(engine)
37-
xray_recorder.clear_trace_entities()
38-
xray_recorder.begin_segment('name')
39-
yield engine
40-
xray_recorder.clear_trace_entities()
41-
unpatch()
42-
43-
44-
@pytest.fixture()
45-
def connection(engine):
46-
return engine.connect()
47-
48-
49-
@pytest.fixture()
50-
def session(engine):
51-
Session = sessionmaker(bind=engine)
52-
return Session()
53-
3+
from aws_xray_sdk.core import xray_recorder
544

555
def test_all(session):
566
""" Test calling all() on get all records.
@@ -63,19 +13,6 @@ def test_all(session):
6313
assert sql_meta['sanitized_query'].endswith('FROM users')
6414

6515

66-
def test_add(session):
67-
""" Test calling add() on insert a row.
68-
Verify we that we capture trace for the add"""
69-
password = "123456"
70-
john = User(name='John', fullname="John Doe", password=password)
71-
session.add(john)
72-
session.commit()
73-
assert len(xray_recorder.current_segment().subsegments) == 1
74-
sql_meta = xray_recorder.current_segment().subsegments[0].sql
75-
assert sql_meta['sanitized_query'].startswith('INSERT INTO users')
76-
assert password not in sql_meta['sanitized_query']
77-
78-
7916
def test_filter_first(session):
8017
""" Test calling filter().first() on get first filtered records.
8118
Verify we run the query and return the SQL as metdata"""
@@ -97,6 +34,7 @@ def test_connection_add(connection):
9734
assert sql_meta['url'] == 'sqlite:///:memory:'
9835
assert password not in sql_meta['sanitized_query']
9936

37+
10038
def test_connection_query(connection):
10139
password = "123456"
10240
statement = Delete(User).where(User.name == 'John').where(User.password == password)
@@ -105,4 +43,4 @@ def test_connection_query(connection):
10543
sql_meta = xray_recorder.current_segment().subsegments[0].sql
10644
assert sql_meta['sanitized_query'].startswith('DELETE FROM users')
10745
assert sql_meta['url'] == 'sqlite:///:memory:'
108-
assert password not in sql_meta['sanitized_query']
46+
assert password not in sql_meta['sanitized_query']
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from .test_base import User, session, engine, connection
2+
from sqlalchemy.sql.expression import select
3+
from aws_xray_sdk.core import xray_recorder
4+
5+
# 2.0 style execution test. see https://docs.sqlalchemy.org/en/14/changelog/migration_14.html#orm-query-is-internally
6+
# -unified-with-select-update-delete-2-0-style-execution-available
7+
def test_orm_style_select_execution(session):
8+
statement = select(User).where(
9+
User.name == 'John'
10+
)
11+
session.execute(statement)
12+
assert len(xray_recorder.current_segment().subsegments) == 1
13+
sql_meta = xray_recorder.current_segment().subsegments[0].sql
14+
assert sql_meta['sanitized_query'].startswith('SELECT')
15+
assert 'FROM users' in sql_meta['sanitized_query']

tox.ini

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ envlist =
66
py{36,37,38,39}-django30
77
py{36,37,38,39}-django31
88
py{36,37,38,39}-django32
9+
py{27,36,37,38,39}-sqlalchemy
10+
py{34,35}-sqlalchemy
911
coverage-report
1012

1113
skip_missing_interpreters = True
@@ -19,8 +21,8 @@ deps =
1921
requests
2022
bottle >= 0.10
2123
flask >= 0.10
22-
sqlalchemy==1.3.*
23-
Flask-SQLAlchemy==2.4.*
24+
sqlalchemy
25+
Flask-SQLAlchemy
2426
future
2527
django22: Django==2.2.*
2628
django30: Django==3.0.*
@@ -51,8 +53,10 @@ deps =
5153
py{35,36,37,38,39}: aiobotocore >= 0.10.0
5254

5355
commands =
54-
py{27,34}-default: coverage run --source aws_xray_sdk -m py.test tests --ignore tests/ext/aiohttp --ignore tests/ext/aiobotocore --ignore tests/ext/django --ignore tests/test_async_local_storage.py --ignore tests/test_async_recorder.py
55-
py{35,36,37,38,39}-default: coverage run --source aws_xray_sdk -m py.test --ignore tests/ext/django tests
56+
py{27,34}-default: coverage run --source aws_xray_sdk -m py.test tests --ignore tests/ext/aiohttp --ignore tests/ext/aiobotocore --ignore tests/ext/django --ignore tests/test_async_local_storage.py --ignore tests/test_async_recorder.py --ignore tests/ext/sqlalchemy_core
57+
py{35,36,37,38,39}-default: coverage run --source aws_xray_sdk -m py.test tests --ignore tests/ext/django --ignore tests/ext/sqlalchemy_core
58+
py{27,36,37,38,39}-default: coverage run --source aws_xray_sdk -m py.test tests/ext/sqlalchemy_core
59+
py{34,35}-default: coverage run --source aws_xray_sdk -m py.test tests/ext/sqlalchemy_core/ --ignore tests/ext/sqlalchemy_core/test_sqlalchemy_core_2.py
5660
django{22,30,31,32}: coverage run --source aws_xray_sdk -m py.test tests/ext/django
5761
codecov
5862

0 commit comments

Comments
 (0)