173 lines
5.7 KiB
Python
173 lines
5.7 KiB
Python
"""
|
|
IMPORTANT: The contents of this file are part of a proof of concept and as such
|
|
are experimental and not suitable for production use. They may be changed or
|
|
removed at any time without prior notice.
|
|
"""
|
|
import sys
|
|
from importlib import import_module
|
|
|
|
from sentry_sdk.integrations import DidNotEnable, Integration
|
|
from sentry_sdk.integrations.opentelemetry.span_processor import SentrySpanProcessor
|
|
from sentry_sdk.integrations.opentelemetry.propagator import SentryPropagator
|
|
from sentry_sdk.utils import logger, _get_installed_modules
|
|
from sentry_sdk._types import TYPE_CHECKING
|
|
|
|
try:
|
|
from opentelemetry import trace # type: ignore
|
|
from opentelemetry.instrumentation.auto_instrumentation._load import ( # type: ignore
|
|
_load_distro,
|
|
_load_instrumentors,
|
|
)
|
|
from opentelemetry.propagate import set_global_textmap # type: ignore
|
|
from opentelemetry.sdk.trace import TracerProvider # type: ignore
|
|
except ImportError:
|
|
raise DidNotEnable("opentelemetry not installed")
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Dict
|
|
|
|
|
|
CLASSES_TO_INSTRUMENT = {
|
|
# A mapping of packages to their entry point class that will be instrumented.
|
|
# This is used to post-instrument any classes that were imported before OTel
|
|
# instrumentation took place.
|
|
"fastapi": "fastapi.FastAPI",
|
|
"flask": "flask.Flask",
|
|
}
|
|
|
|
|
|
class OpenTelemetryIntegration(Integration):
|
|
identifier = "opentelemetry"
|
|
|
|
@staticmethod
|
|
def setup_once():
|
|
# type: () -> None
|
|
logger.warning(
|
|
"[OTel] Initializing highly experimental OpenTelemetry support. "
|
|
"Use at your own risk."
|
|
)
|
|
|
|
original_classes = _record_unpatched_classes()
|
|
|
|
try:
|
|
distro = _load_distro()
|
|
distro.configure()
|
|
_load_instrumentors(distro)
|
|
except Exception:
|
|
logger.exception("[OTel] Failed to auto-initialize OpenTelemetry")
|
|
|
|
try:
|
|
_patch_remaining_classes(original_classes)
|
|
except Exception:
|
|
logger.exception(
|
|
"[OTel] Failed to post-patch instrumented classes. "
|
|
"You might have to make sure sentry_sdk.init() is called before importing anything else."
|
|
)
|
|
|
|
_setup_sentry_tracing()
|
|
|
|
logger.debug("[OTel] Finished setting up OpenTelemetry integration")
|
|
|
|
|
|
def _record_unpatched_classes():
|
|
# type: () -> Dict[str, type]
|
|
"""
|
|
Keep references to classes that are about to be instrumented.
|
|
|
|
Used to search for unpatched classes after the instrumentation has run so
|
|
that they can be patched manually.
|
|
"""
|
|
installed_packages = _get_installed_modules()
|
|
|
|
original_classes = {}
|
|
|
|
for package, orig_path in CLASSES_TO_INSTRUMENT.items():
|
|
if package in installed_packages:
|
|
try:
|
|
original_cls = _import_by_path(orig_path)
|
|
except (AttributeError, ImportError):
|
|
logger.debug("[OTel] Failed to import %s", orig_path)
|
|
continue
|
|
|
|
original_classes[package] = original_cls
|
|
|
|
return original_classes
|
|
|
|
|
|
def _patch_remaining_classes(original_classes):
|
|
# type: (Dict[str, type]) -> None
|
|
"""
|
|
Best-effort attempt to patch any uninstrumented classes in sys.modules.
|
|
|
|
This enables us to not care about the order of imports and sentry_sdk.init()
|
|
in user code. If e.g. the Flask class had been imported before sentry_sdk
|
|
was init()ed (and therefore before the OTel instrumentation ran), it would
|
|
not be instrumented. This function goes over remaining uninstrumented
|
|
occurrences of the class in sys.modules and replaces them with the
|
|
instrumented class.
|
|
|
|
Since this is looking for exact matches, it will not work in some scenarios
|
|
(e.g. if someone is not using the specific class explicitly, but rather
|
|
inheriting from it). In those cases it's still necessary to sentry_sdk.init()
|
|
before importing anything that's supposed to be instrumented.
|
|
"""
|
|
# check which classes have actually been instrumented
|
|
instrumented_classes = {}
|
|
|
|
for package in list(original_classes.keys()):
|
|
original_path = CLASSES_TO_INSTRUMENT[package]
|
|
|
|
try:
|
|
cls = _import_by_path(original_path)
|
|
except (AttributeError, ImportError):
|
|
logger.debug(
|
|
"[OTel] Failed to check if class has been instrumented: %s",
|
|
original_path,
|
|
)
|
|
del original_classes[package]
|
|
continue
|
|
|
|
if not cls.__module__.startswith("opentelemetry."):
|
|
del original_classes[package]
|
|
continue
|
|
|
|
instrumented_classes[package] = cls
|
|
|
|
if not instrumented_classes:
|
|
return
|
|
|
|
# replace occurrences of the original unpatched class in sys.modules
|
|
for module_name, module in sys.modules.copy().items():
|
|
if (
|
|
module_name.startswith("sentry_sdk")
|
|
or module_name in sys.builtin_module_names
|
|
):
|
|
continue
|
|
|
|
for package, original_cls in original_classes.items():
|
|
for var_name, var in vars(module).copy().items():
|
|
if var == original_cls:
|
|
logger.debug(
|
|
"[OTel] Additionally patching %s from %s",
|
|
original_cls,
|
|
module_name,
|
|
)
|
|
|
|
setattr(module, var_name, instrumented_classes[package])
|
|
|
|
|
|
def _import_by_path(path):
|
|
# type: (str) -> type
|
|
parts = path.rsplit(".", maxsplit=1)
|
|
return getattr(import_module(parts[0]), parts[-1])
|
|
|
|
|
|
def _setup_sentry_tracing():
|
|
# type: () -> None
|
|
provider = TracerProvider()
|
|
|
|
provider.add_span_processor(SentrySpanProcessor())
|
|
|
|
trace.set_tracer_provider(provider)
|
|
|
|
set_global_textmap(SentryPropagator())
|