-
Notifications
You must be signed in to change notification settings - Fork 143
/
patcher.py
239 lines (190 loc) · 8.13 KB
/
patcher.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import importlib
import inspect
import logging
import os
import pkgutil
import re
import sys
import wrapt
from aws_xray_sdk import global_sdk_config
from .utils.compat import is_classmethod, is_instance_method
log = logging.getLogger(__name__)
SUPPORTED_MODULES = (
'aiobotocore',
'botocore',
'pynamodb',
'requests',
'sqlite3',
'mysql',
'httplib',
'pymongo',
'pymysql',
'psycopg2',
'pg8000',
'sqlalchemy_core',
'httpx',
)
NO_DOUBLE_PATCH = (
'aiobotocore',
'botocore',
'pynamodb',
'requests',
'sqlite3',
'mysql',
'pymongo',
'pymysql',
'psycopg2',
'pg8000',
'sqlalchemy_core',
'httpx',
)
_PATCHED_MODULES = set()
def patch_all(double_patch=False):
"""
The X-Ray Python SDK supports patching aioboto3, aiobotocore, boto3, botocore, pynamodb, requests,
sqlite3, mysql, httplib, pymongo, pymysql, psycopg2, pg8000, sqlalchemy_core, httpx, and mysql-connector.
To patch all supported libraries::
from aws_xray_sdk.core import patch_all
patch_all()
:param bool double_patch: enable or disable patching of indirect dependencies.
"""
if double_patch:
patch(SUPPORTED_MODULES, raise_errors=False)
else:
patch(NO_DOUBLE_PATCH, raise_errors=False)
def _is_valid_import(module):
module = module.replace('.', '/')
realpath = os.path.realpath(module)
is_module = os.path.isdir(realpath) and (
os.path.isfile('{}/__init__.py'.format(module)) or os.path.isfile('{}/__init__.pyc'.format(module))
)
is_file = not is_module and (
os.path.isfile('{}.py'.format(module)) or os.path.isfile('{}.pyc'.format(module))
)
return is_module or is_file
def patch(modules_to_patch, raise_errors=True, ignore_module_patterns=None):
"""
To patch specific modules::
from aws_xray_sdk.core import patch
i_want_to_patch = ('botocore') # a tuple that contains the libs you want to patch
patch(i_want_to_patch)
:param tuple modules_to_patch: a tuple containing the list of libraries to be patched
"""
enabled = global_sdk_config.sdk_enabled()
if not enabled:
log.debug("Skipped patching modules %s because the SDK is currently disabled." % ', '.join(modules_to_patch))
return # Disable module patching if the SDK is disabled.
modules = set()
for module_to_patch in modules_to_patch:
# boto3 depends on botocore and patching botocore is sufficient
if module_to_patch == 'boto3':
modules.add('botocore')
# aioboto3 depends on aiobotocore and patching aiobotocore is sufficient
elif module_to_patch == 'aioboto3':
modules.add('aiobotocore')
# pynamodb requires botocore to be patched as well
elif module_to_patch == 'pynamodb':
modules.add('botocore')
modules.add(module_to_patch)
else:
modules.add(module_to_patch)
unsupported_modules = set(module for module in modules if module not in SUPPORTED_MODULES)
native_modules = modules - unsupported_modules
external_modules = set(module for module in unsupported_modules if _is_valid_import(module))
unsupported_modules = unsupported_modules - external_modules
if unsupported_modules:
raise Exception('modules %s are currently not supported for patching'
% ', '.join(unsupported_modules))
for m in native_modules:
_patch_module(m, raise_errors)
ignore_module_patterns = [re.compile(pattern) for pattern in ignore_module_patterns or []]
for m in external_modules:
_external_module_patch(m, ignore_module_patterns)
def _patch_module(module_to_patch, raise_errors=True):
try:
_patch(module_to_patch)
except Exception:
if raise_errors:
raise
log.debug('failed to patch module %s', module_to_patch)
def _patch(module_to_patch):
path = 'aws_xray_sdk.ext.%s' % module_to_patch
if module_to_patch in _PATCHED_MODULES:
log.debug('%s already patched', module_to_patch)
return
imported_module = importlib.import_module(path)
imported_module.patch()
_PATCHED_MODULES.add(module_to_patch)
log.info('successfully patched module %s', module_to_patch)
def _patch_func(parent, func_name, func, modifier=lambda x: x):
if func_name not in parent.__dict__:
# Ignore functions not directly defined in parent, i.e. exclude inherited ones
return
from aws_xray_sdk.core import xray_recorder
capture_name = func_name
if func_name.startswith('__') and func_name.endswith('__'):
capture_name = '{}.{}'.format(parent.__name__, capture_name)
setattr(parent, func_name, modifier(xray_recorder.capture(name=capture_name)(func)))
def _patch_class(module, cls):
for member_name, member in inspect.getmembers(cls, inspect.isclass):
if member.__module__ == module.__name__:
# Only patch classes of the module, ignore imports
_patch_class(module, member)
for member_name, member in inspect.getmembers(cls, inspect.ismethod):
if member.__module__ == module.__name__:
# Only patch methods of the class defined in the module, ignore other modules
if is_classmethod(member):
# classmethods are internally generated through descriptors. The classmethod
# decorator must be the last applied, so we cannot apply another one on top
log.warning('Cannot automatically patch classmethod %s.%s, '
'please apply decorator manually', cls.__name__, member_name)
else:
_patch_func(cls, member_name, member)
for member_name, member in inspect.getmembers(cls, inspect.isfunction):
if member.__module__ == module.__name__:
# Only patch static methods of the class defined in the module, ignore other modules
if is_instance_method(cls, member_name, member):
_patch_func(cls, member_name, member)
else:
_patch_func(cls, member_name, member, modifier=staticmethod)
def _on_import(module):
for member_name, member in inspect.getmembers(module, inspect.isfunction):
if member.__module__ == module.__name__:
# Only patch functions of the module, ignore imports
_patch_func(module, member_name, member)
for member_name, member in inspect.getmembers(module, inspect.isclass):
if member.__module__ == module.__name__:
# Only patch classes of the module, ignore imports
_patch_class(module, member)
def _external_module_patch(module, ignore_module_patterns):
if module.startswith('.'):
raise Exception('relative packages not supported for patching: {}'.format(module))
if module in _PATCHED_MODULES:
log.debug('%s already patched', module)
elif any(pattern.match(module) for pattern in ignore_module_patterns):
log.debug('%s ignored due to rules: %s', module, ignore_module_patterns)
else:
if module in sys.modules:
_on_import(sys.modules[module])
else:
wrapt.importer.when_imported(module)(_on_import)
for loader, submodule_name, is_module in pkgutil.iter_modules([module.replace('.', '/')]):
submodule = '.'.join([module, submodule_name])
if is_module:
_external_module_patch(submodule, ignore_module_patterns)
else:
if submodule in _PATCHED_MODULES:
log.debug('%s already patched', submodule)
continue
elif any(pattern.match(submodule) for pattern in ignore_module_patterns):
log.debug('%s ignored due to rules: %s', submodule, ignore_module_patterns)
continue
if submodule in sys.modules:
_on_import(sys.modules[submodule])
else:
wrapt.importer.when_imported(submodule)(_on_import)
_PATCHED_MODULES.add(submodule)
log.info('successfully patched module %s', submodule)
if module not in _PATCHED_MODULES:
_PATCHED_MODULES.add(module)
log.info('successfully patched module %s', module)