diff --git a/CHANGES.rst b/CHANGES.rst index ec2ad5d51..f9c997dd8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,9 @@ Unreleased - Deprecate the ``__version__`` attribute. Use feature detection, or ``importlib.metadata.version("werkzeug")``, instead. :issue:`2770` - ``generate_password_hash`` uses scrypt by default. :issue:`2769` +- Add the ``"werkzeug.profiler"`` item to the WSGI ``environ`` dictionary + passed to `ProfilerMiddleware`'s `filename_format` function. It contains + the ``elapsed`` and ``time`` values for the profiled request. :issue:`2775` Version 2.3.8 diff --git a/src/werkzeug/middleware/profiler.py b/src/werkzeug/middleware/profiler.py index 2d806154c..1120c83ef 100644 --- a/src/werkzeug/middleware/profiler.py +++ b/src/werkzeug/middleware/profiler.py @@ -44,11 +44,16 @@ class ProfilerMiddleware: - ``{method}`` - The request method; GET, POST, etc. - ``{path}`` - The request path or 'root' should one not exist. - - ``{elapsed}`` - The elapsed time of the request. + - ``{elapsed}`` - The elapsed time of the request in milliseconds. - ``{time}`` - The time of the request. - If it is a callable, it will be called with the WSGI ``environ`` - dict and should return a filename. + If it is a callable, it will be called with the WSGI ``environ`` and + be expected to return a filename string. The ``environ`` dictionary + will also have the ``"werkzeug.profiler"`` key populated with a + dictionary containing the following fields (more may be added in the + future): + - ``{elapsed}`` - The elapsed time of the request in milliseconds. + - ``{time}`` - The time of the request. :param app: The WSGI application to wrap. :param stream: Write stats to this stream. Disable with ``None``. @@ -65,6 +70,10 @@ class ProfilerMiddleware: from werkzeug.middleware.profiler import ProfilerMiddleware app = ProfilerMiddleware(app) + .. versionchanged:: 3.0 + Added the ``"werkzeug.profiler"`` key to the ``filename_format(environ)`` + parameter with the ``elapsed`` and ``time`` fields. + .. versionchanged:: 0.15 Stats are written even if ``profile_dir`` is given, and can be disable by passing ``stream=None``. @@ -118,6 +127,10 @@ def runapp() -> None: if self._profile_dir is not None: if callable(self._filename_format): + environ["werkzeug.profiler"] = { + "elapsed": elapsed * 1000.0, + "time": time.time(), + } filename = self._filename_format(environ) else: filename = self._filename_format.format( diff --git a/tests/middleware/test_profiler.py b/tests/middleware/test_profiler.py new file mode 100644 index 000000000..585aeb54b --- /dev/null +++ b/tests/middleware/test_profiler.py @@ -0,0 +1,50 @@ +import datetime +import os +from unittest.mock import ANY +from unittest.mock import MagicMock +from unittest.mock import patch + +from werkzeug.middleware.profiler import Profile +from werkzeug.middleware.profiler import ProfilerMiddleware +from werkzeug.test import Client + + +def dummy_application(environ, start_response): + start_response("200 OK", [("Content-Type", "text/plain")]) + return [b"Foo"] + + +def test_filename_format_function(): + # This should be called once with the generated file name + mock_capture_name = MagicMock() + + def filename_format(env): + now = datetime.datetime.fromtimestamp(env["werkzeug.profiler"]["time"]) + timestamp = now.strftime("%Y-%m-%d:%H:%M:%S") + path = ( + "_".join(token for token in env["PATH_INFO"].split("/") if token) or "ROOT" + ) + elapsed = env["werkzeug.profiler"]["elapsed"] + name = f"{timestamp}.{env['REQUEST_METHOD']}.{path}.{elapsed:.0f}ms.prof" + mock_capture_name(name=name) + return name + + client = Client( + ProfilerMiddleware( + dummy_application, + stream=None, + profile_dir="profiles", + filename_format=filename_format, + ) + ) + + # Replace the Profile class with a function that simulates an __init__() + # call and returns our mock instance. + mock_profile = MagicMock(wraps=Profile()) + mock_profile.dump_stats = MagicMock() + with patch("werkzeug.middleware.profiler.Profile", lambda: mock_profile): + client.get("/foo/bar") + + mock_capture_name.assert_called_once_with(name=ANY) + name = mock_capture_name.mock_calls[0].kwargs["name"] + mock_profile.dump_stats.assert_called_once_with(os.path.join("profiles", name))