From a0891cf1a26d2d437c9c2ce35747596b6939c774 Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Thu, 11 Dec 2025 12:17:29 -0500 Subject: [PATCH 01/13] Merge branch 'main' into dramatiq_core --- ddtrace/contrib/falcon.py | 7 +-- ddtrace/contrib/internal/falcon/middleware.py | 2 +- ddtrace/internal/settings/integration.py | 2 - tests/contrib/falcon/test_suite.py | 4 +- tests/tracer/test_global_config.py | 52 +++++++++++-------- 5 files changed, 37 insertions(+), 30 deletions(-) diff --git a/ddtrace/contrib/falcon.py b/ddtrace/contrib/falcon.py index c0b09c94e01..736bf9b9614 100644 --- a/ddtrace/contrib/falcon.py +++ b/ddtrace/contrib/falcon.py @@ -25,7 +25,7 @@ The following is a list of available tracer hooks that can be used to intercept and modify spans created by this integration. -- ``request`` +- ``falcon.request`` - Called before the response has been finished - ``def on_falcon_request(span, request, response)`` @@ -34,14 +34,15 @@ import ddtrace.auto import falcon - from ddtrace import config + from ddtrace.internal import core app = falcon.API() - @config.falcon.hooks.on('request') def on_falcon_request(span, request, response): span.set_tag('my.custom', 'tag') + core.on('falcon.request', on_falcon_request) + :ref:`Headers tracing ` is supported for this integration. """ diff --git a/ddtrace/contrib/internal/falcon/middleware.py b/ddtrace/contrib/internal/falcon/middleware.py index 38f1c89fb3a..46a4c9b9ebc 100644 --- a/ddtrace/contrib/internal/falcon/middleware.py +++ b/ddtrace/contrib/internal/falcon/middleware.py @@ -83,7 +83,7 @@ def process_response(self, req, resp, resource, req_succeeded=None): # Emit span hook for this response # DEV: Emit before closing so they can overwrite `span.resource` if they want - config.falcon.hooks.emit("request", span, req, resp) + core.dispatch("falcon.request", (span, req, resp)) core.dispatch( "web.request.finish", (span, config.falcon, None, None, status, None, None, resp._headers, route, True) diff --git a/ddtrace/internal/settings/integration.py b/ddtrace/internal/settings/integration.py index 6cea1c33c75..ec1e0a71b9b 100644 --- a/ddtrace/internal/settings/integration.py +++ b/ddtrace/internal/settings/integration.py @@ -1,7 +1,6 @@ import os from typing import Optional # noqa:F401 -from ddtrace._hooks import Hooks from ddtrace.internal.utils.attrdict import AttrDict from .http import HttpConfig @@ -36,7 +35,6 @@ def __init__(self, global_config, name, *args, **kwargs): # DEV: By-pass the `__setattr__` overrides from `AttrDict` to set real properties object.__setattr__(self, "global_config", global_config) object.__setattr__(self, "integration_name", name) - object.__setattr__(self, "hooks", Hooks()) object.__setattr__(self, "http", HttpConfig()) # Trace Analytics was removed in v3.0.0 diff --git a/tests/contrib/falcon/test_suite.py b/tests/contrib/falcon/test_suite.py index 9771158e34d..22a13e128a6 100644 --- a/tests/contrib/falcon/test_suite.py +++ b/tests/contrib/falcon/test_suite.py @@ -3,6 +3,7 @@ from ddtrace.constants import USER_KEEP from ddtrace.contrib.internal.falcon.patch import FALCON_VERSION from ddtrace.ext import http as httpx +from ddtrace.internal import core from tests.tracer.utils_inferred_spans.test_helpers import assert_web_and_inferred_aws_api_gateway_span_data from tests.utils import assert_is_measured from tests.utils import assert_span_http_status_code @@ -225,10 +226,11 @@ def test_404_exception_no_stacktracer(self): assert span.get_tag("span.kind") == "server" def test_falcon_request_hook(self): - @config.falcon.hooks.on("request") def on_falcon_request(span, request, response): span.set_tag("my.custom", "tag") + core.on("falcon.request", on_falcon_request) + out = self.make_test_call("/200", expected_status_code=200) assert out.content.decode("utf-8") == "Success" diff --git a/tests/tracer/test_global_config.py b/tests/tracer/test_global_config.py index 0d60f2319e7..35829890d05 100644 --- a/tests/tracer/test_global_config.py +++ b/tests/tracer/test_global_config.py @@ -4,6 +4,7 @@ import pytest from ddtrace import config as global_config +from ddtrace.internal import core from ddtrace.internal.settings._config import Config from ddtrace.internal.settings.integration import IntegrationConfig @@ -114,46 +115,48 @@ def test_settings_merge_deep(self): def test_settings_hook(self): """ - When calling `Hooks.emit()` + When calling `core.dispatch()` When there is a hook registered we call the hook as expected """ # Setup our hook - @self.config.web.hooks.on("request") def on_web_request(span): span.set_tag("web.request", "/") + core.on("web.request", on_web_request) + # Create our span with self.tracer.start_span("web.request") as span: assert "web.request" not in span.get_tags() # Emit the span - self.config.web.hooks.emit("request", span) + core.dispatch("web.request", (span,)) # Assert we updated the span as expected assert span.get_tag("web.request") == "/" def test_settings_hook_args(self): """ - When calling `Hooks.emit()` with arguments + When calling `core.dispatch()` with arguments When there is a hook registered we call the hook as expected """ # Setup our hook - @self.config.web.hooks.on("request") def on_web_request(span, request, response): span.set_tag("web.request", request) span.set_tag("web.response", response) + core.on("web.request", on_web_request) + # Create our span with self.tracer.start_span("web.request") as span: assert "web.request" not in span.get_tags() # Emit the span - # DEV: The actual values don't matter, we just want to test args + kwargs usage - self.config.web.hooks.emit("request", span, "request", response="response") + # DEV: The actual values don't matter, we just want to test args usage + core.dispatch("web.request", (span, "request", "response")) # Assert we updated the span as expected assert span.get_tag("web.request") == "request" @@ -161,48 +164,50 @@ def on_web_request(span, request, response): def test_settings_hook_args_failure(self): """ - When calling `Hooks.emit()` with arguments + When calling `core.dispatch()` with arguments When there is a hook registered that is missing parameters we do not raise an exception """ # Setup our hook # DEV: We are missing the required "response" argument - @self.config.web.hooks.on("request") def on_web_request(span, request): span.set_tag("web.request", request) + core.on("web.request", on_web_request) + # Create our span with self.tracer.start_span("web.request") as span: assert "web.request" not in span.get_tags() # Emit the span # DEV: This also asserts that no exception was raised - self.config.web.hooks.emit("request", span, "request", response="response") + core.dispatch("web.request", (span, "request", "response")) # Assert we did not update the span assert "web.request" not in span.get_tags() def test_settings_multiple_hooks(self): """ - When calling `Hooks.emit()` + When calling `core.dispatch()` When there are multiple hooks registered we do not raise an exception """ # Setup our hooks - @self.config.web.hooks.on("request") def on_web_request(span): span.set_tag("web.request", "/") - @self.config.web.hooks.on("request") def on_web_request2(span): span.set_tag("web.status", 200) - @self.config.web.hooks.on("request") def on_web_request3(span): span.set_tag("web.method", "GET") + core.on("web.request", on_web_request) + core.on("web.request", on_web_request2) + core.on("web.request", on_web_request3) + # Create our span with self.tracer.start_span("web.request") as span: assert "web.request" not in span.get_tags() @@ -210,7 +215,7 @@ def on_web_request3(span): assert "web.method" not in span.get_tags() # Emit the span - self.config.web.hooks.emit("request", span) + core.dispatch("web.request", (span,)) # Assert we updated the span as expected assert span.get_tag("web.request") == "/" @@ -219,24 +224,24 @@ def on_web_request3(span): def test_settings_hook_failure(self): """ - When calling `Hooks.emit()` + When calling `core.dispatch()` When the hook raises an exception we do not raise an exception """ # Setup our failing hook on_web_request = mock.Mock(side_effect=Exception) - self.config.web.hooks.register("request")(on_web_request) + core.on("web.request", on_web_request) # Create our span with self.tracer.start_span("web.request") as span: # Emit the span # DEV: This is the test, to ensure no exceptions are raised - self.config.web.hooks.emit("request", span) + core.dispatch("web.request", (span,)) on_web_request.assert_called() def test_settings_no_hook(self): """ - When calling `Hooks.emit()` + When calling `core.dispatch()` When no hook is registered we do not raise an exception """ @@ -244,23 +249,24 @@ def test_settings_no_hook(self): with self.tracer.start_span("web.request") as span: # Emit the span # DEV: This is the test, to ensure no exceptions are raised - self.config.web.hooks.emit("request", span) + core.dispatch("web.request", (span,)) def test_settings_no_span(self): """ - When calling `Hooks.emit()` + When calling `core.dispatch()` When no span is provided we do not raise an exception """ # Setup our hooks - @self.config.web.hooks.on("request") def on_web_request(span): span.set_tag("web.request", "/") + core.on("web.request", on_web_request) + # Emit the span # DEV: This is the test, to ensure no exceptions are raised - self.config.web.hooks.emit("request", None) + core.dispatch("web.request", (None,)) def test_dd_version(self): c = Config() From 4d07beedae525718cb1ac3a19c5ccbad7b93a2eb Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Thu, 11 Dec 2025 13:23:08 -0500 Subject: [PATCH 02/13] remove tests for hook --- ddtrace/_hooks.py | 134 ----------------------------- tests/tracer/test_global_config.py | 4 + tests/tracer/test_hooks.py | 29 ------- 3 files changed, 4 insertions(+), 163 deletions(-) delete mode 100644 ddtrace/_hooks.py delete mode 100644 tests/tracer/test_hooks.py diff --git a/ddtrace/_hooks.py b/ddtrace/_hooks.py deleted file mode 100644 index 8aa17365b2d..00000000000 --- a/ddtrace/_hooks.py +++ /dev/null @@ -1,134 +0,0 @@ -import collections -from copy import deepcopy -from typing import Any # noqa:F401 -from typing import Callable # noqa:F401 -from typing import DefaultDict # noqa:F401 -from typing import Optional # noqa:F401 -from typing import Set # noqa:F401 - -from .internal.logger import get_logger - - -log = get_logger(__name__) - - -class Hooks: - """ - Hooks configuration object is used for registering and calling hook functions - - Example:: - - @config.falcon.hooks.on('request') - def on_request(span, request, response): - pass - """ - - _hooks: DefaultDict[str, Set] - __slots__ = ("_hooks",) - - def __init__(self): - self._hooks = collections.defaultdict(set) - - def __deepcopy__(self, memodict=None): - hooks = Hooks() - hooks._hooks = deepcopy(self._hooks, memodict) - return hooks - - def register( - self, - hook, # type: Any - func=None, # type: Optional[Callable] - ): - # type: (...) -> Optional[Callable[..., Any]] - """ - Function used to register a hook for the provided name. - - Example:: - - def on_request(span, request, response): - pass - - config.falcon.hooks.register('request', on_request) - - - If no function is provided then a decorator is returned:: - - @config.falcon.hooks.register('request') - def on_request(span, request, response): - pass - - :param hook: The name of the hook to register the function for - :type hook: object - :param func: The function to register, or ``None`` if a decorator should be returned - :type func: function, None - :returns: Either a function decorator if ``func is None``, otherwise ``None`` - :rtype: function, None - """ - # If they didn't provide a function, then return a decorator - if not func: - - def wrapper(func): - self.register(hook, func) - return func - - return wrapper - self._hooks[hook].add(func) - return None - - # Provide shorthand `on` method for `register` - # >>> @config.falcon.hooks.on('request') - # def on_request(span, request, response): - # pass - on = register - - def deregister( - self, - hook, # type: Any - func, # type: Callable - ): - # type: (...) -> None - """ - Function to deregister a function from a hook it was registered under - - Example:: - - @config.falcon.hooks.on('request') - def on_request(span, request, response): - pass - - config.falcon.hooks.deregister('request', on_request) - - :param hook: The name of the hook to register the function for - :type hook: object - :param func: Function hook to register - :type func: function - """ - if hook in self._hooks: - try: - self._hooks[hook].remove(func) - except KeyError: - pass - - def emit( - self, - hook, # type: Any - *args, # type: Any - **kwargs, # type: Any - ): - # type: (...) -> None - """ - Function used to call registered hook functions. - - :param hook: The hook to call functions for - :type hook: str - :param args: Positional arguments to pass to the hook functions - :type args: list - :param kwargs: Keyword arguments to pass to the hook functions - :type kwargs: dict - """ - # Call registered hooks - for func in self._hooks.get(hook, ()): - try: - func(*args, **kwargs) - except Exception: - log.error("Failed to run hook %s function %s", hook, func, exc_info=True) diff --git a/tests/tracer/test_global_config.py b/tests/tracer/test_global_config.py index 35829890d05..4bccd418863 100644 --- a/tests/tracer/test_global_config.py +++ b/tests/tracer/test_global_config.py @@ -20,6 +20,10 @@ def setUp(self): self.config.web = IntegrationConfig(self.config, "web") self.tracer = DummyTracer() + def tearDown(self): + # Reset all core event listeners after each test to ensure test isolation + core.reset() + def test_registration(self): # ensure an integration can register a new list of settings settings = { diff --git a/tests/tracer/test_hooks.py b/tests/tracer/test_hooks.py deleted file mode 100644 index c09e496797d..00000000000 --- a/tests/tracer/test_hooks.py +++ /dev/null @@ -1,29 +0,0 @@ -from ddtrace import _hooks - - -def test_deregister(): - hooks = _hooks.Hooks() - - x = {} - - @hooks.register("key") - def do_not_call(): - x["x"] = True - - hooks.emit("key") - assert x["x"] - del x["x"] - - hooks.deregister("key", do_not_call) - - hooks.emit("key") - - assert len(x) == 0 - - -def test_deregister_unknown(): - hooks = _hooks.Hooks() - - hooks.deregister("key", test_deregister_unknown) - - hooks.emit("key") From b226bc85612453846c41294847e5ff8751322f35 Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Thu, 11 Dec 2025 13:26:25 -0500 Subject: [PATCH 03/13] nit: use core.reset_listeners() --- tests/tracer/test_global_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tracer/test_global_config.py b/tests/tracer/test_global_config.py index 4bccd418863..7a3841f7d6e 100644 --- a/tests/tracer/test_global_config.py +++ b/tests/tracer/test_global_config.py @@ -22,7 +22,7 @@ def setUp(self): def tearDown(self): # Reset all core event listeners after each test to ensure test isolation - core.reset() + core.reset_listeners() def test_registration(self): # ensure an integration can register a new list of settings From 855aaf13c9dda9322a1d039f376f9bb14b1ae9e8 Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Thu, 11 Dec 2025 14:20:58 -0500 Subject: [PATCH 04/13] remove hooks.py from suitespec --- tests/suitespec.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/suitespec.yml b/tests/suitespec.yml index dd67e52deb9..4f9d4fcb435 100644 --- a/tests/suitespec.yml +++ b/tests/suitespec.yml @@ -114,7 +114,6 @@ components: telemetry: - ddtrace/internal/telemetry/* tracing: - - ddtrace/_hooks.py - ddtrace/_logger.py - ddtrace/_monkey.py - ddtrace/_trace/* From 9ab45f6760b389d78f5f0eb1b751eb8a4ebed0c2 Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Thu, 11 Dec 2025 15:00:04 -0500 Subject: [PATCH 05/13] add with_config_raise_value decorator for hooks test --- tests/tracer/test_global_config.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/tracer/test_global_config.py b/tests/tracer/test_global_config.py index 7a3841f7d6e..b0bc92b2d2a 100644 --- a/tests/tracer/test_global_config.py +++ b/tests/tracer/test_global_config.py @@ -12,6 +12,21 @@ from ..utils import override_env +def with_config_raise_value(raise_value: bool): + def _outer(func): + def _inner(*args, **kwargs): + original_value = global_config._raise + global_config._raise = raise_value + try: + return func(*args, **kwargs) + finally: + global_config._raise = original_value + + return _inner + + return _outer + + class GlobalConfigTestCase(TestCase): """Test the `Configuration` class that stores integration settings""" @@ -166,6 +181,7 @@ def on_web_request(span, request, response): assert span.get_tag("web.request") == "request" assert span.get_tag("web.response") == "response" + @with_config_raise_value(raise_value=False) def test_settings_hook_args_failure(self): """ When calling `core.dispatch()` with arguments @@ -226,6 +242,7 @@ def on_web_request3(span): assert span.get_metric("web.status") == 200 assert span.get_tag("web.method") == "GET" + @with_config_raise_value(raise_value=False) def test_settings_hook_failure(self): """ When calling `core.dispatch()` From 56aecee54b82d1f05770d7060056f3830261b2a0 Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Thu, 11 Dec 2025 15:26:58 -0500 Subject: [PATCH 06/13] Update test_global_config.py --- tests/tracer/test_global_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/tracer/test_global_config.py b/tests/tracer/test_global_config.py index b0bc92b2d2a..106e09dec66 100644 --- a/tests/tracer/test_global_config.py +++ b/tests/tracer/test_global_config.py @@ -272,6 +272,7 @@ def test_settings_no_hook(self): # DEV: This is the test, to ensure no exceptions are raised core.dispatch("web.request", (span,)) + @with_config_raise_value(raise_value=False) def test_settings_no_span(self): """ When calling `core.dispatch()` From f725cdf7c5bc262eca2307a454748cbececc5042 Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Fri, 12 Dec 2025 13:05:57 -0500 Subject: [PATCH 07/13] remove hooks test completely --- ddtrace/contrib/falcon.py | 23 ---- tests/tracer/test_global_config.py | 179 ----------------------------- 2 files changed, 202 deletions(-) diff --git a/ddtrace/contrib/falcon.py b/ddtrace/contrib/falcon.py index 736bf9b9614..8cba178ef81 100644 --- a/ddtrace/contrib/falcon.py +++ b/ddtrace/contrib/falcon.py @@ -20,29 +20,6 @@ To disable distributed tracing when using autopatching, set the ``DD_FALCON_DISTRIBUTED_TRACING`` environment variable to ``False``. -**Supported span hooks** - -The following is a list of available tracer hooks that can be used to intercept -and modify spans created by this integration. - -- ``falcon.request`` - - Called before the response has been finished - - ``def on_falcon_request(span, request, response)`` - - -Example:: - - import ddtrace.auto - import falcon - from ddtrace.internal import core - - app = falcon.API() - - def on_falcon_request(span, request, response): - span.set_tag('my.custom', 'tag') - - core.on('falcon.request', on_falcon_request) - :ref:`Headers tracing ` is supported for this integration. """ diff --git a/tests/tracer/test_global_config.py b/tests/tracer/test_global_config.py index 106e09dec66..323770ee52e 100644 --- a/tests/tracer/test_global_config.py +++ b/tests/tracer/test_global_config.py @@ -1,10 +1,8 @@ from unittest import TestCase -import mock import pytest from ddtrace import config as global_config -from ddtrace.internal import core from ddtrace.internal.settings._config import Config from ddtrace.internal.settings.integration import IntegrationConfig @@ -12,21 +10,6 @@ from ..utils import override_env -def with_config_raise_value(raise_value: bool): - def _outer(func): - def _inner(*args, **kwargs): - original_value = global_config._raise - global_config._raise = raise_value - try: - return func(*args, **kwargs) - finally: - global_config._raise = original_value - - return _inner - - return _outer - - class GlobalConfigTestCase(TestCase): """Test the `Configuration` class that stores integration settings""" @@ -35,10 +18,6 @@ def setUp(self): self.config.web = IntegrationConfig(self.config, "web") self.tracer = DummyTracer() - def tearDown(self): - # Reset all core event listeners after each test to ensure test isolation - core.reset_listeners() - def test_registration(self): # ensure an integration can register a new list of settings settings = { @@ -132,164 +111,6 @@ def test_settings_merge_deep(self): assert self.config.requests["a"]["b"]["c"] is True assert self.config.requests["a"]["b"]["d"] is True - def test_settings_hook(self): - """ - When calling `core.dispatch()` - When there is a hook registered - we call the hook as expected - """ - - # Setup our hook - def on_web_request(span): - span.set_tag("web.request", "/") - - core.on("web.request", on_web_request) - - # Create our span - with self.tracer.start_span("web.request") as span: - assert "web.request" not in span.get_tags() - - # Emit the span - core.dispatch("web.request", (span,)) - - # Assert we updated the span as expected - assert span.get_tag("web.request") == "/" - - def test_settings_hook_args(self): - """ - When calling `core.dispatch()` with arguments - When there is a hook registered - we call the hook as expected - """ - - # Setup our hook - def on_web_request(span, request, response): - span.set_tag("web.request", request) - span.set_tag("web.response", response) - - core.on("web.request", on_web_request) - - # Create our span - with self.tracer.start_span("web.request") as span: - assert "web.request" not in span.get_tags() - - # Emit the span - # DEV: The actual values don't matter, we just want to test args usage - core.dispatch("web.request", (span, "request", "response")) - - # Assert we updated the span as expected - assert span.get_tag("web.request") == "request" - assert span.get_tag("web.response") == "response" - - @with_config_raise_value(raise_value=False) - def test_settings_hook_args_failure(self): - """ - When calling `core.dispatch()` with arguments - When there is a hook registered that is missing parameters - we do not raise an exception - """ - - # Setup our hook - # DEV: We are missing the required "response" argument - def on_web_request(span, request): - span.set_tag("web.request", request) - - core.on("web.request", on_web_request) - - # Create our span - with self.tracer.start_span("web.request") as span: - assert "web.request" not in span.get_tags() - - # Emit the span - # DEV: This also asserts that no exception was raised - core.dispatch("web.request", (span, "request", "response")) - - # Assert we did not update the span - assert "web.request" not in span.get_tags() - - def test_settings_multiple_hooks(self): - """ - When calling `core.dispatch()` - When there are multiple hooks registered - we do not raise an exception - """ - - # Setup our hooks - def on_web_request(span): - span.set_tag("web.request", "/") - - def on_web_request2(span): - span.set_tag("web.status", 200) - - def on_web_request3(span): - span.set_tag("web.method", "GET") - - core.on("web.request", on_web_request) - core.on("web.request", on_web_request2) - core.on("web.request", on_web_request3) - - # Create our span - with self.tracer.start_span("web.request") as span: - assert "web.request" not in span.get_tags() - assert "web.status" not in span.get_metrics() - assert "web.method" not in span.get_tags() - - # Emit the span - core.dispatch("web.request", (span,)) - - # Assert we updated the span as expected - assert span.get_tag("web.request") == "/" - assert span.get_metric("web.status") == 200 - assert span.get_tag("web.method") == "GET" - - @with_config_raise_value(raise_value=False) - def test_settings_hook_failure(self): - """ - When calling `core.dispatch()` - When the hook raises an exception - we do not raise an exception - """ - # Setup our failing hook - on_web_request = mock.Mock(side_effect=Exception) - core.on("web.request", on_web_request) - - # Create our span - with self.tracer.start_span("web.request") as span: - # Emit the span - # DEV: This is the test, to ensure no exceptions are raised - core.dispatch("web.request", (span,)) - on_web_request.assert_called() - - def test_settings_no_hook(self): - """ - When calling `core.dispatch()` - When no hook is registered - we do not raise an exception - """ - # Create our span - with self.tracer.start_span("web.request") as span: - # Emit the span - # DEV: This is the test, to ensure no exceptions are raised - core.dispatch("web.request", (span,)) - - @with_config_raise_value(raise_value=False) - def test_settings_no_span(self): - """ - When calling `core.dispatch()` - When no span is provided - we do not raise an exception - """ - - # Setup our hooks - def on_web_request(span): - span.set_tag("web.request", "/") - - core.on("web.request", on_web_request) - - # Emit the span - # DEV: This is the test, to ensure no exceptions are raised - core.dispatch("web.request", (None,)) - def test_dd_version(self): c = Config() assert c.version is None From d252e62988c81cc5687a79f38eae21e6a2a19300 Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Fri, 12 Dec 2025 14:05:39 -0500 Subject: [PATCH 08/13] make hooks no op and throw deprecation warnings --- ddtrace/contrib/internal/falcon/middleware.py | 4 - ddtrace/internal/settings/integration.py | 75 +++++++++++++++++++ tests/tracer/test_hooks.py | 68 +++++++++++++++++ 3 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 tests/tracer/test_hooks.py diff --git a/ddtrace/contrib/internal/falcon/middleware.py b/ddtrace/contrib/internal/falcon/middleware.py index 46a4c9b9ebc..a689a767fe4 100644 --- a/ddtrace/contrib/internal/falcon/middleware.py +++ b/ddtrace/contrib/internal/falcon/middleware.py @@ -81,10 +81,6 @@ def process_response(self, req, resp, resource, req_succeeded=None): route = req.root_path or "" + req.uri_template - # Emit span hook for this response - # DEV: Emit before closing so they can overwrite `span.resource` if they want - core.dispatch("falcon.request", (span, req, resp)) - core.dispatch( "web.request.finish", (span, config.falcon, None, None, status, None, None, resp._headers, route, True) ) diff --git a/ddtrace/internal/settings/integration.py b/ddtrace/internal/settings/integration.py index ec1e0a71b9b..1090a834390 100644 --- a/ddtrace/internal/settings/integration.py +++ b/ddtrace/internal/settings/integration.py @@ -2,6 +2,8 @@ from typing import Optional # noqa:F401 from ddtrace.internal.utils.attrdict import AttrDict +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning +from ddtrace.vendor.debtcollector import deprecate from .http import HttpConfig @@ -36,6 +38,7 @@ def __init__(self, global_config, name, *args, **kwargs): object.__setattr__(self, "global_config", global_config) object.__setattr__(self, "integration_name", name) object.__setattr__(self, "http", HttpConfig()) + object.__setattr__(self, "hooks", Hooks()) # Trace Analytics was removed in v3.0.0 # TODO(munir): Remove all references to analytics_enabled and analytics_sample_rate @@ -113,3 +116,75 @@ def copy(self): new_instance = self.__class__(self.global_config, self.integration_name) new_instance.update(self) return new_instance + + +class Hooks: + """ + Deprecated no-op Hooks class for backwards compatibility. + + This class previously provided hook registration and invocation functionality, + but has been deprecated in favor of using the public tracing API. + + All methods are now no-ops to maintain backwards compatibility with existing code. + + .. deprecated:: 4.0 + The Hooks class is deprecated and will be removed in v5.0. + To interact with spans, use ``get_current_span()`` or ``get_current_root_span()``. + """ + + __slots__ = () + + def register(self, hook, func=None): + """No-op: Hook registration is deprecated. + + .. deprecated:: 4.0 + Hook registration via ``config..hooks.register()`` is deprecated + and will be removed in v5.0. To interact with spans, use ``get_current_span()`` + or ``get_current_root_span()``. + """ + deprecate( + "Hooks.register() is deprecated and is currently a no-op.", + message="To interact with spans, use get_current_span() or get_current_root_span().", + removal_version="5.0.0", + category=DDTraceDeprecationWarning, + ) + if not func: + # Return a no-op decorator + def wrapper(func): + return func + + return wrapper + return None + + # Provide shorthand `on` method for `register` + on = register + + def deregister(self, hook, func): + """No-op: Hook deregistration is deprecated. + + .. deprecated:: 4.0 + Hook deregistration via ``config..hooks.deregister()`` is deprecated + and will be removed in v5.0. + """ + deprecate( + "Hooks.deregister() is deprecated and is currently a no-op.", + removal_version="5.0.0", + category=DDTraceDeprecationWarning, + ) + pass + + def emit(self, hook, *args, **kwargs): + """No-op: Hook emission is deprecated. + + .. deprecated:: 4.0 + Hook emission via ``config..hooks.emit()`` is deprecated + and will be removed in v5.0. To interact with spans, use ``get_current_span()`` + or ``get_current_root_span()``. + """ + deprecate( + "Hooks.emit() is deprecated and is currently a no-op.", + message="To interact with spans, use get_current_span() or get_current_root_span().", + removal_version="5.0.0", + category=DDTraceDeprecationWarning, + ) + pass diff --git a/tests/tracer/test_hooks.py b/tests/tracer/test_hooks.py new file mode 100644 index 00000000000..ff50c529754 --- /dev/null +++ b/tests/tracer/test_hooks.py @@ -0,0 +1,68 @@ +from unittest import TestCase + +import pytest + +from ddtrace.internal.settings._config import Config +from ddtrace.internal.settings.integration import IntegrationConfig + + +class HooksDeprecationTestCase(TestCase): + """Test that Hooks class methods raise deprecation warnings""" + + def setUp(self): + self.config = Config() + self.config.web = IntegrationConfig(self.config, "web") + + def test_hooks_register_deprecation(self): + """ + When calling `config.hooks.register()` + A deprecation warning is raised + """ + with pytest.warns(match="Hooks.register\\(\\) is deprecated and is currently a no-op"): + self.config.web.hooks.register("test_hook", lambda: None) + + def test_hooks_on_deprecation(self): + """ + When calling `config.hooks.on()` (alias for register) + A deprecation warning is raised + """ + with pytest.warns(match="Hooks.register\\(\\) is deprecated and is currently a no-op"): + self.config.web.hooks.on("test_hook", lambda: None) + + def test_hooks_deregister_deprecation(self): + """ + When calling `config.hooks.deregister()` + A deprecation warning is raised + """ + with pytest.warns(match="Hooks.deregister\\(\\) is deprecated and is currently a no-op"): + self.config.web.hooks.deregister("test_hook", lambda: None) + + def test_hooks_emit_deprecation(self): + """ + When calling `config.hooks.emit()` + A deprecation warning is raised + """ + with pytest.warns(match="Hooks.emit\\(\\) is deprecated and is currently a no-op"): + self.config.web.hooks.emit("test_hook") + + def test_hooks_register_decorator_deprecation(self): + """ + When calling `config.hooks.register()` as a decorator + A deprecation warning is raised + """ + with pytest.warns(match="Hooks.register\\(\\) is deprecated and is currently a no-op"): + + @self.config.web.hooks.register("test_hook") + def my_hook(): + pass + + def test_hooks_on_decorator_deprecation(self): + """ + When calling `config.hooks.on()` as a decorator + A deprecation warning is raised + """ + with pytest.warns(match="Hooks.register\\(\\) is deprecated and is currently a no-op"): + + @self.config.web.hooks.on("test_hook") + def my_hook(): + pass From 346d1ca20d87266266e2b9ccab9783e00b05c20a Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Fri, 12 Dec 2025 14:09:36 -0500 Subject: [PATCH 09/13] remove hooks test from falcon --- tests/contrib/falcon/test_suite.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/tests/contrib/falcon/test_suite.py b/tests/contrib/falcon/test_suite.py index 22a13e128a6..3293259dd4b 100644 --- a/tests/contrib/falcon/test_suite.py +++ b/tests/contrib/falcon/test_suite.py @@ -3,7 +3,6 @@ from ddtrace.constants import USER_KEEP from ddtrace.contrib.internal.falcon.patch import FALCON_VERSION from ddtrace.ext import http as httpx -from ddtrace.internal import core from tests.tracer.utils_inferred_spans.test_helpers import assert_web_and_inferred_aws_api_gateway_span_data from tests.utils import assert_is_measured from tests.utils import assert_span_http_status_code @@ -225,28 +224,6 @@ def test_404_exception_no_stacktracer(self): assert span.get_tag("component") == "falcon" assert span.get_tag("span.kind") == "server" - def test_falcon_request_hook(self): - def on_falcon_request(span, request, response): - span.set_tag("my.custom", "tag") - - core.on("falcon.request", on_falcon_request) - - out = self.make_test_call("/200", expected_status_code=200) - assert out.content.decode("utf-8") == "Success" - - traces = self.tracer.pop_traces() - assert len(traces) == 1 - assert len(traces[0]) == 1 - span = traces[0][0] - assert span.get_tag("http.request.headers.my_header") is None - assert span.get_tag("http.response.headers.my_response_header") is None - - assert span.name == "falcon.request" - - assert span.get_tag("my.custom") == "tag" - - assert span.error == 0 - def test_http_header_tracing(self): with self.override_config("falcon", {}): config.falcon.http.trace_headers(["my-header", "my-response-header"]) From 6913d50b465adcfefa74c9855a62df87a66337c8 Mon Sep 17 00:00:00 2001 From: Mohammad A Islam <73513662+chojomok@users.noreply.github.com> Date: Fri, 12 Dec 2025 15:26:37 -0500 Subject: [PATCH 10/13] Update ddtrace/internal/settings/integration.py Co-authored-by: Munir Abdinur --- ddtrace/internal/settings/integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ddtrace/internal/settings/integration.py b/ddtrace/internal/settings/integration.py index 1090a834390..1d8521fb212 100644 --- a/ddtrace/internal/settings/integration.py +++ b/ddtrace/internal/settings/integration.py @@ -156,8 +156,8 @@ def wrapper(func): return wrapper return None - # Provide shorthand `on` method for `register` - on = register + def on(self): + return self.register() def deregister(self, hook, func): """No-op: Hook deregistration is deprecated. From 010261495dc602a43d96e84495f50768c2cba25a Mon Sep 17 00:00:00 2001 From: Mohammad A Islam <73513662+chojomok@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:28:25 -0500 Subject: [PATCH 11/13] Update ddtrace/internal/settings/integration.py Co-authored-by: Munir Abdinur --- ddtrace/internal/settings/integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/internal/settings/integration.py b/ddtrace/internal/settings/integration.py index 1d8521fb212..6e075051463 100644 --- a/ddtrace/internal/settings/integration.py +++ b/ddtrace/internal/settings/integration.py @@ -182,7 +182,7 @@ def emit(self, hook, *args, **kwargs): or ``get_current_root_span()``. """ deprecate( - "Hooks.emit() is deprecated and is currently a no-op.", + "Hooks.emit() is deprecated", message="To interact with spans, use get_current_span() or get_current_root_span().", removal_version="5.0.0", category=DDTraceDeprecationWarning, From 4bccb7f7b992087f9ec35a0204fc6e68ce4b3ecc Mon Sep 17 00:00:00 2001 From: Mohammad A Islam <73513662+chojomok@users.noreply.github.com> Date: Fri, 12 Dec 2025 16:28:55 -0500 Subject: [PATCH 12/13] Update ddtrace/internal/settings/integration.py Co-authored-by: Munir Abdinur --- ddtrace/internal/settings/integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ddtrace/internal/settings/integration.py b/ddtrace/internal/settings/integration.py index 6e075051463..080d4f11a4c 100644 --- a/ddtrace/internal/settings/integration.py +++ b/ddtrace/internal/settings/integration.py @@ -183,7 +183,7 @@ def emit(self, hook, *args, **kwargs): """ deprecate( "Hooks.emit() is deprecated", - message="To interact with spans, use get_current_span() or get_current_root_span().", + message="Use tracer.current_span() or TraceFilters to retrieve and/or modify spans", removal_version="5.0.0", category=DDTraceDeprecationWarning, ) From b5f89eb641a63299a8d7b86c1c6e43ea96560b2b Mon Sep 17 00:00:00 2001 From: Mohammad Islam Date: Fri, 12 Dec 2025 16:56:39 -0500 Subject: [PATCH 13/13] pr review changes --- ddtrace/internal/settings/integration.py | 39 +---------- tests/tracer/test_hooks.py | 83 ++++++++++++++---------- 2 files changed, 50 insertions(+), 72 deletions(-) diff --git a/ddtrace/internal/settings/integration.py b/ddtrace/internal/settings/integration.py index 080d4f11a4c..f5ea0a93f43 100644 --- a/ddtrace/internal/settings/integration.py +++ b/ddtrace/internal/settings/integration.py @@ -119,29 +119,9 @@ def copy(self): class Hooks: - """ - Deprecated no-op Hooks class for backwards compatibility. - - This class previously provided hook registration and invocation functionality, - but has been deprecated in favor of using the public tracing API. - - All methods are now no-ops to maintain backwards compatibility with existing code. - - .. deprecated:: 4.0 - The Hooks class is deprecated and will be removed in v5.0. - To interact with spans, use ``get_current_span()`` or ``get_current_root_span()``. - """ - - __slots__ = () + """Deprecated no-op Hooks class for backwards compatibility.""" def register(self, hook, func=None): - """No-op: Hook registration is deprecated. - - .. deprecated:: 4.0 - Hook registration via ``config..hooks.register()`` is deprecated - and will be removed in v5.0. To interact with spans, use ``get_current_span()`` - or ``get_current_root_span()``. - """ deprecate( "Hooks.register() is deprecated and is currently a no-op.", message="To interact with spans, use get_current_span() or get_current_root_span().", @@ -156,16 +136,10 @@ def wrapper(func): return wrapper return None - def on(self): - return self.register() + def on(self, hook, func=None): + return self.register(hook, func) def deregister(self, hook, func): - """No-op: Hook deregistration is deprecated. - - .. deprecated:: 4.0 - Hook deregistration via ``config..hooks.deregister()`` is deprecated - and will be removed in v5.0. - """ deprecate( "Hooks.deregister() is deprecated and is currently a no-op.", removal_version="5.0.0", @@ -174,13 +148,6 @@ def deregister(self, hook, func): pass def emit(self, hook, *args, **kwargs): - """No-op: Hook emission is deprecated. - - .. deprecated:: 4.0 - Hook emission via ``config..hooks.emit()`` is deprecated - and will be removed in v5.0. To interact with spans, use ``get_current_span()`` - or ``get_current_root_span()``. - """ deprecate( "Hooks.emit() is deprecated", message="Use tracer.current_span() or TraceFilters to retrieve and/or modify spans", diff --git a/tests/tracer/test_hooks.py b/tests/tracer/test_hooks.py index ff50c529754..882c922a227 100644 --- a/tests/tracer/test_hooks.py +++ b/tests/tracer/test_hooks.py @@ -1,68 +1,79 @@ from unittest import TestCase - -import pytest +import warnings from ddtrace.internal.settings._config import Config from ddtrace.internal.settings.integration import IntegrationConfig class HooksDeprecationTestCase(TestCase): - """Test that Hooks class methods raise deprecation warnings""" - def setUp(self): self.config = Config() self.config.web = IntegrationConfig(self.config, "web") def test_hooks_register_deprecation(self): - """ - When calling `config.hooks.register()` - A deprecation warning is raised - """ - with pytest.warns(match="Hooks.register\\(\\) is deprecated and is currently a no-op"): - self.config.web.hooks.register("test_hook", lambda: None) + warnings.simplefilter("always") + with warnings.catch_warnings(record=True) as warns: + self.config.web.hooks.register("test_hook") + assert len(warns) == 1 + assert ( + "Hooks.register() is deprecated and is currently a no-op. and will be removed in version '5.0.0': To interact with spans, use get_current_span() or get_current_root_span()." # noqa:E501 + in str(warns[0].message) + ) def test_hooks_on_deprecation(self): - """ - When calling `config.hooks.on()` (alias for register) - A deprecation warning is raised - """ - with pytest.warns(match="Hooks.register\\(\\) is deprecated and is currently a no-op"): - self.config.web.hooks.on("test_hook", lambda: None) + warnings.simplefilter("always") + with warnings.catch_warnings(record=True) as warns: + self.config.web.hooks.on("test_hook") + assert len(warns) == 1 + assert ( + "Hooks.register() is deprecated and is currently a no-op. and will be removed in version '5.0.0': To interact with spans, use get_current_span() or get_current_root_span()." # noqa:E501 + in str(warns[0].message) + ) def test_hooks_deregister_deprecation(self): - """ - When calling `config.hooks.deregister()` - A deprecation warning is raised - """ - with pytest.warns(match="Hooks.deregister\\(\\) is deprecated and is currently a no-op"): + warnings.simplefilter("always") + with warnings.catch_warnings(record=True) as warns: self.config.web.hooks.deregister("test_hook", lambda: None) + assert len(warns) == 1 + assert ( + "Hooks.deregister() is deprecated and is currently a no-op. and will be removed in version '5.0.0'" # noqa:E501 + in str(warns[0].message) + ) def test_hooks_emit_deprecation(self): - """ - When calling `config.hooks.emit()` - A deprecation warning is raised - """ - with pytest.warns(match="Hooks.emit\\(\\) is deprecated and is currently a no-op"): + warnings.simplefilter("always") + with warnings.catch_warnings(record=True) as warns: self.config.web.hooks.emit("test_hook") + assert len(warns) == 1 + assert ( + "Hooks.emit() is deprecated and is currently a no-op. and will be removed in version '5.0.0': To interact with spans, use get_current_span() or get_current_root_span()." # noqa:E501 + in str(warns[0].message) + ) def test_hooks_register_decorator_deprecation(self): - """ - When calling `config.hooks.register()` as a decorator - A deprecation warning is raised - """ - with pytest.warns(match="Hooks.register\\(\\) is deprecated and is currently a no-op"): + warnings.simplefilter("always") + with warnings.catch_warnings(record=True) as warns: @self.config.web.hooks.register("test_hook") def my_hook(): pass + assert len(warns) == 1 + assert ( + "Hooks.register() is deprecated and is currently a no-op. and will be removed in version '5.0.0': To interact with spans, use get_current_span() or get_current_root_span()." # noqa:E501 + in str(warns[0].message) + ) + def test_hooks_on_decorator_deprecation(self): - """ - When calling `config.hooks.on()` as a decorator - A deprecation warning is raised - """ - with pytest.warns(match="Hooks.register\\(\\) is deprecated and is currently a no-op"): + warnings.simplefilter("always") + with warnings.catch_warnings(record=True) as warns: @self.config.web.hooks.on("test_hook") def my_hook(): pass + + assert len(warns) == 1 + assert ( + "Hooks.register() is deprecated and is currently a no-op. and will be removed in version '5.0.0': To interact with spans, use get_current_span() or get_current_root_span()." # noqa:E501 + in str(warns[0].message) + )