Skip to content

Commit 9a59cee

Browse files
committed
feature: Add request duration in nanoseconds
So that if you have other systems that record durations to this resolution then they can be in alignment without needing calculation.
1 parent a86ea1e commit 9a59cee

File tree

10 files changed

+28
-26
lines changed

10 files changed

+28
-26
lines changed

docs/source/settings.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ a user agent
215215
T request time in seconds
216216
M request time in milliseconds
217217
D request time in microseconds
218+
N request time in nanoseconds
218219
L request time in decimal seconds
219220
p process ID
220221
{header}i request header
@@ -1148,7 +1149,7 @@ change the worker process user.
11481149
Switch worker process to run as this group.
11491150

11501151
A valid group id (as an integer) or the name of a user that can be
1151-
retrieved with a call to ``pwd.getgrnam(value)`` or ``None`` to not
1152+
retrieved with a call to ``grp.getgrnam(value)`` or ``None`` to not
11521153
change the worker processes group.
11531154

11541155
.. _umask:

gunicorn/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,7 @@ class AccessLogFormat(Setting):
14171417
T request time in seconds
14181418
M request time in milliseconds
14191419
D request time in microseconds
1420+
N request time in nanoseconds
14201421
L request time in decimal seconds
14211422
p process ID
14221423
{header}i request header

gunicorn/glogging.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,11 @@ def atoms(self, resp, req, environ, request_time):
310310
'B': getattr(resp, 'sent', None),
311311
'f': environ.get('HTTP_REFERER', '-'),
312312
'a': environ.get('HTTP_USER_AGENT', '-'),
313-
'T': request_time.seconds,
314-
'D': (request_time.seconds * 1000000) + request_time.microseconds,
315-
'M': (request_time.seconds * 1000) + int(request_time.microseconds / 1000),
316-
'L': "%d.%06d" % (request_time.seconds, request_time.microseconds),
313+
'T': request_time // 1_000_000_000,
314+
'M': request_time // 1_000_000,
315+
'D': request_time // 1_000,
316+
'N': request_time,
317+
'L': "%.6f" % (request_time / 1e+9),
317318
'p': "<%s>" % os.getpid()
318319
}
319320

