Skip to content

Commit 4820d8c

Browse files
joshwilson-dbxpgjones
authored andcommitted
Provide elapsed and timestamp info to filename_format
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
1 parent 599993d commit 4820d8c

File tree

3 files changed

+69
-3
lines changed

3 files changed

+69
-3
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ Unreleased
99
- Deprecate the ``__version__`` attribute. Use feature detection, or
1010
``importlib.metadata.version("werkzeug")``, instead. :issue:`2770`
1111
- ``generate_password_hash`` uses scrypt by default. :issue:`2769`
12+
- Add the ``"werkzeug.profiler"`` item to the WSGI ``environ`` dictionary
13+
passed to `ProfilerMiddleware`'s `filename_format` function. It contains
14+
the ``elapsed`` and ``time`` values for the profiled request. :issue:`2775`
1215

1316

1417
Version 2.3.8

src/werkzeug/middleware/profiler.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,16 @@ class ProfilerMiddleware:
4444
4545
- ``{method}`` - The request method; GET, POST, etc.
4646
- ``{path}`` - The request path or 'root' should one not exist.
47-
- ``{elapsed}`` - The elapsed time of the request.
47+
- ``{elapsed}`` - The elapsed time of the request in milliseconds.
4848
- ``{time}`` - The time of the request.
4949
50-
If it is a callable, it will be called with the WSGI ``environ``
51-
dict and should return a filename.
50+
If it is a callable, it will be called with the WSGI ``environ`` and
51+
be expected to return a filename string. The ``environ`` dictionary
52+
will also have the ``"werkzeug.profiler"`` key populated with a
53+
dictionary containing the following fields (more may be added in the
54+
future):
55+
- ``{elapsed}`` - The elapsed time of the request in milliseconds.
56+
- ``{time}`` - The time of the request.
5257
5358
:param app: The WSGI application to wrap.
5459
:param stream: Write stats to this stream. Disable with ``None``.
@@ -65,6 +70,10 @@ class ProfilerMiddleware:
6570
from werkzeug.middleware.profiler import ProfilerMiddleware
6671
app = ProfilerMiddleware(app)
6772
73+
.. versionchanged:: 3.0
74+
Added the ``"werkzeug.profiler"`` key to the ``filename_format(environ)``
75+
parameter with the ``elapsed`` and ``time`` fields.
76+
6877
.. versionchanged:: 0.15
6978
Stats are written even if ``profile_dir`` is given, and can be
7079
disable by passing ``stream=None``.
@@ -118,6 +127,10 @@ def runapp() -> None:
118127

119128
if self._profile_dir is not None:
120129
if callable(self._filename_format):
130+
environ["werkzeug.profiler"] = {
131+
"elapsed": elapsed * 1000.0,
132+
"time": time.time(),
133+
}
121134
filename = self._filename_format(environ)
122135
else:
123136
filename = self._filename_format.format(

tests/middleware/test_profiler.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import datetime
2+
import os
3+
from unittest.mock import ANY
4+
from unittest.mock import MagicMock
5+
from unittest.mock import patch
6+
7+
from werkzeug.middleware.profiler import Profile
8+
from werkzeug.middleware.profiler import ProfilerMiddleware
9+
from werkzeug.test import Client
10+
11+
12+
def dummy_application(environ, start_response):
13+
start_response("200 OK", [("Content-Type", "text/plain")])
14+
return [b"Foo"]
15+
16+
17+
def test_filename_format_function():
18+
# This should be called once with the generated file name
19+
mock_capture_name = MagicMock()
20+
21+
def filename_format(env):
22+
now = datetime.datetime.fromtimestamp(env["werkzeug.profiler"]["time"])
23+
timestamp = now.strftime("%Y-%m-%d:%H:%M:%S")
24+
path = (
25+
"_".join(token for token in env["PATH_INFO"].split("/") if token) or "ROOT"
26+
)
27+
elapsed = env["werkzeug.profiler"]["elapsed"]
28+
name = f"{timestamp}.{env['REQUEST_METHOD']}.{path}.{elapsed:.0f}ms.prof"
29+
mock_capture_name(name=name)
30+
return name
31+
32+
client = Client(
33+
ProfilerMiddleware(
34+
dummy_application,
35+
stream=None,
36+
profile_dir="profiles",
37+
filename_format=filename_format,
38+
)
39+
)
40+
41+
# Replace the Profile class with a function that simulates an __init__()
42+
# call and returns our mock instance.
43+
mock_profile = MagicMock(wraps=Profile())
44+
mock_profile.dump_stats = MagicMock()
45+
with patch("werkzeug.middleware.profiler.Profile", lambda: mock_profile):
46+
client.get("/foo/bar")
47+
48+
mock_capture_name.assert_called_once_with(name=ANY)
49+
name = mock_capture_name.mock_calls[0].kwargs["name"]
50+
mock_profile.dump_stats.assert_called_once_with(os.path.join("profiles", name))

0 commit comments

Comments
 (0)