bobbui/json-logging-python

Unable to get request body in custom request formatter when using FastAPI

vstrimaitis opened this issue · 6 comments

I'm interested in getting the request body in a custom request formatter when using FastAPI. However, request.body() returns a coroutine and therefore requires the await keyword, but the _format_log_object method is not async. My custom formatter looks something like this:

class _RequestFormatter(json_logging.JSONRequestLogFormatter):
    def _format_log_object(
        self, record, request_util: json_logging.util.RequestUtil
    ):
        request: Request = record.request_response_data._request

        request_body_bytes = await request.body()  # <--- can't do this!
        request_body = bytes.decode(request_body_bytes)

        log_obj = super()._format_log_object(record, request_util)
        log_obj.update({"request_body": request_body})
        return log_obj

Such an approach works in frameworks like Flask, where you don't have to use await to get the body, but fails for FastAPI. Do you have any suggestions on how to work around this?

i would suggest u extend logging.Formatter for maximum flexibility, something like that

class BaseJSONFormatter(logging.Formatter):

Yes, I'm aware of them (and I've used them before with Flask). But the methods inside of this class (format and _format_log_object) are not async, so I can't write something like request_body = await request.body(), which is the only way to get the request body in FastAPI.

@vstrimaitis is not really familiar with async await syntax. But will do some experiment once have time

@vstrimaitis @bobbui I have the same issue. Have you found a solution?

My solution:

with this patch I can execute:

from json_logging.dto import DefaultRequestResponseDTO

...

class async_iterator_wrapper:
    """Class to create async iterator"""

    def __init__(self, obj):
        self._it = iter(obj)

    def __aiter__(self):
        return self

    async def __anext__(self):
        try:
            value = next(self._it)
        except StopIteration:
            raise StopAsyncIteration
        return value


class CustomRequestResponseDTO(DefaultRequestResponseDTO):
    async def async_on_request_complete(self, response):
        super(CustomRequestResponseDTO, self).on_request_complete(response)
        self['request'] = '{} {}'.format(self._request.method, self._request.url)
        resp_body = [section async for section in self._response.body_iterator]
        self['response_body'] = b'\n'.join(resp_body).decode()
        self._response.body_iterator = async_iterator_wrapper(resp_body)

Hi @stephane-klein! I ended up making a custom middleware class which extends BaseHTTPMiddleware and monkeypatched json_logging.framework.fastapi.implementation.JSONLoggingASGIMiddleware to point to this new class. The actual implementation of the class is exactly like JSONLoggingASGIMiddleware except it additionally extracts the request body from the request object and makes it available as an additional entry in the extra dict. This allowed me to then define the custom request formatter class in a standard way and just extract the request body from the record object.

This is obviously not a clean solution in any sense, but it ended up working for me back when I needed it and the implementation has stayed the same since then 😅