Aiohttp logging: how to distinguish log messages of different requests?
Asked Answered
A

1

6

Imagine I have this web application based on Aiohttp:

from aiohttp import web
import asyncio
import logging

logger = logging.getLogger(__name__)

async def hello(request):
    logger.info('Started processing request')
    await asyncio.sleep(1)
    logger.info('Doing something')
    await asyncio.sleep(1)
    return web.Response(text="Hello, world!\n")

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s %(name)-14s %(levelname)s: %(message)s')

app = web.Application()
app.add_routes([web.get('/', hello)])
web.run_app(app)

Its output is (for example):

2019-11-11 13:37:14,757 __main__       INFO: Started processing request
2019-11-11 13:37:14,757 __main__       INFO: Started processing request
2019-11-11 13:37:15,761 __main__       INFO: Doing something
2019-11-11 13:37:15,761 __main__       INFO: Doing something
2019-11-11 13:37:16,765 aiohttp.access INFO: 127.0.0.1 [11/Nov/2019:12:37:14 +0000] "GET / HTTP/1.1" 200 165 "-" "curl/7.66.0"
2019-11-11 13:37:16,768 aiohttp.access INFO: 127.0.0.1 [11/Nov/2019:12:37:14 +0000] "GET / HTTP/1.1" 200 165 "-" "curl/7.66.0"

How do I know what log messages belong to what request?

I would like to see some "request id" in every log message (similar idea to the "correlation id" in microservices)...

Augustineaugustinian answered 11/11, 2019 at 12:51 Comment(0)
A
11

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

Augustineaugustinian answered 11/11, 2019 at 12:51 Comment(1)
Hi, in the first code block, is request['request_id'] = req_id valid? I think it'll throw a TypeError: 'ContextVar' object does not support item assignmentTympanites

© 2022 - 2024 — McMap. All rights reserved.