gunicorn/instrument/statsd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def access(self, resp, req, environ, request_time):
9595
request_time is a datetime.timedelta
9696
"""
9797
Logger.access(self, resp, req, environ, request_time)
98-
duration_in_ms = request_time.seconds * 1000 + float(request_time.microseconds) / 10 ** 3
98+
duration_in_ms = request_time // 1_000_000
9999
status = resp.status
100100
if isinstance(status, bytes):
101101
status = status.decode('utf-8')

gunicorn/workers/base.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import sys
99
import time
1010
import traceback
11-
from datetime import datetime
1211
from random import randint
1312
from ssl import SSLError
1413

@@ -204,7 +203,7 @@ def handle_abort(self, sig, frame):
204203
sys.exit(1)
205204

206205
def handle_error(self, req, client, addr, exc):
207-
request_start = datetime.now()
206+
request_start = time.monotonic_ns()
208207
addr = addr or ('', -1) # unix socket case
209208
if isinstance(exc, (
210209
InvalidRequestLine, InvalidRequestMethod,
@@ -268,7 +267,7 @@ def handle_error(self, req, client, addr, exc):
268267
mesg = ""
269268

270269
if req is not None:
271-
request_time = datetime.now() - request_start
270+
request_time = time.monotonic_ns() - request_start
272271
environ = default_environ(req, client, self.cfg)
273272
environ['REMOTE_ADDR'] = addr[0]
274273
environ['REMOTE_PORT'] = str(addr[1])

gunicorn/workers/base_async.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
# This file is part of gunicorn released under the MIT license.
33
# See the NOTICE for more information.
44

5-
from datetime import datetime
65
import errno
76
import socket
87
import ssl
98
import sys
9+
import time
1010

1111
from gunicorn import http
1212
from gunicorn.http import wsgi
@@ -87,7 +87,7 @@ def handle(self, listener, client, addr):
8787
util.close(client)
8888

8989
def handle_request(self, listener_name, req, sock, addr):
90-
request_start = datetime.now()
90+
request_start = time.monotonic_ns()
9191
environ = {}
9292
resp = None
9393
try:
@@ -115,7 +115,7 @@ def handle_request(self, listener_name, req, sock, addr):
115115
resp.write(item)
116116
resp.close()
117117
finally:
118-
request_time = datetime.now() - request_start
118+
request_time = time.monotonic_ns() - request_start
119119
self.log.access(resp, req, environ, request_time)
120120
if hasattr(respiter, "close"):
121121
respiter.close()

gunicorn/workers/gthread.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import sys
2020
import time
2121
from collections import deque
22-
from datetime import datetime
2322
from functools import partial
2423
from threading import RLock
2524

@@ -314,7 +313,7 @@ def handle_request(self, req, conn):
314313
resp = None
315314
try:
316315
self.cfg.pre_request(self, req)
317-
request_start = datetime.now()
316+
request_start = time.monotonic_ns()
318317
resp, environ = wsgi.create(req, conn.sock, conn.client,
319318
conn.server, self.cfg)
320319
environ["wsgi.multithread"] = True
@@ -340,7 +339,7 @@ def handle_request(self, req, conn):
340339

341340
resp.close()
342341
finally:
343-
request_time = datetime.now() - request_start
342+
request_time = time.monotonic_ns() - request_start
344343
self.log.access(resp, req, environ, request_time)
345344
if hasattr(respiter, "close"):
346345
respiter.close()

gunicorn/workers/sync.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
# See the NOTICE for more information.
44
#
55

6-
from datetime import datetime
76
import errno
87
import os
98
import select
109
import socket
1110
import ssl
1211
import sys
12+
import time
1313

1414
from gunicorn import http
1515
from gunicorn.http import wsgi
@@ -163,7 +163,7 @@ def handle_request(self, listener, req, client, addr):
163163
resp = None
164164
try:
165165
self.cfg.pre_request(self, req)
166-
request_start = datetime.now()
166+
request_start = time.monotonic_ns()
167167
resp, environ = wsgi.create(req, client, addr,
168168
listener.getsockname(), self.cfg)
169169
# Force the connection closed until someone shows
@@ -183,7 +183,7 @@ def handle_request(self, listener, req, client, addr):
183183
resp.write(item)
184184
resp.close()
185185
finally:
186-
request_time = datetime.now() - request_start
186+
request_time = time.monotonic_ns() - request_start
187187
self.log.access(resp, req, environ, request_time)
188188
if hasattr(respiter, "close"):
189189
respiter.close()

tests/test_logger.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from gunicorn.glogging import Logger
88

99

10+
ONE_SECOND_IN_NS = 1_000_000_000
11+
1012
def test_atoms_defaults():
1113
response = SimpleNamespace(
1214
status='200', response_length=1024,
@@ -19,7 +21,7 @@ def test_atoms_defaults():
1921
'SERVER_PROTOCOL': 'HTTP/1.1',
2022
}
2123
logger = Logger(Config())
22-
atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1))
24+
atoms = logger.atoms(response, request, environ, ONE_SECOND_IN_NS)
2325
assert isinstance(atoms, dict)
2426
assert atoms['r'] == 'GET /my/path?foo=bar HTTP/1.1'
2527
assert atoms['m'] == 'GET'
@@ -30,7 +32,7 @@ def test_atoms_defaults():
3032
assert atoms['B'] == 1024
3133
assert atoms['{accept}i'] == 'application/json'
3234
assert atoms['{content-type}o'] == 'application/json'
33-
35+
assert atoms['N'] == ONE_SECOND_IN_NS
3436

3537
def test_atoms_zero_bytes():
3638
response = SimpleNamespace(
@@ -44,7 +46,7 @@ def test_atoms_zero_bytes():
4446
'SERVER_PROTOCOL': 'HTTP/1.1',
4547
}
4648
logger = Logger(Config())
47-
atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1))
49+
atoms = logger.atoms(response, request, environ, ONE_SECOND_IN_NS)
4850
assert atoms['b'] == '0'
4951
assert atoms['B'] == 0
5052

@@ -68,7 +70,7 @@ def test_get_username_from_basic_auth_header(auth):
6870
'HTTP_AUTHORIZATION': auth,
6971
}
7072
logger = Logger(Config())
71-
atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1))
73+
atoms = logger.atoms(response, request, environ, ONE_SECOND_IN_NS)
7274
assert atoms['u'] == 'brk0v'
7375

7476

@@ -87,5 +89,5 @@ def test_get_username_handles_malformed_basic_auth_header():
8789
}
8890
logger = Logger(Config())
8991

90-
atoms = logger.atoms(response, request, environ, datetime.timedelta(seconds=1))
92+
atoms = logger.atoms(response, request, environ, ONE_SECOND_IN_NS)
9193
assert atoms['u'] == '-'

tests/test_statsd.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import shutil
55
import socket
66
import tempfile
7-
from datetime import timedelta
87
from types import SimpleNamespace
98

109
from gunicorn.config import Config
@@ -111,8 +110,8 @@ def test_instrument():
111110
assert logger.sock.msgs[0] == b"gunicorn.log.critical:1|c|@1.0"
112111
logger.sock.reset()
113112

114-
logger.access(SimpleNamespace(status="200 OK"), None, {}, timedelta(seconds=7))
115-
assert logger.sock.msgs[0] == b"gunicorn.request.duration:7000.0|ms"
113+
logger.access(SimpleNamespace(status="200 OK"), None, {}, 7000000000)
114+
assert logger.sock.msgs[0] == b"gunicorn.request.duration:7000|ms"
116115
assert logger.sock.msgs[1] == b"gunicorn.requests:1|c|@1.0"
117116
assert logger.sock.msgs[2] == b"gunicorn.request.status.200:1|c|@1.0"
118117

0 commit comments

Comments
 (0)