mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-28 20:52:57 +08:00

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>
190 lines
5.5 KiB
Python
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
|