Skip to content

Instantly share code, notes, and snippets.

@mebaysan
Last active January 6, 2024 08:22
Show Gist options
  • Save mebaysan/12516b84927c8c9f2ea268d987347e30 to your computer and use it in GitHub Desktop.
Save mebaysan/12516b84927c8c9f2ea268d987347e30 to your computer and use it in GitHub Desktop.
Django Custom User Event Logging

Custom Event Logging

The Custom Event Logging system is designed to bolster observability, accountability, and security within your application. It offers a structured approach to log diverse user activities and system events. By leveraging this logging mechanism, you can systematically track and monitor user interactions, system operations, and security-related events in the database.

This system comprises models like EventLog and UserLog, alongside essential helper functions such as log_user. These components collectively enable the creation of detailed logs tailored to different system functionalities. The approach utilizes Django's robust framework to categorize and store logs efficiently, enhancing the application's visibility and traceability.

image

Models

from datetime import datetime
from django.db import models
from django.contrib.contenttypes.models import ContentType

class EventLog(models.Model):
    """
    This model is used to create log categories of the events that are happening in the system.
    """

    name = models.CharField(max_length=255)
    content_type = models.ForeignKey(
        ContentType,
        models.CASCADE,
        verbose_name="content type",
    )
    codename = models.CharField(max_length=100)

    class Meta:
        verbose_name = "Event Log"
        verbose_name_plural = "Event Logs"

    def __str__(self):
        return self.codename

    @classmethod
    def get_or_create_event_log(cls, name, content_type, codename):
        """
        If there is no event log with the given name, content type and codename, it will create one.
        """
        fetched_content_type = cls.get_content_type(content_type)
        event_log = EventLog.objects.filter(
            name=name, content_type=fetched_content_type, codename=codename
        ).first()
        if event_log is None:
            event_log = EventLog.objects.create(
                name=name, content_type=fetched_content_type, codename=codename
            )
        return event_log

    @classmethod
    def get_content_type(cls, content_type):
        """
        Get content type from a given string by using content_type map for models in the system.
        """
        importer = DynamicImporter()
        content_type_map = {
            "user": importer.import_class("apps.management.models", "User"),
            "notification": importer.import_class(
                "apps.management.models", "Notification"
            ),
            "permission": importer.import_class(
                "django.contrib.auth.models", "Permission"
            ),
            "event_log": importer.import_class("apps.management.models", "EventLog"),
            "user_log": importer.import_class("apps.management.models", "UserLog"),
            # your other models (entities)
        }

        if content_type not in content_type_map:
            logger.error(
                f"Content type {content_type} is not found in content type map!"
            )
            raise AttributeError(
                f"Content type {content_type} is not found in content type map!"
            )
        return ContentType.objects.get_for_model(content_type_map[content_type])


class UserLog(models.Model):
    """
    This model is used to create logs for the events that are happening in the system.
    """

    created_at = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(
        User, on_delete=models.SET_NULL, related_name="user_logs", null=True, blank=True
    )
    event_log = models.ForeignKey(
        EventLog,
        on_delete=models.SET_NULL,
        related_name="user_logs",
        null=True,
        blank=True,
    )
    message = models.TextField(null=True, blank=True)

    class Meta:
        verbose_name = "User Log"
        verbose_name_plural = "User Logs"

    def __str__(self):
        return self.message

    @classmethod
    def create_user_log(cls, user, message, content_type, event_log_codename):
        """
        If there is no event log with the given name, content type and codename, it will create one.
        """
        configured_message = cls.configure_message(message)
        event_log = EventLog.get_or_create_event_log(
            name=event_log_codename,
            content_type=content_type,
            codename=event_log_codename,
        )
        user_log = UserLog.objects.create(
            user=user, message=configured_message, event_log=event_log
        )
        return user_log

    @classmethod
    def configure_message(cls, message, **kwargs):
        """
        This method is used to add datetime and user information to the message.
        """
        now_formatted = datetime.strftime(datetime.now(), "%Y-%m-%d %H:%M:%S")
        message = f"{now_formatted} - {message}"
        return message

Helpers

log_user

log_user helps us to create logs in DB.

from apps.management.models import UserLog


def log_user(user, message, content_type, event_log_codename):
    """This function will create a new UserLog object. A proxy function for UserLog.create_user_log()

    If EventLog object does not exist, it will be created.

    Arguments:
        user (User): User object
        message (str): Message to be logged
        content_type (str): ContentType string. It can be found in apps.management.models.EventLog.get_content_type
        event_log_codename (str): Codename of EventLog object. It can be anything, if it does not exist, it will be created.

    Example:
        log_user(user, "Page X permission given to User", 'management', "permission_updated")
    """
    UserLog.create_user_log(user, message, content_type, event_log_codename)

Dynamic Importer

DynamicImporter helps us to import classes from modules by using strings. I also used this approach in my ajax_method_caller method.

import importlib

class DynamicImporter:
    def import_module(self, module_name):
        return importlib.import_module(module_name)

    def import_class(self, module_name, class_name):
        module = self.import_module(module_name)
        return getattr(module, class_name)


# Usage
importer = DynamicImporter()
user_class = importer.import_class("apps.management.models", "User")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment