Logging user access with Django signals

David
4 min readJul 5, 2020

For multiple reasons, including security and historical tracking, it can be advantageous to record use login/logout activity for a service. In this post we’ll look at using signals to log such activity in a Django project.

Getting Started

To get started, let’s first understand logging configuration in Django. As a matter of best practice, it’s a good idea to setup multiple log files for specific activities. To do this, let’s add some logging configuration in the settings.py file. We’ll define a base log path, some formatting parameters, and set our loggers and handlers.

I like to set the log path as a variable, so let’s create the following:

LOG_PATH = ‘/var/log/my_service’

This path can be whatever you like and all log files will be created here. Just be sure you have proper permissions to write to this location.

In Django, Logging configuration resides in the LOGGING dictionary. Let’s set this variable and add the ‘formatters’ key. Feel free to adjust the format as you see fit; this is just a string that sets how log entries are rendered.

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': "[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)s] %(message)s",
'datefmt': "%Y/%b/%d %H:%M:%S"
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
}

Now we need to define the handlers. The handler is the engine that determines what happens to each message in a logger. We’ll define a couple handlers here, one simply called ‘django’ which will be our catch all, and one called ‘user’ which will log user-specific activity. Our handlers will look like this:

'handlers': {
'django': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(str(LOG_PATH), 'django.log'),
'maxBytes': 1024 * 1024 * 10,
'backupCount': 10,
'formatter': 'verbose',
},
'user': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(str(LOG_PATH), 'user.log'),
'maxBytes': 1024 * 1024 * 10,
'backupCount': 10,
'formatter': 'verbose',
},
},

You’ll see a few parameters of interest here, such as level and maxBytes, etc. The level describes the severity of the messages that the logger will handle, maxBytes sets the size of the log before it rotates and the class, and the formatter is the formatter we created above.

Lastly, we’ll add the actual loggers. A logger is the entry point into the logging system. Each logger is a named bucket to which messages can be written for processing. We will create one for each handler.

'loggers': {
'django': {
'handlers': ['django', 'console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
},
'user': {
'handlers': ['user', 'console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
},
},

Our settings.py file should now have a complete logging section that looks like this:

LOG_PATH = '/var/log/my_service'
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': "[%(asctime)s] %(levelname)s [%(filename)s:%(lineno)s] %(message)s",
'datefmt': "%Y/%b/%d %H:%M:%S"
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'django': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(str(LOG_PATH), 'django.log'),
'maxBytes': (1024 * 1024 * 10),
'backupCount': 10,
'formatter': 'verbose',
},
'user': {
'level': 'INFO',
'class': 'logging.handlers.RotatingFileHandler',
'filename': os.path.join(str(LOG_PATH), 'user.log'),
'maxBytes': 1024 * 1024 * 10,
'backupCount': 10,
'formatter': 'verbose',
},
'loggers': {
'django': {
'handlers': ['django', 'console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'DEBUG'),
},
'user': {
'handlers': ['user', 'console'],
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
},
},
},
}

With our settings complete, we’re ready to create our signals. Let’s create a signals.py file in an app under the Django project. Django provides a few signals specifically for user login, logout, and login failed. These will be the first things we import in the signals.py file:

from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
from django.dispatch import receiver

We want to utilize the ‘user’ log we configured in the settings.py file. To do this import logging and set the logger to the ‘user’ log.

import logging
user_logger = logging.getLogger("user")

Now we create the actually signals. There will be one for each of the following three scenarios: successful user login, failed user login, and user logout, with the appropriate receiver set. Inside the signal we use the logger defined above. Our complete signals.py file will look like this:

from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
from django.dispatch import receiver
import logging
user_logger = logging.getLogger("user")
@receiver(user_logged_in)
def log_user_login(sender, user, **kwargs):
""" log user login to user log """
user_logger.info('%s login successful', user)


@receiver(user_login_failed)
def log_user_login_failed(sender, user=None, **kwargs):
""" log user login to user log """
if user:
user_logger.info('%s login failed', user)
else:
user_logger.error('login failed; unknown user')


@receiver(user_logged_out)
def log_user_logout(sender, user, **kwargs):
""" log user logout to user log """
user_logger.info('%s log out successful', user)

You can set your signals in the AppConfig using the ready function, or call your signals however you prefer. If using AppConfig, you’ll have an apps.py file that looks something like this:

from django.apps import AppConfig


class MyAppConfig(AppConfig):
name = 'myapp'

def ready(self):
import myapp.signals

Your automatic user logging is complete. With this approach you’ll have a dedicated user.log that automatically tracks all login attempts (successful or not) and user logout events.

For more information on logging in django, see the official documentation here: https://docs.djangoproject.com/en/3.0/topics/logging/

For more information on django signals, see the official documentation here: https://docs.djangoproject.com/en/3.0/topics/signals/

--

--