Skip to content

Commit

Permalink
Provide elapsed and timestamp info to filename_format
Browse files Browse the repository at this point in the history
This provides the `elapsed` and `time` values to the
`ProfileMiddleware(filename_format=...)` function. Prior to this change,
one could not replicate the format string format, much less modify how
it would render the timestamp or elapsed time values.

These new values can be found under the `werkzeug.profiler` key in the
WSGI environ dict passed into the `filename_format()` function.

Addresses #2775
  • Loading branch information
joshwilson-dbx authored and pgjones committed Sep 24, 2023
1 parent 599993d commit 4820d8c
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 3 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Expand Up @@ -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
Expand Down
19 changes: 16 additions & 3 deletions src/werkzeug/middleware/profiler.py
Expand Up @@ -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``.
Expand All @@ -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``.
Expand Down Expand Up @@ -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(
Expand Down
50 changes: 50 additions & 0 deletions 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))

0 comments on commit 4820d8c

Please sign in to comment.