Files
alrex ed6dd7f257 Removing support for python 2.7 (#2)
OpenTelemetry doesn't support Python 2.7. Removing it from the tests and removing contrib packages that don't support Python 3. In this change:

- removing pylons contrib
- removing mysqldb contrib
- updating minimum versions of flask (>=1.0), gevent (>=1.1)

Signed-off-by: Alex Boten <aboten@lightstep.com>
2020-03-23 09:43:03 -07:00

190 lines
5.5 KiB
Python

"""Patch librairies to be automatically instrumented.
It can monkey patch supported standard libraries and third party modules.
A patched module will automatically report spans with its default configuration.
A library instrumentation can be configured (for instance, to report as another service)
using Pin. For that, check its documentation.
"""
import importlib
import sys
import threading
from ddtrace.vendor.wrapt.importer import when_imported
from .internal.logger import get_logger
log = get_logger(__name__)
# Default set of modules to automatically patch or not
PATCH_MODULES = {
'asyncio': False,
'boto': True,
'botocore': True,
'bottle': False,
'cassandra': True,
'celery': True,
'consul': True,
'elasticsearch': True,
'algoliasearch': True,
'futures': False, # experimental propagation
'grpc': True,
'mongoengine': True,
'mysql': True,
'mysqldb': True,
'pymysql': True,
'psycopg': True,
'pylibmc': True,
'pymemcache': True,
'pymongo': True,
'redis': True,
'rediscluster': True,
'requests': True,
'sqlalchemy': False, # Prefer DB client instrumentation
'sqlite3': True,
'aiohttp': True, # requires asyncio (Python 3.4+)
'aiopg': True,
'aiobotocore': False,
'httplib': False,
'vertica': True,
'molten': True,
'jinja2': True,
'mako': True,
'flask': True,
'kombu': False,
# Ignore some web framework integrations that might be configured explicitly in code
'django': False,
'falcon': False,
'pyramid': False,
# Standard library modules off by default
'logging': False,
}
_LOCK = threading.Lock()
_PATCHED_MODULES = set()
# Modules which are patched on first use
# DEV: These modules are patched when the user first imports them, rather than
# explicitly importing and patching them on application startup `ddtrace.patch_all(module=True)`
# DEV: This ensures we do not patch a module until it is needed
# DEV: <contrib name> => <list of module names that trigger a patch>
_PATCH_ON_IMPORT = {
'celery': ('celery', ),
'flask': ('flask, '),
'gevent': ('gevent', ),
'requests': ('requests', ),
}
class PatchException(Exception):
"""Wraps regular `Exception` class when patching modules"""
pass
def _on_import_factory(module, raise_errors=True):
"""Factory to create an import hook for the provided module name"""
def on_import(hook):
# Import and patch module
path = 'ddtrace.contrib.%s' % module
imported_module = importlib.import_module(path)
imported_module.patch()
return on_import
def patch_all(**patch_modules):
"""Automatically patches all available modules.
:param dict patch_modules: Override whether particular modules are patched or not.
>>> patch_all(redis=False, cassandra=False)
"""
modules = PATCH_MODULES.copy()
modules.update(patch_modules)
patch(raise_errors=False, **modules)
def patch(raise_errors=True, **patch_modules):
"""Patch only a set of given modules.
:param bool raise_errors: Raise error if one patch fail.
:param dict patch_modules: List of modules to patch.
>>> patch(psycopg=True, elasticsearch=True)
"""
modules = [m for (m, should_patch) in patch_modules.items() if should_patch]
for module in modules:
if module in _PATCH_ON_IMPORT:
# If the module has already been imported then patch immediately
if module in sys.modules:
patch_module(module, raise_errors=raise_errors)
# Otherwise, add a hook to patch when it is imported for the first time
else:
# Use factory to create handler to close over `module` and `raise_errors` values from this loop
when_imported(module)(_on_import_factory(module, raise_errors))
# manually add module to patched modules
_PATCHED_MODULES.add(module)
else:
patch_module(module, raise_errors=raise_errors)
patched_modules = get_patched_modules()
log.info(
'patched %s/%s modules (%s)',
len(patched_modules),
len(modules),
','.join(patched_modules),
)
def patch_module(module, raise_errors=True):
"""Patch a single module
Returns if the module got properly patched.
"""
try:
return _patch_module(module)
except Exception:
if raise_errors:
raise
log.debug('failed to patch %s', module, exc_info=True)
return False
def get_patched_modules():
"""Get the list of patched modules"""
with _LOCK:
return sorted(_PATCHED_MODULES)
def _patch_module(module):
"""_patch_module will attempt to monkey patch the module.
Returns if the module got patched.
Can also raise errors if it fails.
"""
path = 'ddtrace.contrib.%s' % module
with _LOCK:
if module in _PATCHED_MODULES and module not in _PATCH_ON_IMPORT:
log.debug('already patched: %s', path)
return False
try:
imported_module = importlib.import_module(path)
imported_module.patch()
except ImportError:
# if the import fails, the integration is not available
raise PatchException('integration not available')
except AttributeError:
# if patch() is not available in the module, it means
# that the library is not installed in the environment
raise PatchException('module not installed')
_PATCHED_MODULES.add(module)
return True