diff --git a/ddtrace/_trace/product.py b/ddtrace/_trace/product.py index f1fddc45223..34221020f41 100644 --- a/ddtrace/_trace/product.py +++ b/ddtrace/_trace/product.py @@ -36,14 +36,17 @@ def post_preload(): _patch_all(**modules_to_bool) +def enabled(): + return _config.enabled + + def start(): - if _config.enabled: - from ddtrace.internal.settings._config import config + from ddtrace.internal.settings._config import config - if config._trace_methods: - from ddtrace.internal.tracemethods import _install_trace_methods + if config._trace_methods: + from ddtrace.internal.tracemethods import _install_trace_methods - _install_trace_methods(config._trace_methods) + _install_trace_methods(config._trace_methods) def restart(join=False): diff --git a/ddtrace/debugging/_products/code_origin/span.py b/ddtrace/debugging/_products/code_origin/span.py index a7dac660998..9f22ecbce23 100644 --- a/ddtrace/debugging/_products/code_origin/span.py +++ b/ddtrace/debugging/_products/code_origin/span.py @@ -20,50 +20,42 @@ # requires = ["tracer"] -def post_preload() -> None: - pass +# We need to instrument the entrypoints on boot because this is the only +# time the tracer will notify us of entrypoints being registered. +@partial(core.on, "service_entrypoint.patch") +def _(f: t.Union[FunctionType, MethodType]) -> None: + from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry + SpanCodeOriginProcessorEntry.instrument_view(f) -def _start() -> None: - from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry - SpanCodeOriginProcessorEntry.enable() +def post_preload() -> None: + pass def start() -> None: - # We need to instrument the entrypoints on boot because this is the only - # time the tracer will notify us of entrypoints being registered. - @partial(core.on, "service_entrypoint.patch") - def _(f: t.Union[FunctionType, MethodType]) -> None: - from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry + from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry - SpanCodeOriginProcessorEntry.instrument_view(f) + SpanCodeOriginProcessorEntry.enable() - log.debug("Registered entrypoint patching hook for code origin for spans") +def enabled() -> bool: # If dynamic instrumentation is enabled, and code origin for spans is not explicitly disabled, # we'll enable code origin for spans. di_enabled = product_manager.is_enabled(DI_PRODUCT_KEY) and config.value_source(CO_ENABLED) == ValueSource.DEFAULT - if config.span.enabled or di_enabled: - _start() + return config.span.enabled or di_enabled def restart(join: bool = False) -> None: pass -def _stop() -> None: +def stop(join: bool = False) -> None: from ddtrace.debugging._origin.span import SpanCodeOriginProcessorEntry SpanCodeOriginProcessorEntry.disable() -def stop(join: bool = False) -> None: - di_enabled = product_manager.is_enabled(DI_PRODUCT_KEY) and config.value_source(CO_ENABLED) == ValueSource.DEFAULT - if config.span.enabled or di_enabled: - _stop() - - def at_exit(join: bool = False) -> None: stop(join=join) @@ -75,4 +67,4 @@ class APMCapabilities(enum.IntFlag): def apm_tracing_rc(lib_config: t.Any, _config: t.Any) -> None: if (enabled := lib_config.get("code_origin_enabled")) is not None: should_start = (config.span.spec.enabled.full_name not in config.source or config.span.enabled) and enabled - _start() if should_start else _stop() + start() if should_start else stop() diff --git a/ddtrace/debugging/_products/dynamic_instrumentation.py b/ddtrace/debugging/_products/dynamic_instrumentation.py index 28fab8f8b55..947a80e4969 100644 --- a/ddtrace/debugging/_products/dynamic_instrumentation.py +++ b/ddtrace/debugging/_products/dynamic_instrumentation.py @@ -1,9 +1,15 @@ import enum from typing import Any +from ddtrace.debugging._import import DebuggerModuleWatchdog from ddtrace.internal.settings.dynamic_instrumentation import config +# We need to install this on start-up because if DI gets enabled remotely +# we won't be able to capture many of the code objects from the modules +# that are already loaded. +DebuggerModuleWatchdog.install() + requires = ["remote-configuration"] @@ -11,22 +17,14 @@ def post_preload() -> None: pass -def _start() -> None: +def start() -> None: from ddtrace.debugging import DynamicInstrumentation DynamicInstrumentation.enable() -def start() -> None: - from ddtrace.debugging._import import DebuggerModuleWatchdog - - # We need to install this on start-up because if DI gets enabled remotely - # we won't be able to capture many of the code objects from the modules - # that are already loaded. - DebuggerModuleWatchdog.install() - - if config.enabled: - _start() +def enabled() -> bool: + return config.enabled def before_fork() -> None: @@ -40,10 +38,9 @@ def restart(join: bool = False) -> None: def stop(join: bool = False) -> None: - if config.enabled: - from ddtrace.debugging import DynamicInstrumentation + from ddtrace.debugging import DynamicInstrumentation - DynamicInstrumentation.disable(join=join) + DynamicInstrumentation.disable(join=join) def at_exit(join: bool = False) -> None: @@ -57,4 +54,4 @@ class APMCapabilities(enum.IntFlag): def apm_tracing_rc(lib_config: Any, _config: Any) -> None: if (enabled := lib_config.get("dynamic_instrumentation_enabled")) is not None: should_start = (config.spec.enabled.full_name not in config.source or config.enabled) and enabled - _start() if should_start else stop() + start() if should_start else stop() diff --git a/ddtrace/debugging/_products/exception_replay.py b/ddtrace/debugging/_products/exception_replay.py index fd53e8a05a4..6bfda9cf92b 100644 --- a/ddtrace/debugging/_products/exception_replay.py +++ b/ddtrace/debugging/_products/exception_replay.py @@ -12,32 +12,26 @@ def post_preload() -> None: pass -def _start() -> None: +def start() -> None: from ddtrace.debugging._exception.replay import SpanExceptionHandler SpanExceptionHandler.enable() -def start() -> None: - if config.enabled: - _start() +def enabled() -> bool: + return config.enabled def restart(join: bool = False) -> None: pass -def _stop() -> None: +def stop(join: bool = False) -> None: from ddtrace.debugging._exception.replay import SpanExceptionHandler SpanExceptionHandler.disable() -def stop(join: bool = False) -> None: - if config.enabled: - _stop() - - def at_exit(join: bool = False) -> None: stop(join=join) @@ -49,4 +43,4 @@ class APMCapabilities(enum.IntFlag): def apm_tracing_rc(lib_config: Any, _config: Any) -> None: if (enabled := lib_config.get("exception_replay_enabled")) is not None: should_start = (config.spec.enabled.full_name not in config.source or config.enabled) and enabled - _start() if should_start else _stop() + start() if should_start else stop() diff --git a/ddtrace/debugging/_products/live_debugger.py b/ddtrace/debugging/_products/live_debugger.py index 2c89ee68d16..0cec879b6c7 100644 --- a/ddtrace/debugging/_products/live_debugger.py +++ b/ddtrace/debugging/_products/live_debugger.py @@ -9,11 +9,14 @@ def post_preload() -> None: pass +def enabled() -> bool: + return config.enabled + + def start() -> None: - if config.enabled: - from ddtrace.debugging._live import enable + from ddtrace.debugging._live import enable - enable() + enable() def restart(join: bool = False) -> None: @@ -21,10 +24,9 @@ def restart(join: bool = False) -> None: def stop(join: bool = False) -> None: - if config.enabled: - from ddtrace.debugging._live import disable + from ddtrace.debugging._live import disable - disable() + disable() def at_exit(join: bool = False) -> None: diff --git a/ddtrace/errortracking/product.py b/ddtrace/errortracking/product.py index 3540143e0f0..f7b5383019d 100644 --- a/ddtrace/errortracking/product.py +++ b/ddtrace/errortracking/product.py @@ -12,11 +12,14 @@ def post_preload(): pass +def enabled() -> bool: + return config.enabled + + def start() -> None: - if config.enabled: - from ddtrace.errortracking._handled_exceptions.collector import HandledExceptionCollector + from ddtrace.errortracking._handled_exceptions.collector import HandledExceptionCollector - HandledExceptionCollector.enable() + HandledExceptionCollector.enable() def restart(join: bool = False) -> None: @@ -24,10 +27,9 @@ def restart(join: bool = False) -> None: def stop(join: bool = False): - if config.enabled: - from ddtrace.errortracking._handled_exceptions.collector import HandledExceptionCollector + from ddtrace.errortracking._handled_exceptions.collector import HandledExceptionCollector - HandledExceptionCollector.disable() + HandledExceptionCollector.disable() def at_exit(join: bool = False): diff --git a/ddtrace/internal/README.md b/ddtrace/internal/README.md index 787cd25626b..881596a7ef1 100644 --- a/ddtrace/internal/README.md +++ b/ddtrace/internal/README.md @@ -15,11 +15,12 @@ object that implements the product protocol. This consists of a Python object | Attribute | Description | |-----------|-------------| -| `start() -> None` | A function with the logic required to start the product | +| `post_preload() -> None` | A function with the logic required to finish initialization after the library preload stage | +| `enabled() -> bool` | A function that returns whether the product should be started; called before `start()` by the product manager | +| `start() -> None` | A function with the logic required to enable the product (called only when `enabled()` returns `True`) | | `restart(join: bool = False) -> None` | A function with the logic required to restart the product after a fork | | `stop(join: bool = False) -> None` | A function with the logic required to stop the product | | `at_exit(join: bool = False) -> None` | A function with the logic required to stop the product at exit | -| `post_preload() -> None` | A function with the logic required to finish initialization after the library preload stage | The product object needs to be made available to the Python plugin system by defining an entry point in the `project.entry-points.'ddtrace.products'` section diff --git a/ddtrace/internal/appsec/product.py b/ddtrace/internal/appsec/product.py index 0e54e71c276..a5657e97796 100644 --- a/ddtrace/internal/appsec/product.py +++ b/ddtrace/internal/appsec/product.py @@ -9,6 +9,12 @@ def post_preload(): pass +def enabled(): + return ( + config._asm_enabled or config._asm_can_be_enabled or config._asm_rc_enabled or ai_guard_config._ai_guard_enabled + ) + + def start(): if config._asm_enabled or config._asm_can_be_enabled: from ddtrace.appsec._listeners import load_common_appsec_modules diff --git a/ddtrace/internal/iast/product.py b/ddtrace/internal/iast/product.py index 792c989f025..7f2b9145890 100644 --- a/ddtrace/internal/iast/product.py +++ b/ddtrace/internal/iast/product.py @@ -91,14 +91,20 @@ def post_preload(): # Dropping inspect causes: "unexpected object in __signature__ attribute" +def enabled(): + """ + Return whether the IAST product is enabled. + """ + return asm_config._iast_enabled + + def start(): """ Start the IAST product. """ - if asm_config._iast_enabled: - from ddtrace.appsec._iast.processor import AppSecIastSpanProcessor + from ddtrace.appsec._iast.processor import AppSecIastSpanProcessor - AppSecIastSpanProcessor.enable() + AppSecIastSpanProcessor.enable() def restart(join=False): diff --git a/ddtrace/internal/openfeature/product.py b/ddtrace/internal/openfeature/product.py index 54d9a08b265..f92f9fa9a19 100644 --- a/ddtrace/internal/openfeature/product.py +++ b/ddtrace/internal/openfeature/product.py @@ -8,11 +8,14 @@ def post_preload(): pass +def enabled(): + return ffe_config.experimental_flagging_provider_enabled + + def start(): - if ffe_config.experimental_flagging_provider_enabled: - from ddtrace.internal.openfeature._remoteconfiguration import enable_featureflags_rc + from ddtrace.internal.openfeature._remoteconfiguration import enable_featureflags_rc - enable_featureflags_rc() + enable_featureflags_rc() def restart(join=False): @@ -20,10 +23,9 @@ def restart(join=False): def stop(join=False): - if ffe_config.experimental_flagging_provider_enabled: - from ddtrace.internal.openfeature._remoteconfiguration import disable_featureflags_rc + from ddtrace.internal.openfeature._remoteconfiguration import disable_featureflags_rc - disable_featureflags_rc() + disable_featureflags_rc() def at_exit(join=False): diff --git a/ddtrace/internal/products.py b/ddtrace/internal/products.py index 34296ebdadf..45377e39823 100644 --- a/ddtrace/internal/products.py +++ b/ddtrace/internal/products.py @@ -37,6 +37,8 @@ class Product(Protocol): def post_preload(self) -> None: ... + def enabled(self) -> bool: ... + def start(self) -> None: ... def restart(self, join: bool = False) -> None: ... @@ -129,6 +131,9 @@ def start_products(self) -> None: continue try: + if not product.enabled(): + log.debug("Product '%s' is not enabled, skipping", name) + continue product.start() log.debug("Started product '%s'", name) telemetry_writer.product_activated(name.replace("-", "_"), True) @@ -169,6 +174,8 @@ def restart_products(self, join: bool = False) -> None: def stop_products(self, join: bool = False) -> None: for name, product in reversed(self.products): try: + if not product.enabled(): + continue product.stop(join=join) log.debug("Stopped product '%s'", name) telemetry_writer.product_activated(name.replace("-", "_"), False) @@ -178,6 +185,8 @@ def stop_products(self, join: bool = False) -> None: def exit_products(self, join: bool = False) -> None: for name, product in reversed(self.products): try: + if not product.enabled(): + continue log.debug("Exiting product '%s'", name) product.at_exit(join=join) except Exception: @@ -232,14 +241,11 @@ def _() -> None: else: self._do_products() - def is_enabled(self, product_name: str, enabled_attribute: str = "enabled") -> bool: + def is_enabled(self, product_name: str) -> bool: if (product := self.__products__.get(product_name)) is None: return False - if (config := getattr(product, "config", None)) is None: - return False - - return getattr(config, enabled_attribute, False) + return product.enabled() manager = ProductManager() diff --git a/ddtrace/internal/remoteconfig/products/apm_tracing.py b/ddtrace/internal/remoteconfig/products/apm_tracing.py index ec327baa59f..c666079be6d 100644 --- a/ddtrace/internal/remoteconfig/products/apm_tracing.py +++ b/ddtrace/internal/remoteconfig/products/apm_tracing.py @@ -119,23 +119,26 @@ def post_preload(): pass +def enabled() -> bool: + return config._remote_config_enabled + + def start(): - if config._remote_config_enabled: - from ddtrace.internal.products import manager - from ddtrace.internal.remoteconfig.worker import remoteconfig_poller - - remoteconfig_poller.register( - "APM_TRACING", - APMTracingCallback(), - capabilities=[ - cap for product in manager.__products__.values() for cap in getattr(product, "APMCapabilities", []) - ], - ) + from ddtrace.internal.products import manager + from ddtrace.internal.remoteconfig.worker import remoteconfig_poller + + remoteconfig_poller.register( + "APM_TRACING", + APMTracingCallback(), + capabilities=[ + cap for product in manager.__products__.values() for cap in getattr(product, "APMCapabilities", []) + ], + ) - # Register remote config handlers - for name, product in manager.__products__.items(): - if (rc_handler := getattr(product, "apm_tracing_rc", None)) is not None: - on("apm-tracing.rc", rc_handler, name) + # Register remote config handlers + for name, product in manager.__products__.items(): + if (rc_handler := getattr(product, "apm_tracing_rc", None)) is not None: + on("apm-tracing.rc", rc_handler, name) def restart(join=False): @@ -143,10 +146,9 @@ def restart(join=False): def stop(join=False): - if config._remote_config_enabled: - from ddtrace.internal.remoteconfig.worker import remoteconfig_poller + from ddtrace.internal.remoteconfig.worker import remoteconfig_poller - remoteconfig_poller.unregister("APM_TRACING") + remoteconfig_poller.unregister("APM_TRACING") def at_exit(join=False): diff --git a/ddtrace/internal/remoteconfig/products/client.py b/ddtrace/internal/remoteconfig/products/client.py index 9a83e3ce722..508a65a7c31 100644 --- a/ddtrace/internal/remoteconfig/products/client.py +++ b/ddtrace/internal/remoteconfig/products/client.py @@ -35,12 +35,15 @@ def post_preload(): pass +def enabled(): + return config._remote_config_enabled + + def start(): - if config._remote_config_enabled: - from ddtrace.internal.remoteconfig.worker import remoteconfig_poller + from ddtrace.internal.remoteconfig.worker import remoteconfig_poller - remoteconfig_poller.enable() - _register_rc_products() + remoteconfig_poller.enable() + _register_rc_products() def restart(join=False): @@ -51,12 +54,11 @@ def restart(join=False): def stop(join=False): - if config._remote_config_enabled: - from ddtrace.internal.remoteconfig.worker import remoteconfig_poller + from ddtrace.internal.remoteconfig.worker import remoteconfig_poller - remoteconfig_poller.disable(join=join) + remoteconfig_poller.disable(join=join) def at_exit(join=False): - if config._remote_config_enabled and not rc_config.skip_shutdown: + if not rc_config.skip_shutdown: stop(join=join) diff --git a/ddtrace/internal/symbol_db/product.py b/ddtrace/internal/symbol_db/product.py index bbc493d63e6..b8f4beb057a 100644 --- a/ddtrace/internal/symbol_db/product.py +++ b/ddtrace/internal/symbol_db/product.py @@ -8,11 +8,14 @@ def post_preload(): pass +def enabled(): + return config.enabled + + def start(): - if config.enabled: - from ddtrace.internal import symbol_db + from ddtrace.internal import symbol_db - symbol_db.bootstrap() + symbol_db.bootstrap() def restart(join=False): diff --git a/tests/internal/test_products.py b/tests/internal/test_products.py index b2404a05d63..9685d0993e8 100644 --- a/tests/internal/test_products.py +++ b/tests/internal/test_products.py @@ -9,6 +9,7 @@ def __init__(self, products, failed=set()) -> None: self._products = None self.__products__ = products self._failed = failed + self._started = set() class BaseProduct(Product): @@ -20,6 +21,9 @@ def __init__(self) -> None: def post_preload(self) -> None: self.post_preloaded = True + def enabled(self) -> bool: + return True + def start(self) -> None: self.started = True @@ -116,25 +120,20 @@ def test_product_manager_restart(): def test_product_manager_is_enabled(): - class ProductWithConfig: - def __init__(self, enabled): - self.config = type("Config", (), {"enabled": enabled})() + class DisabledProduct(BaseProduct): + def enabled(self) -> bool: + return False # Test when product doesn't exist manager = ProductManagerTest({}) assert not manager.is_enabled("nonexistent") - # Test when product exists but has no config - product_no_config = BaseProduct() - manager = ProductManagerTest({"no_config": product_no_config}) - assert not manager.is_enabled("no_config") - # Test when product exists and is enabled - enabled_product = ProductWithConfig(True) + enabled_product = BaseProduct() manager = ProductManagerTest({"enabled": enabled_product}) assert manager.is_enabled("enabled") # Test when product exists but is disabled - disabled_product = ProductWithConfig(False) + disabled_product = DisabledProduct() manager = ProductManagerTest({"disabled": disabled_product}) assert not manager.is_enabled("disabled")