Search code examples
pythondjangodjango-admin

How can I add the currently logged in username to the access log of django.server?


I'm trying to add the currently logged in username, if any, to the access log of a Django app:

INFO [django.server:161] "GET / HTTP/1.1" 200 116181
                        ^ username should go here

My main problem is how do I share the user/username between the middleware and the filter, since in the filter I don't have access to the request object?

I've got a working solution using thread-local for storage, but this doesn't seem like a good idea.

Especially since I can't cleanup the value in process_request as it is then cleared too early, before the log line is printed.

Solution with threading.local()

log.py

import logging
import threading

local = threading.local()


class LoggedInUsernameFilter(logging.Filter):
    def filter(self, record):
        user = getattr(local, 'user', None)
        if user and user.username:
            record.username = user.username
        else:
            record.username = '-'
        return True


class LoggedInUserMiddleware(object):

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        self.process_request(request)
        return self.get_response(request)

    def process_request(self, request):
        from django.contrib.auth import get_user
        user = get_user(request)
        setattr(local, 'user', user)

django settings

MIDDLEWARE = (
    ...
    'commons.log.LoggedInUserMiddleware',
    ...
)

...

LOGGING = {
    'version': 1,
    'disable_existing_loggers': True,
    'formatters': {
        ...
        'verbose_with_username': {
            'format': '[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(username)s %(message)s'
        }
    },
    'filters': {
        'logged_in_username': {
            '()': 'commons.log.LoggedInUsernameFilter',
        }
    },
    'handlers': {
        'console_with_username': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'verbose_with_username',
            'stream': sys.stdout,
        },
    },
    'loggers': {
        ...
        'django.server': {
            'handlers': ['console_with_username'],
            'level': 'INFO',
            'propagate': False,
            'formatter': 'verbose_with_username',
            'filters': ['logged_in_username', ],
        },
        ...
    },
}

Solution

  • I've got a working solution using thread-local for storage, but this doesn't seem like a good idea.

    Some answers do state it is "extremely useful" or an "excellent method", with an "async" caveat.

    Even the authoritative Python howto https://docs.python.org/3/howto/logging-cookbook.html#using-filters-to-impart-contextual-information suggests thread-local storage:

    For example in a web application, the request being processed (or at least, the interesting parts of it) can be stored in a threadlocal (threading.local) variable, and then accessed from a Filter to add, say, information from the request - say, the remote IP address and remote user’s username - to the LogRecord ...

    It also suggests Using LoggerAdapters and Use of contextvars, which are not suitable for django.server logging that is out of your control.

    That said, asgiref.local.Local is a good drop-in replacement for the "async" caveat.

    # import threading
    # local = threading.local()
    from asgiref.local import Local
    local = Local()