From 35eeb9a32424129a8a5e4ddb80cc76bccce42185 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Thu, 16 Jan 2025 10:59:08 +0000 Subject: [PATCH 01/12] chore: introduce APM_TRACING RC product We introduce the APM_TRACING remote configuration product that allows dispatching remote configuration to the library for remote enablement/ configuration of library components and features. --- ddtrace/debugging/_debugger.py | 3 - .../_products/dynamic_instrumentation.py | 38 ++++++++- .../remoteconfig/products/__init__.py | 0 .../remoteconfig/products/apm_tracing.py | 83 +++++++++++++++++++ .../{product.py => products/client.py} | 0 pyproject.toml | 3 +- tests/debugging/mocking.py | 6 +- 7 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 ddtrace/internal/remoteconfig/products/__init__.py create mode 100644 ddtrace/internal/remoteconfig/products/apm_tracing.py rename ddtrace/internal/remoteconfig/{product.py => products/client.py} (100%) diff --git a/ddtrace/debugging/_debugger.py b/ddtrace/debugging/_debugger.py index ee503da2bbd..ef863790a84 100644 --- a/ddtrace/debugging/_debugger.py +++ b/ddtrace/debugging/_debugger.py @@ -275,8 +275,6 @@ def enable(cls) -> None: di_config.enabled = True - cls.__watchdog__.install() - if di_config.metrics: metrics.enable() @@ -308,7 +306,6 @@ def disable(cls, join: bool = True) -> None: cls._instance.stop(join=join) cls._instance = None - cls.__watchdog__.uninstall() if di_config.metrics: metrics.disable() diff --git a/ddtrace/debugging/_products/dynamic_instrumentation.py b/ddtrace/debugging/_products/dynamic_instrumentation.py index 136d5692ec8..46a36cc0473 100644 --- a/ddtrace/debugging/_products/dynamic_instrumentation.py +++ b/ddtrace/debugging/_products/dynamic_instrumentation.py @@ -1,3 +1,5 @@ +import enum + from ddtrace.settings.dynamic_instrumentation import config @@ -5,14 +7,23 @@ def post_preload(): - pass + from ddtrace.debugging._debugger import Debugger + + # 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. + Debugger.__watchdog__.install() + + +def _start(): + from ddtrace.debugging import DynamicInstrumentation + + DynamicInstrumentation.enable() def start(): if config.enabled: - from ddtrace.debugging import DynamicInstrumentation - - DynamicInstrumentation.enable() + _start() def restart(join=False): @@ -29,3 +40,22 @@ def stop(join=False): def at_exit(join=False): stop(join=join) + + +class APMCapabilities(enum.IntFlag): + APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION = 1 << 38 + + +def apm_tracing_rc(lib_config): + enabled = lib_config.get("dynamic_instrumentation_enabled") + try: + if enabled is not None: # and config.spec.enabled.full_name not in config.source: + if (config.spec.enabled.full_name not in config.source or config.enabled) and enabled: + _start() + else: + stop() + except Exception: + from traceback import print_exc + + print_exc() + raise diff --git a/ddtrace/internal/remoteconfig/products/__init__.py b/ddtrace/internal/remoteconfig/products/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ddtrace/internal/remoteconfig/products/apm_tracing.py b/ddtrace/internal/remoteconfig/products/apm_tracing.py new file mode 100644 index 00000000000..392b458a090 --- /dev/null +++ b/ddtrace/internal/remoteconfig/products/apm_tracing.py @@ -0,0 +1,83 @@ +from ddtrace import config +from ddtrace.internal.core.event_hub import dispatch +from ddtrace.internal.core.event_hub import on +from ddtrace.internal.logger import get_logger +from ddtrace.internal.remoteconfig._connectors import PublisherSubscriberConnector +from ddtrace.internal.remoteconfig._publishers import RemoteConfigPublisher +from ddtrace.internal.remoteconfig._pubsub import PubSub +from ddtrace.internal.remoteconfig._subscribers import RemoteConfigSubscriber + + +requires = ["remote-configuration"] + + +log = get_logger(__name__) + + +def _rc_callback(data, test_tracer=None): + for metadata, data in zip(data["metadata"], data["config"]): + if metadata is None or not isinstance(data, dict): + continue + + service_target = data.get("service_target") + if service_target is not None: + service = service_target.get("service") + if service is not None and service != config.service: + continue + + env = service_target.get("env") + if env is not None and env != config.env: + continue + + lib_config = data.get("lib_config") + if lib_config is not None: + dispatch("apm-tracing.rc", (lib_config,)) + + +class APMTracingAdapter(PubSub): + __publisher_class__ = RemoteConfigPublisher + __subscriber_class__ = RemoteConfigSubscriber + __shared_data__ = PublisherSubscriberConnector() + + def __init__(self): + self._publisher = self.__publisher_class__(self.__shared_data__) + self._subscriber = self.__subscriber_class__(self.__shared_data__, _rc_callback, "APM_TRACING") + + +def post_preload(): + pass + + +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", + APMTracingAdapter(), + restart_on_fork=True, + 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) + + +def restart(join=False): + pass + + +def stop(join=False): + if config._remote_config_enabled: + from ddtrace.internal.remoteconfig.worker import remoteconfig_poller + + remoteconfig_poller.unregister("APM_TRACING") + + +def at_exit(join=False): + stop(join=join) diff --git a/ddtrace/internal/remoteconfig/product.py b/ddtrace/internal/remoteconfig/products/client.py similarity index 100% rename from ddtrace/internal/remoteconfig/product.py rename to ddtrace/internal/remoteconfig/products/client.py diff --git a/pyproject.toml b/pyproject.toml index 64be4ab4b59..b1c553c09f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,11 +59,12 @@ ddtrace = "ddtrace.contrib.internal.pytest.plugin" "ddtrace.pytest_benchmark" = "ddtrace.contrib.internal.pytest_benchmark.plugin" [project.entry-points.'ddtrace.products'] +"apm-tracing-rc" = "ddtrace.internal.remoteconfig.products.apm_tracing" "code-origin-for-spans" = "ddtrace.debugging._products.code_origin.span" "dynamic-instrumentation" = "ddtrace.debugging._products.dynamic_instrumentation" "exception-replay" = "ddtrace.debugging._products.exception_replay" "live-debugger" = "ddtrace.debugging._products.live_debugger" -"remote-configuration" = "ddtrace.internal.remoteconfig.product" +"remote-configuration" = "ddtrace.internal.remoteconfig.products.client" "symbol-database" = "ddtrace.internal.symbol_db.product" "appsec" = "ddtrace.internal.appsec.product" "iast" = "ddtrace.internal.iast.product" diff --git a/tests/debugging/mocking.py b/tests/debugging/mocking.py index 6c293988efc..0aa37410262 100644 --- a/tests/debugging/mocking.py +++ b/tests/debugging/mocking.py @@ -203,7 +203,11 @@ def _debugger(config_to_override: En, config_overrides: Any) -> Generator[TestDe def debugger(**config_overrides: Any) -> Generator[TestDebugger, None, None]: """Test with the debugger enabled.""" with _debugger(di_config, config_overrides) as debugger: - yield debugger + debugger.__watchdog__.install() + try: + yield debugger + finally: + debugger.__watchdog__.uninstall() class MockSpanExceptionHandler(SpanExceptionHandler): From ac960abd48e682afe839a7c60d73d4133be8fc3a Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Thu, 27 Feb 2025 11:25:02 +0000 Subject: [PATCH 02/12] remove commented out code --- .../_products/dynamic_instrumentation.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/ddtrace/debugging/_products/dynamic_instrumentation.py b/ddtrace/debugging/_products/dynamic_instrumentation.py index 46a36cc0473..c09e6d72b84 100644 --- a/ddtrace/debugging/_products/dynamic_instrumentation.py +++ b/ddtrace/debugging/_products/dynamic_instrumentation.py @@ -47,15 +47,6 @@ class APMCapabilities(enum.IntFlag): def apm_tracing_rc(lib_config): - enabled = lib_config.get("dynamic_instrumentation_enabled") - try: - if enabled is not None: # and config.spec.enabled.full_name not in config.source: - if (config.spec.enabled.full_name not in config.source or config.enabled) and enabled: - _start() - else: - stop() - except Exception: - from traceback import print_exc - - print_exc() - raise + 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() From f27447dd5c13167878f0507e4724170038a4ce77 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Fri, 28 Feb 2025 11:32:16 +0000 Subject: [PATCH 03/12] eager-loading DI --- ddtrace/debugging/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ddtrace/debugging/__init__.py b/ddtrace/debugging/__init__.py index 18868b37d33..ef905142949 100644 --- a/ddtrace/debugging/__init__.py +++ b/ddtrace/debugging/__init__.py @@ -38,9 +38,4 @@ Dynamic Instrumentation. """ -from ddtrace.internal.module import lazy - - -@lazy -def _(): - from ddtrace.debugging._debugger import Debugger as DynamicInstrumentation # noqa +from ddtrace.debugging._debugger import Debugger as DynamicInstrumentation # noqa From c6ce9edc7ab7f53e9250a73f1aba8af551e5de81 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Fri, 28 Feb 2025 11:46:45 +0000 Subject: [PATCH 04/12] minimal imports --- ddtrace/debugging/__init__.py | 7 +- ddtrace/debugging/_debugger.py | 68 +---------------- ddtrace/debugging/_import.py | 73 +++++++++++++++++++ .../_products/dynamic_instrumentation.py | 10 ++- tests/debugging/exploration/debugger.py | 2 +- 5 files changed, 89 insertions(+), 71 deletions(-) create mode 100644 ddtrace/debugging/_import.py diff --git a/ddtrace/debugging/__init__.py b/ddtrace/debugging/__init__.py index ef905142949..18868b37d33 100644 --- a/ddtrace/debugging/__init__.py +++ b/ddtrace/debugging/__init__.py @@ -38,4 +38,9 @@ Dynamic Instrumentation. """ -from ddtrace.debugging._debugger import Debugger as DynamicInstrumentation # noqa +from ddtrace.internal.module import lazy + + +@lazy +def _(): + from ddtrace.debugging._debugger import Debugger as DynamicInstrumentation # noqa diff --git a/ddtrace/debugging/_debugger.py b/ddtrace/debugging/_debugger.py index ef863790a84..246395eb4c7 100644 --- a/ddtrace/debugging/_debugger.py +++ b/ddtrace/debugging/_debugger.py @@ -7,7 +7,6 @@ import sys import threading import time -from types import CodeType from types import FunctionType from types import ModuleType from types import TracebackType @@ -27,6 +26,7 @@ from ddtrace.debugging._function.discovery import FunctionDiscovery from ddtrace.debugging._function.store import FullyNamedContextWrappedFunction from ddtrace.debugging._function.store import FunctionStore +from ddtrace.debugging._import import DebuggerModuleWatchdog from ddtrace.debugging._metrics import metrics from ddtrace.debugging._probe.model import FunctionLocationMixin from ddtrace.debugging._probe.model import FunctionProbe @@ -45,8 +45,6 @@ from ddtrace.debugging._uploader import UploaderProduct from ddtrace.internal.logger import get_logger from ddtrace.internal.metrics import Metrics -from ddtrace.internal.module import ModuleHookType -from ddtrace.internal.module import ModuleWatchdog from ddtrace.internal.module import origin from ddtrace.internal.module import register_post_run_module_hook from ddtrace.internal.module import unregister_post_run_module_hook @@ -71,70 +69,6 @@ class DebuggerError(Exception): pass -class DebuggerModuleWatchdog(ModuleWatchdog): - _locations: Set[str] = set() - - def transform(self, code: CodeType, module: ModuleType) -> CodeType: - return FunctionDiscovery.transformer(code, module) - - @classmethod - def register_origin_hook(cls, origin: Path, hook: ModuleHookType) -> None: - if origin in cls._locations: - # We already have a hook for this origin, don't register a new one - # but invoke it directly instead, if the module was already loaded. - module = cls.get_by_origin(origin) - if module is not None: - hook(module) - - return - - cls._locations.add(str(origin)) - - super().register_origin_hook(origin, hook) - - @classmethod - def unregister_origin_hook(cls, origin: Path, hook: ModuleHookType) -> None: - try: - cls._locations.remove(str(origin)) - except KeyError: - # Nothing to unregister. - return - - return super().unregister_origin_hook(origin, hook) - - @classmethod - def register_module_hook(cls, module: str, hook: ModuleHookType) -> None: - if module in cls._locations: - # We already have a hook for this origin, don't register a new one - # but invoke it directly instead, if the module was already loaded. - mod = sys.modules.get(module) - if mod is not None: - hook(mod) - - return - - cls._locations.add(module) - - super().register_module_hook(module, hook) - - @classmethod - def unregister_module_hook(cls, module: str, hook: ModuleHookType) -> None: - try: - cls._locations.remove(module) - except KeyError: - # Nothing to unregister. - return - - return super().unregister_module_hook(module, hook) - - @classmethod - def on_run_module(cls, module: ModuleType) -> None: - if cls._instance is not None: - # Treat run module as an import to trigger import hooks and register - # the module's origin. - cls._instance.after_import(module) - - class DebuggerWrappingContext(WrappingContext): __priority__ = 99 # Execute after all other contexts diff --git a/ddtrace/debugging/_import.py b/ddtrace/debugging/_import.py new file mode 100644 index 00000000000..107be9d3706 --- /dev/null +++ b/ddtrace/debugging/_import.py @@ -0,0 +1,73 @@ +from pathlib import Path +import sys +from types import CodeType +from types import ModuleType +from typing import Set + +from ddtrace.debugging._function.discovery import FunctionDiscovery +from ddtrace.internal.module import ModuleHookType +from ddtrace.internal.module import ModuleWatchdog + + +class DebuggerModuleWatchdog(ModuleWatchdog): + _locations: Set[str] = set() + + def transform(self, code: CodeType, module: ModuleType) -> CodeType: + return FunctionDiscovery.transformer(code, module) + + @classmethod + def register_origin_hook(cls, origin: Path, hook: ModuleHookType) -> None: + if origin in cls._locations: + # We already have a hook for this origin, don't register a new one + # but invoke it directly instead, if the module was already loaded. + module = cls.get_by_origin(origin) + if module is not None: + hook(module) + + return + + cls._locations.add(str(origin)) + + super().register_origin_hook(origin, hook) + + @classmethod + def unregister_origin_hook(cls, origin: Path, hook: ModuleHookType) -> None: + try: + cls._locations.remove(str(origin)) + except KeyError: + # Nothing to unregister. + return + + return super().unregister_origin_hook(origin, hook) + + @classmethod + def register_module_hook(cls, module: str, hook: ModuleHookType) -> None: + if module in cls._locations: + # We already have a hook for this origin, don't register a new one + # but invoke it directly instead, if the module was already loaded. + mod = sys.modules.get(module) + if mod is not None: + hook(mod) + + return + + cls._locations.add(module) + + super().register_module_hook(module, hook) + + @classmethod + def unregister_module_hook(cls, module: str, hook: ModuleHookType) -> None: + try: + cls._locations.remove(module) + except KeyError: + # Nothing to unregister. + return + + return super().unregister_module_hook(module, hook) + + @classmethod + def on_run_module(cls, module: ModuleType) -> None: + if cls._instance is not None: + # Treat run module as an import to trigger import hooks and register + # the module's origin. + cls._instance.after_import(module) diff --git a/ddtrace/debugging/_products/dynamic_instrumentation.py b/ddtrace/debugging/_products/dynamic_instrumentation.py index c09e6d72b84..f7089c8748f 100644 --- a/ddtrace/debugging/_products/dynamic_instrumentation.py +++ b/ddtrace/debugging/_products/dynamic_instrumentation.py @@ -1,5 +1,11 @@ import enum +# We need to make sure that remote configuration adapters are loaded before the +# main application starts. This is to make sure that we make all the necessary +# interactions with the multiprocessing module. If the main application uses +# gevent, the module reloading mechanism might cause the multiprocessing module +# to misbehave with errors like "TypeError: this type has no size". +import ddtrace.debugging._probe.remoteconfig # noqa from ddtrace.settings.dynamic_instrumentation import config @@ -7,12 +13,12 @@ def post_preload(): - from ddtrace.debugging._debugger import Debugger + 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. - Debugger.__watchdog__.install() + DebuggerModuleWatchdog.install() def _start(): diff --git a/tests/debugging/exploration/debugger.py b/tests/debugging/exploration/debugger.py index 87224a07746..1c96cf1703f 100644 --- a/tests/debugging/exploration/debugger.py +++ b/tests/debugging/exploration/debugger.py @@ -13,9 +13,9 @@ from ddtrace.debugging._config import di_config import ddtrace.debugging._debugger as _debugger from ddtrace.debugging._debugger import Debugger -from ddtrace.debugging._debugger import DebuggerModuleWatchdog from ddtrace.debugging._encoding import LogSignalJsonEncoder from ddtrace.debugging._function.discovery import FunctionDiscovery +from ddtrace.debugging._import import DebuggerModuleWatchdog from ddtrace.debugging._probe.model import Probe from ddtrace.debugging._probe.remoteconfig import ProbePollerEvent from ddtrace.debugging._signal.collector import SignalCollector From 89b3d7e30267e92fa68a88460342c89de213e7de Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Wed, 5 Mar 2025 17:26:57 +0000 Subject: [PATCH 05/12] migrate tracing to product interface --- ddtrace/_trace/product.py | 112 ++++++++++++++++++ ddtrace/_trace/tracer.py | 5 - ddtrace/bootstrap/preload.py | 29 +---- ddtrace/settings/_config.py | 85 +------------ pyproject.toml | 1 + tests/contrib/aredis/test_aredis.py | 3 +- tests/contrib/graphene/test_graphene.py | 2 + tests/contrib/mariadb/test_mariadb.py | 2 + .../contrib/psycopg/test_psycopg_snapshot.py | 2 + .../contrib/psycopg2/test_psycopg_snapshot.py | 2 + tests/contrib/rediscluster/test.py | 4 +- tests/contrib/yaaredis/test_yaaredis.py | 2 + tests/integration/test_settings.py | 15 ++- tests/internal/test_settings.py | 50 ++++---- tests/opentelemetry/test_context.py | 2 + tests/tracer/test_memory_leak.py | 5 +- tests/tracer/test_tracer.py | 16 ++- 17 files changed, 187 insertions(+), 150 deletions(-) create mode 100644 ddtrace/_trace/product.py diff --git a/ddtrace/_trace/product.py b/ddtrace/_trace/product.py new file mode 100644 index 00000000000..5446d5f9348 --- /dev/null +++ b/ddtrace/_trace/product.py @@ -0,0 +1,112 @@ +import enum +import os +import typing as t + +from envier import En + +from ddtrace.internal.utils.formats import asbool # noqa:F401 +from ddtrace.internal.utils.formats import parse_tags_str # noqa:F401 + + +requires = ["remote-configuration"] + + +class Config(En): + __prefix__ = "dd.trace" + + enabled = En.v(bool, "enabled", default=True) + + +_config = Config() + + +def post_preload(): + if _config.enabled: + from ddtrace._monkey import patch_all + + modules_to_patch = os.getenv("DD_PATCH_MODULES") + modules_to_str = parse_tags_str(modules_to_patch) + modules_to_bool = {k: asbool(v) for k, v in modules_to_str.items()} + patch_all(**modules_to_bool) + + +def start(): + if _config.enabled: + from ddtrace import config + + if config._trace_methods: + from ddtrace.internal.tracemethods import _install_trace_methods # noqa:F401 + + _install_trace_methods(config._trace_methods) + + if "DD_TRACE_GLOBAL_TAGS" in os.environ: + from ddtrace.trace import tracer + + env_tags = os.getenv("DD_TRACE_GLOBAL_TAGS") + tracer.set_tags(parse_tags_str(env_tags)) + + +def restart(join=False): + from ddtrace.trace import tracer + + if tracer.enabled: + tracer._child_after_fork() + + +def stop(join=False): + from ddtrace.trace import tracer + + if tracer.enabled: + tracer.shutdown() + + +def at_exit(join=False): + from ddtrace.trace import tracer + + if tracer.enabled: + tracer._atexit() + + +class APMCapabilities(enum.IntFlag): + APM_TRACING_SAMPLE_RATE = 1 << 12 + APM_TRACING_LOGS_INJECTION = 1 << 13 + APM_TRACING_HTTP_HEADER_TAGS = 1 << 14 + APM_TRACING_CUSTOM_TAGS = 1 << 15 + APM_TRACING_ENABLED = 1 << 19 + APM_TRACING_SAMPLE_RULES = 1 << 29 + + +def apm_tracing_rc(lib_config): + from ddtrace import config + + base_rc_config: t.Dict[str, t.Any] = {n: None for n in config._config} + + if "tracing_sampling_rules" in lib_config or "tracing_sampling_rate" in lib_config: + global_sampling_rate = lib_config.get("tracing_sampling_rate") + trace_sampling_rules = lib_config.get("tracing_sampling_rules") or [] + # returns None if no rules + trace_sampling_rules = config._convert_rc_trace_sampling_rules(trace_sampling_rules, global_sampling_rate) + if trace_sampling_rules: + base_rc_config["_trace_sampling_rules"] = trace_sampling_rules + + if "log_injection_enabled" in lib_config: + base_rc_config["_logs_injection"] = lib_config["log_injection_enabled"] + + if "tracing_tags" in lib_config: + tags = lib_config["tracing_tags"] + if tags: + tags = config._format_tags(lib_config["tracing_tags"]) + base_rc_config["tags"] = tags + + if "tracing_enabled" in lib_config and lib_config["tracing_enabled"] is not None: + base_rc_config["_tracing_enabled"] = asbool(lib_config["tracing_enabled"]) + + if "tracing_header_tags" in lib_config: + tags = lib_config["tracing_header_tags"] + if tags: + tags = config._format_tags(lib_config["tracing_header_tags"]) + base_rc_config["_trace_http_header_tags"] = tags + + config._set_config_items([(k, v, "remote_config") for k, v in base_rc_config.items()]) + # called unconditionally to handle the case where header tags have been unset + config._handle_remoteconfig_header_tags(base_rc_config) diff --git a/ddtrace/_trace/tracer.py b/ddtrace/_trace/tracer.py index 999ad3a40d2..04a95d343c2 100644 --- a/ddtrace/_trace/tracer.py +++ b/ddtrace/_trace/tracer.py @@ -33,7 +33,6 @@ from ddtrace.constants import PID from ddtrace.constants import VERSION_KEY from ddtrace.internal import agent -from ddtrace.internal import atexit from ddtrace.internal import compat from ddtrace.internal import debug from ddtrace.internal import forksafe @@ -283,9 +282,7 @@ def __init__( register_on_exit_signal(self._atexit) self._hooks = _hooks.Hooks() - atexit.register(self._atexit) forksafe.register_before_fork(self._sample_before_fork) - forksafe.register(self._child_after_fork) self._shutdown_lock = RLock() @@ -1071,8 +1068,6 @@ def shutdown(self, timeout: Optional[float] = None) -> None: if hasattr(processor, "shutdown"): processor.shutdown(timeout) - atexit.unregister(self._atexit) - forksafe.unregister(self._child_after_fork) forksafe.unregister_before_fork(self._sample_before_fork) self.start_span = self._start_span_after_shutdown # type: ignore[assignment] diff --git a/ddtrace/bootstrap/preload.py b/ddtrace/bootstrap/preload.py index 7980b9bf746..da5992ceaf0 100644 --- a/ddtrace/bootstrap/preload.py +++ b/ddtrace/bootstrap/preload.py @@ -3,23 +3,18 @@ Add all monkey-patching that needs to run by default here """ -import os # noqa:I001 +import typing as t from ddtrace import config # noqa:F401 -from ddtrace.settings.profiling import config as profiling_config # noqa:F401 from ddtrace.internal.logger import get_logger # noqa:F401 from ddtrace.internal.module import ModuleWatchdog # noqa:F401 from ddtrace.internal.products import manager # noqa:F401 from ddtrace.internal.runtime.runtime_metrics import RuntimeWorker # noqa:F401 -from ddtrace.internal.tracemethods import _install_trace_methods # noqa:F401 -from ddtrace.internal.utils.formats import asbool # noqa:F401 -from ddtrace.internal.utils.formats import parse_tags_str # noqa:F401 from ddtrace.settings.crashtracker import config as crashtracker_config +from ddtrace.settings.profiling import config as profiling_config # noqa:F401 from ddtrace.trace import tracer -import typing as t - # Register operations to be performned after the preload is complete. In # general, we might need to perform some cleanup operations after the # initialisation of the library, while also execute some more code after that. @@ -86,26 +81,6 @@ def _(_): LLMObs.enable() -if asbool(os.getenv("DD_TRACE_ENABLED", default=True)): - from ddtrace import patch_all - - @register_post_preload - def _(): - # We need to clean up after we have imported everything we need from - # ddtrace, but before we register the patch-on-import hooks for the - # integrations. - modules_to_patch = os.getenv("DD_PATCH_MODULES") - modules_to_str = parse_tags_str(modules_to_patch) - modules_to_bool = {k: asbool(v) for k, v in modules_to_str.items()} - patch_all(**modules_to_bool) - - if config._trace_methods: - _install_trace_methods(config._trace_methods) - -if "DD_TRACE_GLOBAL_TAGS" in os.environ: - env_tags = os.getenv("DD_TRACE_GLOBAL_TAGS") - tracer.set_tags(parse_tags_str(env_tags)) - @register_post_preload def _(): diff --git a/ddtrace/settings/_config.py b/ddtrace/settings/_config.py index d095e818015..936df1d5b90 100644 --- a/ddtrace/settings/_config.py +++ b/ddtrace/settings/_config.py @@ -1,5 +1,4 @@ from copy import deepcopy -import enum import json import os import re @@ -397,15 +396,6 @@ def _default_config() -> Dict[str, _ConfigItem]: } -class Capabilities(enum.IntFlag): - APM_TRACING_SAMPLE_RATE = 1 << 12 - APM_TRACING_LOGS_INJECTION = 1 << 13 - APM_TRACING_HTTP_HEADER_TAGS = 1 << 14 - APM_TRACING_CUSTOM_TAGS = 1 << 15 - APM_TRACING_ENABLED = 1 << 19 - APM_TRACING_SAMPLE_RULES = 1 << 29 - - class Config(object): """Configuration object that exposes an API to set and retrieve global settings for each integration. All integrations must use @@ -601,10 +591,7 @@ def __init__(self): x_datadog_tags_max_length = _get_config("DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH", 512, int) if x_datadog_tags_max_length < 0: log.warning( - ( - "Invalid value %r provided for DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, " - "only non-negative values allowed" - ), + ("Invalid value %r provided for DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH, only non-negative values allowed"), x_datadog_tags_max_length, ) x_datadog_tags_max_length = 0 @@ -825,74 +812,6 @@ def _get_source(self, item): # type: (str) -> str return self._config[item].source() - def _remoteconfigPubSub(self): - from ddtrace.internal.remoteconfig._connectors import PublisherSubscriberConnector - from ddtrace.internal.remoteconfig._publishers import RemoteConfigPublisher - from ddtrace.internal.remoteconfig._pubsub import PubSub - from ddtrace.internal.remoteconfig._pubsub import RemoteConfigSubscriber - - class _GlobalConfigPubSub(PubSub): - __publisher_class__ = RemoteConfigPublisher - __subscriber_class__ = RemoteConfigSubscriber - __shared_data__ = PublisherSubscriberConnector() - - def __init__(self, callback): - self._publisher = self.__publisher_class__(self.__shared_data__, None) - self._subscriber = self.__subscriber_class__(self.__shared_data__, callback, "GlobalConfig") - - return _GlobalConfigPubSub - - def _handle_remoteconfig(self, data, test_tracer=None): - # type: (Any, Any) -> None - if not isinstance(data, dict) or (isinstance(data, dict) and "config" not in data): - log.warning("unexpected RC payload %r", data) - return - if len(data["config"]) == 0: - log.warning("unexpected number of RC payloads %r", data) - return - - # Check if 'lib_config' is a key in the dictionary since other items can be sent in the payload - config = None - for config_item in data["config"]: - if isinstance(config_item, Dict): - if "lib_config" in config_item: - config = config_item - break - - # If no data is submitted then the RC config has been deleted. Revert the settings. - base_rc_config = {n: None for n in self._config} - - if config and "lib_config" in config: - lib_config = config["lib_config"] - if "tracing_sampling_rules" in lib_config or "tracing_sampling_rate" in lib_config: - global_sampling_rate = lib_config.get("tracing_sampling_rate") - trace_sampling_rules = lib_config.get("tracing_sampling_rules") or [] - # returns None if no rules - trace_sampling_rules = self._convert_rc_trace_sampling_rules(trace_sampling_rules, global_sampling_rate) - if trace_sampling_rules: - base_rc_config["_trace_sampling_rules"] = trace_sampling_rules # type: ignore[assignment] - - if "log_injection_enabled" in lib_config: - base_rc_config["_logs_injection"] = lib_config["log_injection_enabled"] - - if "tracing_tags" in lib_config: - tags = lib_config["tracing_tags"] - if tags: - tags = self._format_tags(lib_config["tracing_tags"]) - base_rc_config["tags"] = tags - - if "tracing_enabled" in lib_config and lib_config["tracing_enabled"] is not None: - base_rc_config["_tracing_enabled"] = asbool(lib_config["tracing_enabled"]) # type: ignore[assignment] - - if "tracing_header_tags" in lib_config: - tags = lib_config["tracing_header_tags"] - if tags: - tags = self._format_tags(lib_config["tracing_header_tags"]) - base_rc_config["_trace_http_header_tags"] = tags - self._set_config_items([(k, v, "remote_config") for k, v in base_rc_config.items()]) - # called unconditionally to handle the case where header tags have been unset - self._handle_remoteconfig_header_tags(base_rc_config) - def _handle_remoteconfig_header_tags(self, base_rc_config): """Implements precedence order between remoteconfig header tags from code, env, and RC""" header_tags_conf = self._config["_trace_http_header_tags"] @@ -919,10 +838,8 @@ def _enable_remote_configuration(self): from ddtrace.internal.flare.handler import _tracerFlarePubSub from ddtrace.internal.remoteconfig.worker import remoteconfig_poller - remoteconfig_pubsub = self._remoteconfigPubSub()(self._handle_remoteconfig) flare = Flare(trace_agent_url=self._trace_agent_url, api_key=self._dd_api_key, ddconfig=self.__dict__) tracerflare_pubsub = _tracerFlarePubSub()(_handle_tracer_flare, flare) - remoteconfig_poller.register("APM_TRACING", remoteconfig_pubsub, capabilities=Capabilities) remoteconfig_poller.register("AGENT_CONFIG", tracerflare_pubsub) remoteconfig_poller.register("AGENT_TASK", tracerflare_pubsub) diff --git a/pyproject.toml b/pyproject.toml index b1c553c09f7..d9d5f2f36d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ ddtrace = "ddtrace.contrib.internal.pytest.plugin" "symbol-database" = "ddtrace.internal.symbol_db.product" "appsec" = "ddtrace.internal.appsec.product" "iast" = "ddtrace.internal.iast.product" +"tracer" = "ddtrace._trace.product" [project.urls] "Bug Tracker" = "https://github.com/DataDog/dd-trace-py/issues" diff --git a/tests/contrib/aredis/test_aredis.py b/tests/contrib/aredis/test_aredis.py index 298abdbf85b..6efc6c733e8 100644 --- a/tests/contrib/aredis/test_aredis.py +++ b/tests/contrib/aredis/test_aredis.py @@ -201,11 +201,12 @@ async def test_opentracing(tracer, snapshot_context): @pytest.mark.subprocess(env=dict(DD_REDIS_RESOURCE_ONLY_COMMAND="false")) @pytest.mark.snapshot def test_full_command_in_resource_env(): + import ddtrace.auto # noqa + import asyncio import aredis - import ddtrace from tests.contrib.config import REDIS_CONFIG async def traced_client(): diff --git a/tests/contrib/graphene/test_graphene.py b/tests/contrib/graphene/test_graphene.py index c1aa07d5904..e0164d8344b 100644 --- a/tests/contrib/graphene/test_graphene.py +++ b/tests/contrib/graphene/test_graphene.py @@ -115,6 +115,8 @@ async def test_schema_execute_async_with_resolvers(test_schema, test_source_str, @pytest.mark.subprocess(env=dict(DD_TRACE_GRAPHQL_ERROR_EXTENSIONS="code, status")) @pytest.mark.snapshot(ignores=["meta.events", "meta.error.stack"]) def test_schema_failing_extensions(test_schema, test_source_str, enable_graphql_resolvers): + import ddtrace.auto # noqa + import graphene from ddtrace.contrib.internal.graphql.patch import patch diff --git a/tests/contrib/mariadb/test_mariadb.py b/tests/contrib/mariadb/test_mariadb.py index 2f51f2e9b0a..cc2c89156c4 100644 --- a/tests/contrib/mariadb/test_mariadb.py +++ b/tests/contrib/mariadb/test_mariadb.py @@ -186,6 +186,8 @@ def test_user_specified_dd_mariadb_service_snapshot(): When a user specifies a service for the app The mariadb integration should not use it. """ + import ddtrace.auto # noqa + import mariadb from ddtrace import patch diff --git a/tests/contrib/psycopg/test_psycopg_snapshot.py b/tests/contrib/psycopg/test_psycopg_snapshot.py index c4f904bfaa4..3c9cedbc7e2 100644 --- a/tests/contrib/psycopg/test_psycopg_snapshot.py +++ b/tests/contrib/psycopg/test_psycopg_snapshot.py @@ -67,6 +67,8 @@ def test_connect_traced_via_env(run_python_code_in_subprocess): """When explicitly enabled, we trace psycopg.connect method""" code = """ +import ddtrace.auto + import psycopg import ddtrace diff --git a/tests/contrib/psycopg2/test_psycopg_snapshot.py b/tests/contrib/psycopg2/test_psycopg_snapshot.py index 6947faf15b8..376c71476f3 100644 --- a/tests/contrib/psycopg2/test_psycopg_snapshot.py +++ b/tests/contrib/psycopg2/test_psycopg_snapshot.py @@ -49,6 +49,8 @@ def test_connect_traced_via_env(run_python_code_in_subprocess): """When explicitly enabled, we trace psycopg2.connect method""" code = """ +import ddtrace.auto + import psycopg2 import ddtrace diff --git a/tests/contrib/rediscluster/test.py b/tests/contrib/rediscluster/test.py index 79b4c806440..de21e7508d4 100644 --- a/tests/contrib/rediscluster/test.py +++ b/tests/contrib/rediscluster/test.py @@ -260,9 +260,11 @@ def test_cmd_max_length_env(): r.get("here-is-a-long-key") -@pytest.mark.subprocess(env=dict(DD_REDIS_RESOURCE_ONLY_COMMAND="false")) +@pytest.mark.subprocess(env=dict(DD_REDIS_RESOURCE_ONLY_COMMAND="false", DD_TRACE_REDIS_ENABLED="0")) @pytest.mark.snapshot def test_full_command_in_resource_env(): + import ddtrace.auto # noqa + import ddtrace from tests.contrib.rediscluster.test import _get_test_client diff --git a/tests/contrib/yaaredis/test_yaaredis.py b/tests/contrib/yaaredis/test_yaaredis.py index df064817aef..bfa55cd5fa1 100644 --- a/tests/contrib/yaaredis/test_yaaredis.py +++ b/tests/contrib/yaaredis/test_yaaredis.py @@ -204,6 +204,8 @@ async def test_basics(traced_yaaredis): @pytest.mark.subprocess(env=dict(DD_REDIS_RESOURCE_ONLY_COMMAND="false")) @pytest.mark.snapshot def test_full_command_in_resource_env(): + import ddtrace.auto # noqa + import asyncio import yaaredis diff --git a/tests/integration/test_settings.py b/tests/integration/test_settings.py index 79a6ce97a6e..47742aa906c 100644 --- a/tests/integration/test_settings.py +++ b/tests/integration/test_settings.py @@ -136,27 +136,28 @@ def test_remoteconfig_sampling_rate_default(test_agent_session, run_python_code_ """ from ddtrace import config, tracer from tests.internal.test_settings import _base_rc_config +from ddtrace._trace.product import apm_tracing_rc with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") is None -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.5})) +apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.5})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.5 -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": None})) +apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": None})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") is None, "Unsetting remote config trace sample rate" -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.8})) +apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.8})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.8 -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": None})) +apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": None})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") is None, "(second time) unsetting remote config trace sample rate" @@ -182,8 +183,9 @@ def test_remoteconfig_sampling_rate_telemetry(test_agent_session, run_python_cod """ from ddtrace import config, tracer from tests.internal.test_settings import _base_rc_config +from ddtrace._trace.product import apm_tracing_rc -config._handle_remoteconfig( +apm_tracing_rc( _base_rc_config( { "tracing_sampling_rules": [ @@ -230,8 +232,9 @@ def test_remoteconfig_header_tags_telemetry(test_agent_session, run_python_code_ from ddtrace import config, tracer from ddtrace.contrib import trace_utils from tests.internal.test_settings import _base_rc_config +from ddtrace._trace.product import apm_tracing_rc -config._handle_remoteconfig(_base_rc_config({ +apm_tracing_rc(_base_rc_config({ "tracing_header_tags": [ {"header": "used", "tag_name":"header_tag_69"}, {"header": "unused", "tag_name":"header_tag_70"}, diff --git a/tests/internal/test_settings.py b/tests/internal/test_settings.py index d62e39160fd..8a549cfc5ed 100644 --- a/tests/internal/test_settings.py +++ b/tests/internal/test_settings.py @@ -4,6 +4,7 @@ import mock import pytest +from ddtrace._trace.product import apm_tracing_rc from ddtrace.settings import Config @@ -194,7 +195,7 @@ def test_settings_parametrized(testcase, config, monkeypatch): rc_items = testcase.get("rc", {}) if rc_items: - config._handle_remoteconfig(_base_rc_config(rc_items), None) + apm_tracing_rc(_base_rc_config(rc_items)) for expected_name, expected_value in testcase["expected"].items(): assert getattr(config, expected_name) == expected_value @@ -224,7 +225,7 @@ def test_settings_missing_lib_config(config, monkeypatch): del base_rc_config["config"][1]["lib_config"] assert "lib_config" not in base_rc_config["config"][0] - config._handle_remoteconfig(base_rc_config, None) + apm_tracing_rc(base_rc_config) for expected_name, expected_value in testcase["expected"].items(): assert getattr(config, expected_name) == expected_value @@ -250,13 +251,14 @@ def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): from ddtrace import config, tracer from ddtrace._trace.sampler import DatadogSampler from tests.internal.test_settings import _base_rc_config, _deleted_rc_config +from ddtrace._trace.product import apm_tracing_rc with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.1 assert span.get_tag("_dd.p.dm") == "-3" -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rules":[ +apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ { "service": "*", "name": "test", @@ -270,7 +272,7 @@ def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): assert span.get_metric("_dd.rule_psr") == 0.2 assert span.get_tag("_dd.p.dm") == "-11" -config._handle_remoteconfig(_base_rc_config({})) +apm_tracing_rc(_base_rc_config({})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.1 @@ -282,7 +284,7 @@ def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): assert span.get_metric("_dd.rule_psr") == 0.3 assert span.get_tag("_dd.p.dm") == "-3" -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rules":[ +apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ { "service": "*", "name": "test", @@ -296,13 +298,13 @@ def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): assert span.get_metric("_dd.rule_psr") == 0.4 assert span.get_tag("_dd.p.dm") == "-12" -config._handle_remoteconfig(_base_rc_config({})) +apm_tracing_rc(_base_rc_config({})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.3 assert span.get_tag("_dd.p.dm") == "-3" -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rules":[ +apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ { "service": "ok", "name": "test", @@ -316,7 +318,7 @@ def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): assert span.get_metric("_dd.rule_psr") == 0.4 assert span.get_tag("_dd.p.dm") == "-11" -config._handle_remoteconfig(_deleted_rc_config()) +apm_tracing_rc(_deleted_rc_config()) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.3 @@ -340,6 +342,8 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess from ddtrace import config, tracer from ddtrace._trace.sampler import DatadogSampler from tests.internal.test_settings import _base_rc_config, _deleted_rc_config +from ddtrace._trace.product import apm_tracing_rc + # Ensure that sampling rule with operation name "rules" is used with tracer.trace("rules") as span: pass @@ -351,7 +355,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_metric("_dd.rule_psr") == 0.8 assert span.get_tag("_dd.p.dm") == "-3" # Override all sampling rules set via env var with a new rule -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rules":[ +apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ { "service": "*", "name": "rules", @@ -372,7 +376,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_tag("_dd.p.dm") == "-0" # Set a new default sampling rate via rc -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.2})) +apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.2})) # Ensure that the new default sampling rate is used with tracer.trace("sample_rate") as span: pass @@ -385,7 +389,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_tag("_dd.p.dm") == "-3" # Set a new sampling rules via rc -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rate": 0.3})) +apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.3})) # Ensure that the new default sampling rate is used with tracer.trace("sample_rate") as span: pass @@ -398,7 +402,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_tag("_dd.p.dm") == "-3" # Remove all sampling rules from remote config -config._handle_remoteconfig(_base_rc_config({})) +apm_tracing_rc(_base_rc_config({})) # Ensure that the default sampling rate set via env vars is used with tracer.trace("rules") as span: pass @@ -411,7 +415,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_tag("_dd.p.dm") == "-3" # Test swtting dynamic and customer sampling rules -config._handle_remoteconfig(_base_rc_config({"tracing_sampling_rules":[ +apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ { "service": "*", "name": "rules_dynamic", @@ -455,18 +459,19 @@ def test_remoteconfig_custom_tags(run_python_code_in_subprocess): """ from ddtrace import config, tracer from tests.internal.test_settings import _base_rc_config +from ddtrace._trace.product import apm_tracing_rc with tracer.trace("test") as span: pass assert span.get_tag("team") == "apm" -config._handle_remoteconfig(_base_rc_config({"tracing_tags": ["team:onboarding"]})) +apm_tracing_rc(_base_rc_config({"tracing_tags": ["team:onboarding"]})) with tracer.trace("test") as span: pass assert span.get_tag("team") == "onboarding", span._meta -config._handle_remoteconfig(_base_rc_config({})) +apm_tracing_rc(_base_rc_config({})) with tracer.trace("test") as span: pass assert span.get_tag("team") == "apm" @@ -483,14 +488,15 @@ def test_remoteconfig_tracing_enabled(run_python_code_in_subprocess): """ from ddtrace import config, tracer from tests.internal.test_settings import _base_rc_config +from ddtrace._trace.product import apm_tracing_rc assert tracer.enabled is True -config._handle_remoteconfig(_base_rc_config({"tracing_enabled": "false"})) +apm_tracing_rc(_base_rc_config({"tracing_enabled": "false"})) assert tracer.enabled is False -config._handle_remoteconfig(_base_rc_config({"tracing_enabled": "true"})) +apm_tracing_rc(_base_rc_config({"tracing_enabled": "true"})) assert tracer.enabled is False """, @@ -506,17 +512,18 @@ def test_remoteconfig_logs_injection_jsonlogger(run_python_code_in_subprocess): from pythonjsonlogger import jsonlogger from ddtrace import config, tracer from tests.internal.test_settings import _base_rc_config +from ddtrace._trace.product import apm_tracing_rc log = logging.getLogger() log.level = logging.CRITICAL logHandler = logging.StreamHandler(); logHandler.setFormatter(jsonlogger.JsonFormatter()) log.addHandler(logHandler) # Enable logs injection -config._handle_remoteconfig(_base_rc_config({"log_injection_enabled": True})) +apm_tracing_rc(_base_rc_config({"log_injection_enabled": True})) with tracer.trace("test") as span: print(span.trace_id) log.critical("Hello, World!") # Disable logs injection -config._handle_remoteconfig(_base_rc_config({"log_injection_enabled": False})) +apm_tracing_rc(_base_rc_config({"log_injection_enabled": False})) with tracer.trace("test") as span: print(span.trace_id) log.critical("Hello, World!") @@ -538,6 +545,7 @@ def test_remoteconfig_header_tags(run_python_code_in_subprocess): from ddtrace import config, tracer from ddtrace.contrib import trace_utils from tests.internal.test_settings import _base_rc_config +from ddtrace._trace.product import apm_tracing_rc with tracer.trace("test") as span: trace_utils.set_http_meta(span, @@ -548,7 +556,7 @@ def test_remoteconfig_header_tags(run_python_code_in_subprocess): config._http._reset() config._header_tag_name.invalidate() -config._handle_remoteconfig(_base_rc_config({"tracing_header_tags": +apm_tracing_rc(_base_rc_config({"tracing_header_tags": [{"header": "X-Header-Tag-420", "tag_name":"header_tag_420"}]})) with tracer.trace("test_rc_override") as span2: @@ -560,7 +568,7 @@ def test_remoteconfig_header_tags(run_python_code_in_subprocess): config._http._reset() config._header_tag_name.invalidate() -config._handle_remoteconfig(_base_rc_config({})) +apm_tracing_rc(_base_rc_config({})) with tracer.trace("test") as span3: trace_utils.set_http_meta(span3, diff --git a/tests/opentelemetry/test_context.py b/tests/opentelemetry/test_context.py index dffa3f715cd..d59ee854329 100644 --- a/tests/opentelemetry/test_context.py +++ b/tests/opentelemetry/test_context.py @@ -74,6 +74,8 @@ def target(parent_context): def _subprocess_task(parent_span_context, errors): + import ddtrace.auto # noqa + from ddtrace.opentelemetry import TracerProvider # Tracer provider must be set in the subprocess otherwise the default tracer will be used diff --git a/tests/tracer/test_memory_leak.py b/tests/tracer/test_memory_leak.py index b13cc0ec4ee..3d3cd15fccb 100644 --- a/tests/tracer/test_memory_leak.py +++ b/tests/tracer/test_memory_leak.py @@ -1,6 +1,7 @@ """ Variety of test cases ensuring that ddtrace does not leak memory. """ + from weakref import WeakValueDictionary import pytest @@ -131,12 +132,14 @@ def _target(ctx): assert len(wd) == 0 -@pytest.mark.subprocess +@pytest.mark.subprocess(err=None) def test_fork_open_span(): """ When a fork occurs with an open span then the child process should not have a strong reference to the span because it might never be closed. """ + import ddtrace.auto # noqa + import gc import os from weakref import WeakValueDictionary diff --git a/tests/tracer/test_tracer.py b/tests/tracer/test_tracer.py index 48977bae0d5..096ab3f2c43 100644 --- a/tests/tracer/test_tracer.py +++ b/tests/tracer/test_tracer.py @@ -1029,6 +1029,8 @@ def test_enable(): ) def test_unfinished_span_warning_log(): """Test that a warning log is emitted when the tracer is shut down with unfinished spans.""" + import ddtrace.auto # noqa + from ddtrace.constants import MANUAL_KEEP_KEY from ddtrace.trace import tracer @@ -1689,8 +1691,10 @@ def _target(span): assert len(spans) == 2 -@pytest.mark.subprocess +@pytest.mark.subprocess(err=None) def test_fork_manual_span_same_context(): + import ddtrace.auto # noqa + import os from ddtrace.trace import tracer @@ -1713,8 +1717,10 @@ def test_fork_manual_span_same_context(): assert exit_code == 12 -@pytest.mark.subprocess() +@pytest.mark.subprocess(err=None) def test_fork_manual_span_different_contexts(): + import ddtrace.auto # noqa + import os from ddtrace.trace import tracer @@ -1736,8 +1742,10 @@ def test_fork_manual_span_different_contexts(): assert exit_code == 12 -@pytest.mark.subprocess +@pytest.mark.subprocess(err=None) def test_fork_pid(): + import ddtrace.auto # noqa + import os from ddtrace.constants import PID @@ -1950,5 +1958,5 @@ def test_multiple_tracer_instances(): with mock.patch("ddtrace._trace.tracer.log") as log: ddtrace.trace.Tracer() log.error.assert_called_once_with( - "Multiple Tracer instances can not be initialized. " "Use ``ddtrace.trace.tracer`` instead." + "Multiple Tracer instances can not be initialized. Use ``ddtrace.trace.tracer`` instead." ) From 2677d3f27e56cdd44c58f87482f75438493de571 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Tue, 1 Apr 2025 09:48:50 +0100 Subject: [PATCH 06/12] remove double-registration --- ddtrace/_trace/product.py | 11 ++++++-- ddtrace/internal/README.md | 4 ++- .../internal/remoteconfig/products/client.py | 27 ------------------- ddtrace/settings/_config.py | 9 ------- 4 files changed, 12 insertions(+), 39 deletions(-) diff --git a/ddtrace/_trace/product.py b/ddtrace/_trace/product.py index 92c9bc9714f..4874a834377 100644 --- a/ddtrace/_trace/product.py +++ b/ddtrace/_trace/product.py @@ -6,6 +6,7 @@ from ddtrace.internal.utils.formats import asbool from ddtrace.internal.utils.formats import parse_tags_str +from ddtrace.settings.http import HttpConfig requires = ["remote-configuration"] @@ -108,5 +109,11 @@ def apm_tracing_rc(lib_config): base_rc_config["_trace_http_header_tags"] = tags config._set_config_items([(k, v, "remote_config") for k, v in base_rc_config.items()]) - # called unconditionally to handle the case where header tags have been unset - config._handle_remoteconfig_header_tags(base_rc_config) + + # unconditionally handle the case where header tags have been unset + header_tags_conf = config._config["_trace_http_header_tags"] + env_headers = header_tags_conf._env_value or {} + code_headers = header_tags_conf._code_value or {} + non_rc_header_tags = {**code_headers, **env_headers} + selected_header_tags = base_rc_config.get("_trace_http_header_tags") or non_rc_header_tags + config._http = HttpConfig(header_tags=selected_header_tags) diff --git a/ddtrace/internal/README.md b/ddtrace/internal/README.md index f406c0d2fb1..536e01b48a7 100644 --- a/ddtrace/internal/README.md +++ b/ddtrace/internal/README.md @@ -38,4 +38,6 @@ gets extended to add support for additional features. | Attribute | Description | |-----------|-------------| | `requires: list[str]` | A list of other product names that the product depends on | -| `config: DDConfig` | A configuration object; when an instance of `DDConfig`, configuration telemetry is automatically reported | +| `config: DDConfig` | A configuration object; when an instance of `DDConfig`, configuration telemetry is automatically reported | +| `APMCapabilities: Type[enum.IntFlag]` | A set of capabilities that the product provides | +| `apm_tracing_rc: (dict) -> None` | Product-specific remote configuration handler (e.g. remote enablement) | diff --git a/ddtrace/internal/remoteconfig/products/client.py b/ddtrace/internal/remoteconfig/products/client.py index 4b7fbd3241b..9ccda2d496c 100644 --- a/ddtrace/internal/remoteconfig/products/client.py +++ b/ddtrace/internal/remoteconfig/products/client.py @@ -1,32 +1,7 @@ -import enum - from ddtrace import config -from ddtrace.internal.remoteconfig._connectors import PublisherSubscriberConnector -from ddtrace.internal.remoteconfig._publishers import RemoteConfigPublisher -from ddtrace.internal.remoteconfig._pubsub import PubSub -from ddtrace.internal.remoteconfig._pubsub import RemoteConfigSubscriber from ddtrace.internal.remoteconfig.client import config as rc_config -class GlobalConfigPubSub(PubSub): - __publisher_class__ = RemoteConfigPublisher - __subscriber_class__ = RemoteConfigSubscriber - __shared_data__ = PublisherSubscriberConnector() - - def __init__(self, callback): - self._publisher = self.__publisher_class__(self.__shared_data__, None) - self._subscriber = self.__subscriber_class__(self.__shared_data__, callback, "GlobalConfig") - - -class Capabilities(enum.IntFlag): - APM_TRACING_SAMPLE_RATE = 1 << 12 - APM_TRACING_LOGS_INJECTION = 1 << 13 - APM_TRACING_HTTP_HEADER_TAGS = 1 << 14 - APM_TRACING_CUSTOM_TAGS = 1 << 15 - APM_TRACING_ENABLED = 1 << 19 - APM_TRACING_SAMPLE_RULES = 1 << 29 - - # TODO: Modularize better into their own respective components def _register_rc_products() -> None: """Enable fetching configuration from Datadog.""" @@ -35,10 +10,8 @@ def _register_rc_products() -> None: from ddtrace.internal.flare.handler import _tracerFlarePubSub from ddtrace.internal.remoteconfig.worker import remoteconfig_poller - remoteconfig_pubsub = GlobalConfigPubSub(config._handle_remoteconfig) flare = Flare(trace_agent_url=config._trace_agent_url, api_key=config._dd_api_key, ddconfig=config.__dict__) tracerflare_pubsub = _tracerFlarePubSub()(_handle_tracer_flare, flare) - remoteconfig_poller.register("APM_TRACING", remoteconfig_pubsub, capabilities=Capabilities) remoteconfig_poller.register("AGENT_CONFIG", tracerflare_pubsub) remoteconfig_poller.register("AGENT_TASK", tracerflare_pubsub) diff --git a/ddtrace/settings/_config.py b/ddtrace/settings/_config.py index 243f8fb280d..7516283e742 100644 --- a/ddtrace/settings/_config.py +++ b/ddtrace/settings/_config.py @@ -803,15 +803,6 @@ def _get_source(self, item): # type: (str) -> str return self._config[item].source() - def _handle_remoteconfig_header_tags(self, base_rc_config): - """Implements precedence order between remoteconfig header tags from code, env, and RC""" - header_tags_conf = self._config["_trace_http_header_tags"] - env_headers = header_tags_conf._env_value or {} - code_headers = header_tags_conf._code_value or {} - non_rc_header_tags = {**code_headers, **env_headers} - selected_header_tags = base_rc_config.get("_trace_http_header_tags") or non_rc_header_tags - self._http = HttpConfig(header_tags=selected_header_tags) - def _format_tags(self, tags: List[Union[str, Dict]]) -> Dict[str, str]: if not tags: return {} From e2fcdec9f2525512b32f7236e8a68c898b67f3b4 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Tue, 1 Apr 2025 11:14:27 +0100 Subject: [PATCH 07/12] adapt to RC refactor --- .../remoteconfig/products/apm_tracing.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/ddtrace/internal/remoteconfig/products/apm_tracing.py b/ddtrace/internal/remoteconfig/products/apm_tracing.py index 392b458a090..699a4162964 100644 --- a/ddtrace/internal/remoteconfig/products/apm_tracing.py +++ b/ddtrace/internal/remoteconfig/products/apm_tracing.py @@ -1,7 +1,10 @@ +import typing as t + from ddtrace import config from ddtrace.internal.core.event_hub import dispatch from ddtrace.internal.core.event_hub import on from ddtrace.internal.logger import get_logger +from ddtrace.internal.remoteconfig import Payload from ddtrace.internal.remoteconfig._connectors import PublisherSubscriberConnector from ddtrace.internal.remoteconfig._publishers import RemoteConfigPublisher from ddtrace.internal.remoteconfig._pubsub import PubSub @@ -14,23 +17,19 @@ log = get_logger(__name__) -def _rc_callback(data, test_tracer=None): - for metadata, data in zip(data["metadata"], data["config"]): - if metadata is None or not isinstance(data, dict): +def _rc_callback(payloads: t.Sequence[Payload]) -> None: + for payload in payloads: + if payload.metadata is None or (content := payload.content) is None: continue - service_target = data.get("service_target") - if service_target is not None: - service = service_target.get("service") - if service is not None and service != config.service: + if (service_target := t.cast(t.Optional[dict], content.get("service_target"))) is not None: + if (service := t.cast(str, service_target.get("service"))) is not None and service != config.service: continue - env = service_target.get("env") - if env is not None and env != config.env: + if (env := t.cast(str, service_target.get("env"))) is not None and env != config.env: continue - lib_config = data.get("lib_config") - if lib_config is not None: + if (lib_config := t.cast(dict, content.get("lib_config"))) is not None: dispatch("apm-tracing.rc", (lib_config,)) From 1761ef3e102b72b1915c9e42ae4feb4cb2687bb4 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Tue, 1 Apr 2025 12:11:15 +0100 Subject: [PATCH 08/12] remove DI changes --- ddtrace/debugging/_debugger.py | 71 +++++++++++++++++- ddtrace/debugging/_import.py | 73 ------------------- .../_products/dynamic_instrumentation.py | 35 +-------- tests/debugging/exploration/debugger.py | 2 +- tests/debugging/mocking.py | 6 +- 5 files changed, 76 insertions(+), 111 deletions(-) delete mode 100644 ddtrace/debugging/_import.py diff --git a/ddtrace/debugging/_debugger.py b/ddtrace/debugging/_debugger.py index 246395eb4c7..ee503da2bbd 100644 --- a/ddtrace/debugging/_debugger.py +++ b/ddtrace/debugging/_debugger.py @@ -7,6 +7,7 @@ import sys import threading import time +from types import CodeType from types import FunctionType from types import ModuleType from types import TracebackType @@ -26,7 +27,6 @@ from ddtrace.debugging._function.discovery import FunctionDiscovery from ddtrace.debugging._function.store import FullyNamedContextWrappedFunction from ddtrace.debugging._function.store import FunctionStore -from ddtrace.debugging._import import DebuggerModuleWatchdog from ddtrace.debugging._metrics import metrics from ddtrace.debugging._probe.model import FunctionLocationMixin from ddtrace.debugging._probe.model import FunctionProbe @@ -45,6 +45,8 @@ from ddtrace.debugging._uploader import UploaderProduct from ddtrace.internal.logger import get_logger from ddtrace.internal.metrics import Metrics +from ddtrace.internal.module import ModuleHookType +from ddtrace.internal.module import ModuleWatchdog from ddtrace.internal.module import origin from ddtrace.internal.module import register_post_run_module_hook from ddtrace.internal.module import unregister_post_run_module_hook @@ -69,6 +71,70 @@ class DebuggerError(Exception): pass +class DebuggerModuleWatchdog(ModuleWatchdog): + _locations: Set[str] = set() + + def transform(self, code: CodeType, module: ModuleType) -> CodeType: + return FunctionDiscovery.transformer(code, module) + + @classmethod + def register_origin_hook(cls, origin: Path, hook: ModuleHookType) -> None: + if origin in cls._locations: + # We already have a hook for this origin, don't register a new one + # but invoke it directly instead, if the module was already loaded. + module = cls.get_by_origin(origin) + if module is not None: + hook(module) + + return + + cls._locations.add(str(origin)) + + super().register_origin_hook(origin, hook) + + @classmethod + def unregister_origin_hook(cls, origin: Path, hook: ModuleHookType) -> None: + try: + cls._locations.remove(str(origin)) + except KeyError: + # Nothing to unregister. + return + + return super().unregister_origin_hook(origin, hook) + + @classmethod + def register_module_hook(cls, module: str, hook: ModuleHookType) -> None: + if module in cls._locations: + # We already have a hook for this origin, don't register a new one + # but invoke it directly instead, if the module was already loaded. + mod = sys.modules.get(module) + if mod is not None: + hook(mod) + + return + + cls._locations.add(module) + + super().register_module_hook(module, hook) + + @classmethod + def unregister_module_hook(cls, module: str, hook: ModuleHookType) -> None: + try: + cls._locations.remove(module) + except KeyError: + # Nothing to unregister. + return + + return super().unregister_module_hook(module, hook) + + @classmethod + def on_run_module(cls, module: ModuleType) -> None: + if cls._instance is not None: + # Treat run module as an import to trigger import hooks and register + # the module's origin. + cls._instance.after_import(module) + + class DebuggerWrappingContext(WrappingContext): __priority__ = 99 # Execute after all other contexts @@ -209,6 +275,8 @@ def enable(cls) -> None: di_config.enabled = True + cls.__watchdog__.install() + if di_config.metrics: metrics.enable() @@ -240,6 +308,7 @@ def disable(cls, join: bool = True) -> None: cls._instance.stop(join=join) cls._instance = None + cls.__watchdog__.uninstall() if di_config.metrics: metrics.disable() diff --git a/ddtrace/debugging/_import.py b/ddtrace/debugging/_import.py deleted file mode 100644 index 107be9d3706..00000000000 --- a/ddtrace/debugging/_import.py +++ /dev/null @@ -1,73 +0,0 @@ -from pathlib import Path -import sys -from types import CodeType -from types import ModuleType -from typing import Set - -from ddtrace.debugging._function.discovery import FunctionDiscovery -from ddtrace.internal.module import ModuleHookType -from ddtrace.internal.module import ModuleWatchdog - - -class DebuggerModuleWatchdog(ModuleWatchdog): - _locations: Set[str] = set() - - def transform(self, code: CodeType, module: ModuleType) -> CodeType: - return FunctionDiscovery.transformer(code, module) - - @classmethod - def register_origin_hook(cls, origin: Path, hook: ModuleHookType) -> None: - if origin in cls._locations: - # We already have a hook for this origin, don't register a new one - # but invoke it directly instead, if the module was already loaded. - module = cls.get_by_origin(origin) - if module is not None: - hook(module) - - return - - cls._locations.add(str(origin)) - - super().register_origin_hook(origin, hook) - - @classmethod - def unregister_origin_hook(cls, origin: Path, hook: ModuleHookType) -> None: - try: - cls._locations.remove(str(origin)) - except KeyError: - # Nothing to unregister. - return - - return super().unregister_origin_hook(origin, hook) - - @classmethod - def register_module_hook(cls, module: str, hook: ModuleHookType) -> None: - if module in cls._locations: - # We already have a hook for this origin, don't register a new one - # but invoke it directly instead, if the module was already loaded. - mod = sys.modules.get(module) - if mod is not None: - hook(mod) - - return - - cls._locations.add(module) - - super().register_module_hook(module, hook) - - @classmethod - def unregister_module_hook(cls, module: str, hook: ModuleHookType) -> None: - try: - cls._locations.remove(module) - except KeyError: - # Nothing to unregister. - return - - return super().unregister_module_hook(module, hook) - - @classmethod - def on_run_module(cls, module: ModuleType) -> None: - if cls._instance is not None: - # Treat run module as an import to trigger import hooks and register - # the module's origin. - cls._instance.after_import(module) diff --git a/ddtrace/debugging/_products/dynamic_instrumentation.py b/ddtrace/debugging/_products/dynamic_instrumentation.py index 73b21f5b294..136d5692ec8 100644 --- a/ddtrace/debugging/_products/dynamic_instrumentation.py +++ b/ddtrace/debugging/_products/dynamic_instrumentation.py @@ -1,11 +1,3 @@ -import enum - -# We need to make sure that remote configuration adapters are loaded before the -# main application starts. This is to make sure that we make all the necessary -# interactions with the multiprocessing module. If the main application uses -# gevent, the module reloading mechanism might cause the multiprocessing module -# to misbehave with errors like "TypeError: this type has no size". -import ddtrace.debugging._probe.remoteconfig # noqa from ddtrace.settings.dynamic_instrumentation import config @@ -13,23 +5,14 @@ def post_preload(): - from ddtrace.debugging._debugger import Debugger - - # 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. - Debugger.__watchdog__.install() - - -def _start(): - from ddtrace.debugging import DynamicInstrumentation - - DynamicInstrumentation.enable() + pass def start(): if config.enabled: - _start() + from ddtrace.debugging import DynamicInstrumentation + + DynamicInstrumentation.enable() def restart(join=False): @@ -46,13 +29,3 @@ def stop(join=False): def at_exit(join=False): stop(join=join) - - -class APMCapabilities(enum.IntFlag): - APM_TRACING_ENABLE_DYNAMIC_INSTRUMENTATION = 1 << 38 - - -def apm_tracing_rc(lib_config): - 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() diff --git a/tests/debugging/exploration/debugger.py b/tests/debugging/exploration/debugger.py index 1c96cf1703f..87224a07746 100644 --- a/tests/debugging/exploration/debugger.py +++ b/tests/debugging/exploration/debugger.py @@ -13,9 +13,9 @@ from ddtrace.debugging._config import di_config import ddtrace.debugging._debugger as _debugger from ddtrace.debugging._debugger import Debugger +from ddtrace.debugging._debugger import DebuggerModuleWatchdog from ddtrace.debugging._encoding import LogSignalJsonEncoder from ddtrace.debugging._function.discovery import FunctionDiscovery -from ddtrace.debugging._import import DebuggerModuleWatchdog from ddtrace.debugging._probe.model import Probe from ddtrace.debugging._probe.remoteconfig import ProbePollerEvent from ddtrace.debugging._signal.collector import SignalCollector diff --git a/tests/debugging/mocking.py b/tests/debugging/mocking.py index 2b87c2766db..a6125b7d2a0 100644 --- a/tests/debugging/mocking.py +++ b/tests/debugging/mocking.py @@ -202,11 +202,7 @@ def _debugger(config_to_override: DDConfig, config_overrides: Any) -> Generator[ def debugger(**config_overrides: Any) -> Generator[TestDebugger, None, None]: """Test with the debugger enabled.""" with _debugger(di_config, config_overrides) as debugger: - debugger.__watchdog__.install() - try: - yield debugger - finally: - debugger.__watchdog__.uninstall() + yield debugger class MockSpanExceptionHandler(SpanExceptionHandler): From a3c24bebd8a251e0091d53523fe7285166637a33 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Tue, 1 Apr 2025 16:03:00 +0100 Subject: [PATCH 09/12] add test helper --- tests/integration/test_settings.py | 18 ++++----- tests/internal/test_settings.py | 65 ++++++++++++++++-------------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/tests/integration/test_settings.py b/tests/integration/test_settings.py index 47742aa906c..71daa34ea11 100644 --- a/tests/integration/test_settings.py +++ b/tests/integration/test_settings.py @@ -136,28 +136,28 @@ def test_remoteconfig_sampling_rate_default(test_agent_session, run_python_code_ """ from ddtrace import config, tracer from tests.internal.test_settings import _base_rc_config -from ddtrace._trace.product import apm_tracing_rc +from tests.internal.test_settings import call_apm_tracing_rc with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") is None -apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.5})) +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.5})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.5 -apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": None})) +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": None})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") is None, "Unsetting remote config trace sample rate" -apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.8})) +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.8})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.8 -apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": None})) +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": None})) with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") is None, "(second time) unsetting remote config trace sample rate" @@ -183,9 +183,9 @@ def test_remoteconfig_sampling_rate_telemetry(test_agent_session, run_python_cod """ from ddtrace import config, tracer from tests.internal.test_settings import _base_rc_config -from ddtrace._trace.product import apm_tracing_rc +from tests.internal.test_settings import call_call_apm_tracing_rc -apm_tracing_rc( +call_apm_tracing_rc( _base_rc_config( { "tracing_sampling_rules": [ @@ -232,9 +232,9 @@ def test_remoteconfig_header_tags_telemetry(test_agent_session, run_python_code_ from ddtrace import config, tracer from ddtrace.contrib import trace_utils from tests.internal.test_settings import _base_rc_config -from ddtrace._trace.product import apm_tracing_rc +from tests.internal.test_settings import call_call_apm_tracing_rc -apm_tracing_rc(_base_rc_config({ +call_apm_tracing_rc(_base_rc_config({ "tracing_header_tags": [ {"header": "used", "tag_name":"header_tag_69"}, {"header": "unused", "tag_name":"header_tag_70"}, diff --git a/tests/internal/test_settings.py b/tests/internal/test_settings.py index 46436f47b77..68919ae1b1a 100644 --- a/tests/internal/test_settings.py +++ b/tests/internal/test_settings.py @@ -1,10 +1,12 @@ import json import os +from typing import Sequence import mock import pytest from ddtrace._trace.product import apm_tracing_rc +from ddtrace.internal.remoteconfig import Payload from ddtrace.settings._config import Config from tests.utils import remote_config_build_payload as build_payload @@ -51,6 +53,14 @@ def _deleted_rc_config(): return [build_payload("APM_TRACING", None, "config", sha_hash="1234")] +def call_apm_tracing_rc(payloads: Sequence[Payload]): + for payload in payloads: + if payload.content is None: + continue + if (lib_config := payload.content.get("lib_config")) is not None: + apm_tracing_rc(lib_config) + + @pytest.mark.parametrize( "testcase", [ @@ -199,7 +209,7 @@ def test_settings_parametrized(testcase, config, monkeypatch): rc_items = testcase.get("rc", {}) if rc_items: - apm_tracing_rc(_base_rc_config(rc_items)) + call_apm_tracing_rc(_base_rc_config(rc_items)) for expected_name, expected_value in testcase["expected"].items(): assert getattr(config, expected_name) == expected_value @@ -229,7 +239,7 @@ def test_settings_missing_lib_config(config, monkeypatch): del base_rc_config[1].content["lib_config"] assert "lib_config" not in base_rc_config[0].content - apm_tracing_rc(base_rc_config) + call_apm_tracing_rc(base_rc_config) for expected_name, expected_value in testcase["expected"].items(): assert getattr(config, expected_name) == expected_value @@ -254,8 +264,7 @@ def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): """ from ddtrace import config, tracer from ddtrace._trace.sampler import DatadogSampler -from tests.internal.test_settings import _base_rc_config, _deleted_rc_config -from ddtrace._trace.product import apm_tracing_rc +from tests.internal.test_settings import _base_rc_config, _deleted_rc_config, call_apm_tracing_rc # Span is sampled using sampling rules from env var with tracer.trace("test") as span: @@ -263,7 +272,7 @@ def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): assert span.get_metric("_dd.rule_psr") == 0.1 assert span.get_tag("_dd.p.dm") == "-3" -apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ { "service": "*", "name": "test", @@ -284,14 +293,14 @@ def test_remoteconfig_sampling_rules(run_python_code_in_subprocess): assert span.context.sampling_priority == 1 # Agent sampling rules do not contain any sampling rules -apm_tracing_rc(_base_rc_config({})) +call_apm_tracing_rc(_base_rc_config({})) # Span is sampled using sampling rules from env var with tracer.trace("test") as span: pass assert span.get_metric("_dd.rule_psr") == 0.1 # Agent sampling rules are set to match service, name, and resource -apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ { "service": "ok", "name": "test", @@ -327,8 +336,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess """ from ddtrace import config, tracer from ddtrace._trace.sampler import DatadogSampler -from tests.internal.test_settings import _base_rc_config, _deleted_rc_config -from ddtrace._trace.product import apm_tracing_rc +from tests.internal.test_settings import _base_rc_config, _deleted_rc_config, call_apm_tracing_rc # Ensure that sampling rule with operation name "rules" is used with tracer.trace("rules") as span: @@ -341,7 +349,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_metric("_dd.rule_psr") == 0.8 assert span.get_tag("_dd.p.dm") == "-3" # Override all sampling rules set via env var with a new rule -apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ { "service": "*", "name": "rules", @@ -362,7 +370,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_tag("_dd.p.dm") == "-0" # Set a new default sampling rate via rc -apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.2})) +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.2})) # Ensure that the new default sampling rate is used with tracer.trace("sample_rate") as span: pass @@ -375,7 +383,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_tag("_dd.p.dm") == "-3" # Set a new sampling rules via rc -apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.3})) +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rate": 0.3})) # Ensure that the new default sampling rate is used with tracer.trace("sample_rate") as span: pass @@ -388,7 +396,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_tag("_dd.p.dm") == "-3" # Remove all sampling rules from remote config -apm_tracing_rc(_base_rc_config({})) +call_apm_tracing_rc(_base_rc_config({})) # Ensure that the default sampling rate set via env vars is used with tracer.trace("rules") as span: pass @@ -401,7 +409,7 @@ def test_remoteconfig_global_sample_rate_and_rules(run_python_code_in_subprocess assert span.get_tag("_dd.p.dm") == "-3" # Test swtting dynamic and customer sampling rules -apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ +call_apm_tracing_rc(_base_rc_config({"tracing_sampling_rules":[ { "service": "*", "name": "rules_dynamic", @@ -444,20 +452,19 @@ def test_remoteconfig_custom_tags(run_python_code_in_subprocess): out, err, status, _ = run_python_code_in_subprocess( """ from ddtrace import config, tracer -from tests.internal.test_settings import _base_rc_config -from ddtrace._trace.product import apm_tracing_rc +from tests.internal.test_settings import _base_rc_config, call_apm_tracing_rc with tracer.trace("test") as span: pass assert span.get_tag("team") == "apm" -apm_tracing_rc(_base_rc_config({"tracing_tags": ["team:onboarding"]})) +call_apm_tracing_rc(_base_rc_config({"tracing_tags": ["team:onboarding"]})) with tracer.trace("test") as span: pass assert span.get_tag("team") == "onboarding", span._meta -apm_tracing_rc(_base_rc_config({})) +call_apm_tracing_rc(_base_rc_config({})) with tracer.trace("test") as span: pass assert span.get_tag("team") == "apm" @@ -473,16 +480,15 @@ def test_remoteconfig_tracing_enabled(run_python_code_in_subprocess): out, err, status, _ = run_python_code_in_subprocess( """ from ddtrace import config, tracer -from tests.internal.test_settings import _base_rc_config -from ddtrace._trace.product import apm_tracing_rc +from tests.internal.test_settings import _base_rc_config, call_apm_tracing_rc assert tracer.enabled is True -apm_tracing_rc(_base_rc_config({"tracing_enabled": "false"})) +call_apm_tracing_rc(_base_rc_config({"tracing_enabled": "false"})) assert tracer.enabled is False -apm_tracing_rc(_base_rc_config({"tracing_enabled": "true"})) +call_apm_tracing_rc(_base_rc_config({"tracing_enabled": "true"})) assert tracer.enabled is False """, @@ -497,19 +503,19 @@ def test_remoteconfig_logs_injection_jsonlogger(run_python_code_in_subprocess): import logging from pythonjsonlogger import jsonlogger from ddtrace import config, tracer -from tests.internal.test_settings import _base_rc_config -from ddtrace._trace.product import apm_tracing_rc +from tests.internal.test_settings import _base_rc_config, call_apm_tracing_rc + log = logging.getLogger() log.level = logging.CRITICAL logHandler = logging.StreamHandler(); logHandler.setFormatter(jsonlogger.JsonFormatter()) log.addHandler(logHandler) # Enable logs injection -apm_tracing_rc(_base_rc_config({"log_injection_enabled": True})) +call_apm_tracing_rc(_base_rc_config({"log_injection_enabled": True})) with tracer.trace("test") as span: print(span.trace_id) log.critical("Hello, World!") # Disable logs injection -apm_tracing_rc(_base_rc_config({"log_injection_enabled": False})) +call_apm_tracing_rc(_base_rc_config({"log_injection_enabled": False})) with tracer.trace("test") as span: print(span.trace_id) log.critical("Hello, World!") @@ -530,8 +536,7 @@ def test_remoteconfig_header_tags(run_python_code_in_subprocess): """ from ddtrace import config, tracer from ddtrace.contrib import trace_utils -from tests.internal.test_settings import _base_rc_config -from ddtrace._trace.product import apm_tracing_rc +from tests.internal.test_settings import _base_rc_config, call_apm_tracing_rc with tracer.trace("test") as span: trace_utils.set_http_meta(span, @@ -542,7 +547,7 @@ def test_remoteconfig_header_tags(run_python_code_in_subprocess): config._http._reset() config._header_tag_name.invalidate() -apm_tracing_rc(_base_rc_config({"tracing_header_tags": +call_apm_tracing_rc(_base_rc_config({"tracing_header_tags": [{"header": "X-Header-Tag-420", "tag_name":"header_tag_420"}]})) with tracer.trace("test_rc_override") as span2: @@ -554,7 +559,7 @@ def test_remoteconfig_header_tags(run_python_code_in_subprocess): config._http._reset() config._header_tag_name.invalidate() -apm_tracing_rc(_base_rc_config({})) +call_apm_tracing_rc(_base_rc_config({})) with tracer.trace("test") as span3: trace_utils.set_http_meta(span3, From 252be2cbdf6ddf08e1e9accde7c2907dcf83a959 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Wed, 2 Apr 2025 09:08:38 +0100 Subject: [PATCH 10/12] fix settings tests --- tests/internal/test_settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/internal/test_settings.py b/tests/internal/test_settings.py index 68919ae1b1a..fcd3799b4c4 100644 --- a/tests/internal/test_settings.py +++ b/tests/internal/test_settings.py @@ -13,7 +13,13 @@ @pytest.fixture def config(): - yield Config() + import ddtrace + + original_config = ddtrace.config + ddtrace.config = Config() + yield ddtrace.config + # Reset the config to its original state + ddtrace.config = original_config def _base_rc_config(cfg): From 0383218ac2e06e95575c4ebeb8bd607a20a47df6 Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Wed, 2 Apr 2025 09:38:48 +0100 Subject: [PATCH 11/12] make testagent tests verbose --- riotfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/riotfile.py b/riotfile.py index 65e51850c01..25b5d7a4999 100644 --- a/riotfile.py +++ b/riotfile.py @@ -224,7 +224,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT name="integration", # Enabling coverage for integration tests breaks certain tests in CI # Also, running two separate pytest sessions, the ``civisibility`` one with --no-ddtrace - command="pytest --no-ddtrace --no-cov --ignore-glob='*civisibility*' {cmdargs} tests/integration/", + command="pytest -vv --no-ddtrace --no-cov --ignore-glob='*civisibility*' {cmdargs} tests/integration/", pkgs={"msgpack": [latest], "coverage": latest, "pytest-randomly": latest}, pys=select_pys(), venvs=[ From e7e9df7b87806028d3d7ee9177775b1c59df63be Mon Sep 17 00:00:00 2001 From: "Gabriele N. Tornetta" Date: Wed, 2 Apr 2025 09:59:54 +0100 Subject: [PATCH 12/12] fix testagent tests --- tests/integration/test_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_settings.py b/tests/integration/test_settings.py index 71daa34ea11..b7313d71e59 100644 --- a/tests/integration/test_settings.py +++ b/tests/integration/test_settings.py @@ -183,7 +183,7 @@ def test_remoteconfig_sampling_rate_telemetry(test_agent_session, run_python_cod """ from ddtrace import config, tracer from tests.internal.test_settings import _base_rc_config -from tests.internal.test_settings import call_call_apm_tracing_rc +from tests.internal.test_settings import call_apm_tracing_rc call_apm_tracing_rc( _base_rc_config( @@ -232,7 +232,7 @@ def test_remoteconfig_header_tags_telemetry(test_agent_session, run_python_code_ from ddtrace import config, tracer from ddtrace.contrib import trace_utils from tests.internal.test_settings import _base_rc_config -from tests.internal.test_settings import call_call_apm_tracing_rc +from tests.internal.test_settings import call_apm_tracing_rc call_apm_tracing_rc(_base_rc_config({ "tracing_header_tags": [