Skip to content

Commit 3892b6b

Browse files
authored
Internalize, deprecate ddtrace.monkey module (#2928)
The ddtrace/monkey.py should be an internal module, so I've moved the contents of monkey.py into the new _monkey.py. monkey.py imports the methods from _monkey.py in order to retain compatibility as we migrate to solely _monkey.py in v1.0. Deprecation warning is included in monkey.py
1 parent 758ef00 commit 3892b6b

File tree

6 files changed

+301
-277
lines changed

6 files changed

+301
-277
lines changed

ddtrace/_monkey.py

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
import importlib
2+
import os
3+
import sys
4+
import threading
5+
from typing import Any
6+
from typing import Callable
7+
from typing import List
8+
9+
from ddtrace.vendor.wrapt.importer import when_imported
10+
11+
from .internal.logger import get_logger
12+
from .settings import _config as config
13+
from .utils import formats
14+
from .utils.deprecation import deprecated
15+
16+
17+
log = get_logger(__name__)
18+
19+
# Default set of modules to automatically patch or not
20+
PATCH_MODULES = {
21+
"asyncio": True,
22+
"boto": True,
23+
"botocore": True,
24+
"bottle": False,
25+
"cassandra": True,
26+
"celery": True,
27+
"consul": True,
28+
"django": True,
29+
"elasticsearch": True,
30+
"algoliasearch": True,
31+
"futures": True,
32+
"grpc": True,
33+
"httpx": True,
34+
"mongoengine": True,
35+
"mysql": True,
36+
"mysqldb": True,
37+
"pymysql": True,
38+
"mariadb": True,
39+
"psycopg": True,
40+
"pylibmc": True,
41+
"pymemcache": True,
42+
"pymongo": True,
43+
"redis": True,
44+
"rediscluster": True,
45+
"requests": True,
46+
"rq": True,
47+
"sanic": True,
48+
"snowflake": False,
49+
"sqlalchemy": False, # Prefer DB client instrumentation
50+
"sqlite3": True,
51+
"aiohttp": True, # requires asyncio (Python 3.4+)
52+
"aiopg": True,
53+
"aiobotocore": False,
54+
"httplib": False,
55+
"urllib3": False,
56+
"vertica": True,
57+
"molten": True,
58+
"jinja2": True,
59+
"mako": True,
60+
"flask": True,
61+
"kombu": False,
62+
"starlette": True,
63+
# Ignore some web framework integrations that might be configured explicitly in code
64+
"falcon": False,
65+
"pylons": False,
66+
"pyramid": False,
67+
# Auto-enable logging if the environment variable DD_LOGS_INJECTION is true
68+
"logging": config.logs_injection,
69+
"pynamodb": True,
70+
"pyodbc": True,
71+
"fastapi": True,
72+
"dogpile_cache": True,
73+
}
74+
75+
_LOCK = threading.Lock()
76+
_PATCHED_MODULES = set()
77+
78+
# Modules which are patched on first use
79+
# DEV: These modules are patched when the user first imports them, rather than
80+
# explicitly importing and patching them on application startup `ddtrace.patch_all(module=True)`
81+
# DEV: This ensures we do not patch a module until it is needed
82+
# DEV: <contrib name> => <list of module names that trigger a patch>
83+
_PATCH_ON_IMPORT = {
84+
"aiohttp": ("aiohttp",),
85+
"aiobotocore": ("aiobotocore",),
86+
"celery": ("celery",),
87+
"flask": ("flask",),
88+
"gevent": ("gevent",),
89+
"requests": ("requests",),
90+
"botocore": ("botocore",),
91+
"elasticsearch": (
92+
"elasticsearch",
93+
"elasticsearch2",
94+
"elasticsearch5",
95+
"elasticsearch6",
96+
"elasticsearch7",
97+
),
98+
"pynamodb": ("pynamodb",),
99+
}
100+
101+
102+
class PatchException(Exception):
103+
"""Wraps regular `Exception` class when patching modules"""
104+
105+
pass
106+
107+
108+
class ModuleNotFoundException(PatchException):
109+
pass
110+
111+
112+
def _on_import_factory(module, raise_errors=True):
113+
# type: (str, bool) -> Callable[[Any], None]
114+
"""Factory to create an import hook for the provided module name"""
115+
116+
def on_import(hook):
117+
# Import and patch module
118+
path = "ddtrace.contrib.%s" % module
119+
try:
120+
imported_module = importlib.import_module(path)
121+
except ImportError:
122+
if raise_errors:
123+
raise
124+
log.error("failed to import ddtrace module %r when patching on import", path, exc_info=True)
125+
else:
126+
imported_module.patch()
127+
128+
return on_import
129+
130+
131+
def patch_all(**patch_modules):
132+
# type: (bool) -> None
133+
"""Automatically patches all available modules.
134+
135+
In addition to ``patch_modules``, an override can be specified via an
136+
environment variable, ``DD_TRACE_<module>_ENABLED`` for each module.
137+
138+
``patch_modules`` have the highest precedence for overriding.
139+
140+
:param dict patch_modules: Override whether particular modules are patched or not.
141+
142+
>>> patch_all(redis=False, cassandra=False)
143+
"""
144+
modules = PATCH_MODULES.copy()
145+
146+
# The enabled setting can be overridden by environment variables
147+
for module, enabled in modules.items():
148+
env_var = "DD_TRACE_%s_ENABLED" % module.upper()
149+
if env_var not in os.environ:
150+
continue
151+
152+
override_enabled = formats.asbool(os.environ[env_var])
153+
modules[module] = override_enabled
154+
155+
# Arguments take precedence over the environment and the defaults.
156+
modules.update(patch_modules)
157+
158+
patch(raise_errors=False, **modules)
159+
160+
161+
def patch(raise_errors=True, **patch_modules):
162+
# type: (bool, bool) -> None
163+
"""Patch only a set of given modules.
164+
165+
:param bool raise_errors: Raise error if one patch fail.
166+
:param dict patch_modules: List of modules to patch.
167+
168+
>>> patch(psycopg=True, elasticsearch=True)
169+
"""
170+
modules = [m for (m, should_patch) in patch_modules.items() if should_patch]
171+
for module in modules:
172+
if module in _PATCH_ON_IMPORT:
173+
modules_to_poi = _PATCH_ON_IMPORT[module]
174+
for m in modules_to_poi:
175+
# If the module has already been imported then patch immediately
176+
if m in sys.modules:
177+
_patch_module(module, raise_errors=raise_errors)
178+
break
179+
# Otherwise, add a hook to patch when it is imported for the first time
180+
else:
181+
# Use factory to create handler to close over `module` and `raise_errors` values from this loop
182+
when_imported(m)(_on_import_factory(module, raise_errors))
183+
184+
# manually add module to patched modules
185+
with _LOCK:
186+
_PATCHED_MODULES.add(module)
187+
else:
188+
_patch_module(module, raise_errors=raise_errors)
189+
190+
patched_modules = _get_patched_modules()
191+
log.info(
192+
"patched %s/%s modules (%s)",
193+
len(patched_modules),
194+
len(modules),
195+
",".join(patched_modules),
196+
)
197+
198+
199+
@deprecated(
200+
message="This function will be removed.",
201+
version="1.0.0",
202+
)
203+
def patch_module(module, raise_errors=True):
204+
# type: (str, bool) -> bool
205+
return _patch_module(module, raise_errors=raise_errors)
206+
207+
208+
def _patch_module(module, raise_errors=True):
209+
# type: (str, bool) -> bool
210+
"""Patch a single module
211+
212+
Returns if the module got properly patched.
213+
"""
214+
try:
215+
return _attempt_patch_module(module)
216+
except ModuleNotFoundException:
217+
if raise_errors:
218+
raise
219+
return False
220+
except Exception:
221+
if raise_errors:
222+
raise
223+
log.debug("failed to patch %s", module, exc_info=True)
224+
return False
225+
226+
227+
@deprecated(
228+
message="This function will be removed.",
229+
version="1.0.0",
230+
)
231+
def get_patched_modules():
232+
# type: () -> List[str]
233+
return _get_patched_modules()
234+
235+
236+
def _get_patched_modules():
237+
# type: () -> List[str]
238+
"""Get the list of patched modules"""
239+
with _LOCK:
240+
return sorted(_PATCHED_MODULES)
241+
242+
243+
def _attempt_patch_module(module):
244+
# type: (str) -> bool
245+
"""_patch_module will attempt to monkey patch the module.
246+
247+
Returns if the module got patched.
248+
Can also raise errors if it fails.
249+
"""
250+
path = "ddtrace.contrib.%s" % module
251+
with _LOCK:
252+
if module in _PATCHED_MODULES and module not in _PATCH_ON_IMPORT:
253+
log.debug("already patched: %s", path)
254+
return False
255+
256+
try:
257+
imported_module = importlib.import_module(path)
258+
except ImportError:
259+
# if the import fails, the integration is not available
260+
raise ModuleNotFoundException(
261+
"integration module %s does not exist, module will not have tracing available" % path
262+
)
263+
else:
264+
# if patch() is not available in the module, it means
265+
# that the library is not installed in the environment
266+
if not hasattr(imported_module, "patch"):
267+
raise AttributeError(
268+
"%s.patch is not found. '%s' is not configured for this environment" % (path, module)
269+
)
270+
271+
imported_module.patch() # type: ignore
272+
_PATCHED_MODULES.add(module)
273+
return True

ddtrace/internal/debug.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def collect(tracer):
7878
# TODO: this check doesn't work in all cases... we need a mapping
7979
# between the module and the library name.
8080
module_available = module in packages_available
81-
module_instrumented = module in ddtrace.monkey._PATCHED_MODULES
81+
module_instrumented = module in ddtrace._monkey._PATCHED_MODULES
8282
module_imported = module in sys.modules
8383

8484
if enabled:

0 commit comments

Comments
 (0)