In "classic" non-async web apps it's simple - one process (or thread) processes only one request at a time, so you just log process/thread id (logging format: %(process)d %(thread)d
).
In async (asyncio) programs there are usually multiple different things running in an event loop in a single thread (in web app: different requests being processed), so logging process/thread id is not enough. You need to somehow identify not an operating system thread, but a "thread" of related asyncio tasks - that's what ContextVar is for.
Step 1: create contextvar
request_id = ContextVar('request_id')
Step 2: set this contextvar value for each request
@web.middleware
async def add_request_id_middleware(request, handler):
'''
Aiohttp middleware that sets request_id contextvar and request['request_id']
to some random value identifying the given request.
'''
req_id = secrets.token_urlsafe(5).replace('_', 'x').replace('-', 'X')
request['request_id'] = req_id
token = request_id.set(req_id)
try:
return await handler(request)
finally:
request_id.reset(token)
app = web.Application(middlewares=[add_request_id_middleware])
Step 3: insert this contextvar value in every log message automatically
def setup_log_record_factory():
'''
Wrap logging request factory so that [{request_id}] is prepended to each message
'''
old_factory = logging.getLogRecordFactory()
def new_factory(*args, **kwargs):
record = old_factory(*args, **kwargs)
req_id = request_id.get(None)
if req_id:
record.msg = f'[{req_id}] {record.msg}'
return record
logging.setLogRecordFactory(new_factory)
setup_log_record_factory()
Step 4: since aiohttp request access log message is logged outside the scope where we set the context var, we need to define our own AccessLogger that fixes this:
from aiohttp.web_log import AccessLogger
class CustomAccessLogger (AccessLogger):
def log(self, request, response, time):
token = request_id.set(request['request_id'])
try:
super().log(request, response, time)
finally:
request_id.reset(token)
web.run_app(app, access_log_class=CustomAccessLogger)
Done 🎉 Example output:
2019-11-11 13:49:34,167 __main__ INFO: [cNniXu8] Started processing request
2019-11-11 13:49:34,168 __main__ INFO: [oWzMYds] Started processing request
2019-11-11 13:49:35,169 __main__ INFO: [cNniXu8] Doing something
2019-11-11 13:49:35,169 __main__ INFO: [oWzMYds] Doing something
2019-11-11 13:49:36,172 aiohttp.access INFO: [cNniXu8] 127.0.0.1 [11/Nov/2019:12:49:34 +0000] "GET / HTTP/1.1" 200 165 "-" "curl/7.66.0"
2019-11-11 13:49:36,174 aiohttp.access INFO: [oWzMYds] 127.0.0.1 [11/Nov/2019:12:49:34 +0000] "GET / HTTP/1.1" 200 165 "-" "curl/7.66.0"
Full source code is here: gist.github.com/messa/c538fc267550ec67a1fed244183dcf1e
Update: I have created a library for this :) github.com/messa/aiohttp-request-id-logging
request['request_id'] = req_id
valid? I think it'll throw aTypeError: 'ContextVar' object does not support item assignment
– Tympanites