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.
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)
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', ],
},
...
},
}
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 aFilter
to add, say, information from the request - say, the remote IP address and remote user’s username - to theLogRecord
...
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()