Skip to main content

DRF Audit Logger

Audit logging for Django REST Framework with multilingual support.

PyPI version Python Django DRF License

Overview

DRF Audit Logger is a Django + Django REST Framework package that automatically logs user actions such as login, logout, create, update, and delete events. All messages are rendered dynamically using Django's gettext framework, so they automatically adapt to the active language — no code changes needed when adding a new language.

Whether you need a simple audit trail for compliance, a debugging tool for tracking data changes, or a full activity log with REST API access, DRF Audit Logger provides it out of the box.


Features

  • Automatic logging via Django signals — no code changes in your models or views
  • Login / Logout / Failed login tracking
  • Create / Update / Delete tracking with full changes diff
  • Multilingual messages via Django's gettext (add a language by dropping a .po file)
  • Dynamic model names from Meta.verbose_name (auto-translated)
  • Dynamic field names from field.verbose_name (auto-translated)
  • Sensitive field masking (password, token, api_key, ...)
  • Works with any authentication system (Session, JWT, Token, OAuth, Custom)
  • Custom user model support via AUTH_USER_MODEL
  • Request metadata capture — IP address, user agent
  • Configurable model exclusions via AUDIT_LOG_EXCLUDE_MODEL_LOGGING
  • REST API for querying and filtering logs
  • Django admin integration with color-coded action badges
  • Database-indexed for fast queries on large datasets

Requirements

  • Python >= 3.8
  • Django >= 3.2
  • djangorestframework >= 3.12

Installation

pip install drf-audit-logger

Setup

1. Add to INSTALLED_APPS

INSTALLED_APPS = [
    # ...
    'rest_framework',
    'drf_audit_logger',
    # ...
]

2. Add the middleware

MIDDLEWARE = [
    # ...
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    # ...
    'drf_audit_logger.middleware.AuditLogMiddleware',
]

⚠️ Important: AuditLogMiddleware must be placed after AuthenticationMiddleware. If you have custom middleware that checks request.user (like a role-based middleware), place AuditLogMiddleware before it so it can authenticate the user with DRF's authenticators.

3. Add the URLs

In your project's main urls.py:

from django.urls import path, include

urlpatterns = [
    # ...
    path('auditlog/', include('drf_audit_logger.urls', namespace='drf_audit_logger')),
    # ...
]

4. Run migrations

python manage.py migrate drf_audit_logger

5. Configure DRF authentication (if not already done)

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.TokenAuthentication',
        # or JWT:
        # 'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

Configuration (optional)

Add these to your settings.py to customize behavior:

# Models that should NOT be logged
AUDIT_LOG_EXCLUDE_MODEL_LOGGING = [
    'admin.LogEntry',
    'sessions.Session',
    'contenttypes.ContentType',
    'drf_audit_logger.AuditLog',
    'authtoken.Token',
    'token_blacklist.OutstandingToken',
    'token_blacklist.BlacklistedToken',
]

# Fields that should be masked in `changes`
AUDIT_LOG_SENSITIVE_FIELDS = [
    'password',
    'password1',
    'password2',
    'token',
    'access',
    'refresh',
    'secret',
    'api_key',
    'authorization',
]

# Enable / disable event types
AUDIT_LOG_LOG_AUTH_EVENTS = True
AUDIT_LOG_LOG_MODEL_EVENTS = True

# Max length of field values in `changes`
AUDIT_LOG_MAX_FIELD_LENGTH = 100

🌍 Multilingual Support

DRF Audit Logger uses Django's gettext framework. Messages are rendered at display time, so they always reflect the currently active language.

Supported languages out of the box

  • 🇬🇧 English (en)
  • 🇮🇷 Persian / Farsi (fa)

How to enable multilingual support

Step 1: Configure your settings.py

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

LANGUAGE_CODE = 'fa'     # or 'en'
USE_I18N = True
USE_TZ = True

LANGUAGES = [
    ('fa', 'فارسی'),
    ('en', 'English'),
]

LOCALE_PATHS = [
    BASE_DIR / 'locale',   # 👈 your project's locale folder
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',   # 👈 required
    'django.middleware.common.CommonMiddleware',
    # ...
]

Step 2: Create the locale/ folder in your project

Create this structure in your project's root (next to manage.py):

your_project/
├── manage.py
├── locale/
│   ├── fa/
│   │   └── LC_MESSAGES/
│   │       └── django.po
│   └── en/
│       └── LC_MESSAGES/
│           └── django.po
└── ...

Step 3: Translate your project's models and fields

In locale/fa/LC_MESSAGES/django.po:

msgid ""
msgstr ""
"Language: fa\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

# ---------- Model names ----------
msgid "Company"
msgstr "شرکت"

msgid "Companies"
msgstr "شرکت‌ها"

msgid "Product"
msgstr "محصول"

msgid "Order"
msgstr "سفارش"

# ---------- Field names ----------
msgid "Title"
msgstr "عنوان"

msgid "Name"
msgstr "نام"

msgid "Price"
msgstr "قیمت"

msgid "Description"
msgstr "توضیحات"

# ---------- (Optional) Override package messages ----------
msgid "User %(user)s logged in"
msgstr "کاربر %(user)s وارد سیستم شد"

In locale/en/LC_MESSAGES/django.po:

msgid ""
msgstr ""
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

msgid "Company"
msgstr "Company"

msgid "Product"
msgstr "Product"

msgid "Title"
msgstr "Title"

msgid "Price"
msgstr "Price"

⚠️ Important: For translations to work, your models must use gettext_lazy (_()) for verbose_name and field names:

from django.utils.translation import gettext_lazy as _

class Company(models.Model):
    title = models.CharField(_('Title'), max_length=255)

    class Meta:
        verbose_name = _('Company')
        verbose_name_plural = _('Companies')

Step 4: Compile translations

Option A: If you have GNU gettext installed

cd your_project
django-admin compilemessages

Option B: If gettext is not installed (Windows users)

Install GNU gettext for Windows from: https://mlocati.github.io/articles/gettext-iconv-windows.html

Or compile manually with msgfmt:

msgfmt locale\fa\LC_MESSAGES\django.po -o locale\fa\LC_MESSAGES\django.mo
msgfmt locale\en\LC_MESSAGES\django.po -o locale\en\LC_MESSAGES\django.mo

Step 5: Add a new language

Just create a new folder locale/<lang_code>/LC_MESSAGES/, add a django.po file with translations, and compile it. No code changes required.


How to change the active language

You can change the active language in three ways:

1. Globally in settings.py

LANGUAGE_CODE = 'fa'   # or 'en'

2. Per-request via Accept-Language header

curl -H "Accept-Language: fa" \
     -H "Authorization: Bearer TOKEN" \
     http://localhost:8000/auditlog/api/logs/

curl -H "Accept-Language: en" \
     -H "Authorization: Bearer TOKEN" \
     http://localhost:8000/auditlog/api/logs/

3. Programmatically

from django.utils import translation

with translation.override('en'):
    print(log.message)   # in English

You can also fetch a log in a specific language:

log.get_message_in_language('fa')   # Persian
log.get_message_in_language('en')   # English

API Endpoints

All endpoints require superuser authentication.

Method Endpoint Description
GET /auditlog/api/logs/ List all logs with filtering
GET /auditlog/api/logs/<pk>/ Retrieve a single log
GET /auditlog/api/logs/today/ Today's logs
GET /auditlog/api/logs/me/ Current user's logs
GET /auditlog/api/logs/user/<user_id>/ Logs of a specific user
GET /auditlog/api/logs/model/<model_name>/ Logs of a specific model
GET /auditlog/api/logs/object/<model_name>/<object_id>/ Logs of a specific object
GET /auditlog/api/recent/ Most recent activity
GET /auditlog/api/stats/ Statistics
GET /auditlog/api/actions/ Available action choices

Query Parameters

The list endpoint (/auditlog/api/logs/) supports:

Parameter Description Example
action Filter by action type ?action=login
model Filter by model name ?model=Product
user Filter by user ID ?user=1
from Filter from date (YYYY-MM-DD) ?from=2025-01-01
to Filter to date (YYYY-MM-DD) ?to=2025-01-31
search Search in object, model, username ?search=laptop
page Page number (pagination) ?page=2

Examples

Login event

{
  "action": "login",
  "action_display": "ورود",
  "user_display": "علی رضایی",
  "ip_address": "192.168.1.1",
  "message": "کاربر علی رضایی وارد شد"
}

Create event

{
  "action": "create",
  "action_display": "ایجاد",
  "user_display": "علی رضایی",
  "model_name": "Product",
  "object_repr": "لپ‌تاپ ایسوس",
  "changes": {
    "name": "لپ‌تاپ ایسوس",
    "price": 1000,
    "stock": 50
  },
  "message": "کاربر علی رضایی محصول «لپ‌تاپ ایسوس» را ایجاد کرد"
}

Update event (single field)

{
  "action": "update",
  "action_display": "ویرایش",
  "user_display": "علی رضایی",
  "model_name": "Product",
  "object_repr": "لپ‌تاپ ایسوس",
  "changes": {
    "price": {
      "old": 1000,
      "new": 1500
    }
  },
  "changes_list": [
    {
      "field": "price",
      "field_verbose": "قیمت",
      "old": 1000,
      "new": 1500
    }
  ],
  "message": "کاربر علی رضایی «قیمت» محصول «لپ‌تاپ ایسوس» را از «1000» به «1500» تغییر داد"
}

Delete event

{
  "action": "delete",
  "user_display": "علی رضایی",
  "model_name": "Product",
  "object_repr": "لپ‌تاپ ایسوس",
  "message": "کاربر علی رضایی محصول «لپ‌تاپ ایسوس» را حذف کرد"
}

Failed login

{
  "action": "login_failed",
  "ip_address": "192.168.1.1",
  "message": "تلاش ناموفق برای ورود از IP 192.168.1.1"
}

Manual Logging

Log custom events from anywhere in your code:

from drf_audit_logger import log

log(
    user=request.user,
    action='custom',
    message_id='User %(user)s downloaded the report',
    params={'user': request.user.get_full_name()},
)

Or use the service directly:

from drf_audit_logger.services import AuditLogService

# Login / logout
AuditLogService.log_login(user=user, ip_address='192.168.1.1')
AuditLogService.log_logout(user=user)
AuditLogService.log_login_failed(username='ali', ip_address='192.168.1.1')

# Custom event
AuditLogService.log_custom(
    user=request.user,
    message_id='Report downloaded by %(user)s',
    params={'user': request.user.username},
)

Django Admin

Navigate to /admin/drf_audit_logger/auditlog/ to browse logs with:

  • Color-coded action badges (green = login, red = delete, yellow = update)
  • Filters by action, model, timestamp, user
  • Search across model, object, username, IP
  • Date hierarchy navigation
  • Read-only enforcement (logs cannot be edited)

How It Works

  1. AuditLogMiddleware authenticates the request using DRF's configured authenticators (JWT, Token, Session, etc.).
  2. It stores request.user, IP, and user-agent in thread-local storage.
  3. Django's pre_save, post_save, and post_delete signals trigger the audit service.
  4. The service detects changes, masks sensitive fields, and stores raw data.
  5. Messages are rendered dynamically at display time using gettext.

Security

  • Sensitive field masking: passwords, tokens, and API keys are replaced with ***MASKED*** before storage.
  • Superuser-only API access: all endpoints are protected by IsSuperUser.
  • Read-only logs: audit entries cannot be modified via admin or API.
  • Session-based admin: the Django admin uses session authentication as usual.

Configuration Reference

Setting Default Description
AUDIT_LOG_EXCLUDE_MODEL_LOGGING See above List of models to exclude from logging
AUDIT_LOG_SENSITIVE_FIELDS See above Fields to mask in changes
AUDIT_LOG_LOG_AUTH_EVENTS True Log login / logout / failed login
AUDIT_LOG_LOG_MODEL_EVENTS True Log create / update / delete
AUDIT_LOG_MAX_FIELD_LENGTH 100 Max length of field values in changes

Troubleshooting

Messages are not translated

  1. Check that LocaleMiddleware is enabled in MIDDLEWARE.
  2. Check that LANGUAGES is set in settings.py.
  3. Compile the .po files with django-admin compilemessages or msgfmt.
  4. Restart the Django server (translations are loaded at startup).
  5. Verify .mo files exist next to .po files.

AuditLogMiddleware breaks my admin login

Make sure AuditLogMiddleware is placed after AuthenticationMiddleware and after any custom middleware that runs on the admin paths. If you have a middleware that checks request.user before DRF authenticates, place AuditLogMiddleware before it.

Changes are not detected on update

The package uses pre_save signals to capture the old state. Make sure you're using instance.save() (not bulk_update or QuerySet.update(), which bypass signals).

django-admin compilemessages fails with "Cannot find msgfmt"

Install GNU gettext for Windows: https://mlocati.github.io/articles/gettext-iconv-windows.html

Or compile manually:

msgfmt locale\fa\LC_MESSAGES\django.po -o locale\fa\LC_MESSAGES\django.mo

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-feature)
  3. Commit your changes (git commit -m "Add my feature")
  4. Push to the branch (git push origin feature/my-feature)
  5. Open a Pull Request

Reporting Issues

Found a bug? Please open an issue at: https://github.com/tahazarei777/drf-audit-logger/issues

Include:

  • Your Python / Django / DRF versions
  • Minimal reproduction steps
  • The expected vs actual behavior
  • Any relevant logs or tracebacks

License

Licensed under the BSD 3-Clause License. See LICENSE.md for details.

Copyright © 2025, Taha Zarei.

Release files for drf-audit-logger 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for drf-audit-logger 1.0.0
File Size Uploaded
drf_audit_logger-1.0.0.tar.gz 28.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for drf-audit-logger 1.0.0
File Interpreter ABI Platform
drf_audit_logger-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 57.9 kB

Release files / drf_audit_logger-1.0.0.tar.gz

Download URL drf_audit_logger-1.0.0.tar.gz
Size 28.9 kB
Tags Source
SHA-256 checksum
How to use checksums
1db6c43e9ab9328ca0cc7538317e723a2022e6c67b241f740e8427aebb2cc83b
BLAKE2b-256 checksum
How to use checksums
8910a3f3f36370c74fcb87de81a7e3dc67685b202804996c646a415633d14666
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.0

Release files / drf_audit_logger-1.0.0-py3-none-any.whl

Download URL drf_audit_logger-1.0.0-py3-none-any.whl
Size 29.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3d126286f84bb1ba4f9b75f44f3a5ba38ff3e532d3a7e5b619d2e695b684474b
BLAKE2b-256 checksum
How to use checksums
b8254ffac7ca4d00fb7c177643e5b16578a99ffaa9cceeb5b283d0e50f42002f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.0

Release history Release notifications | RSS feed

1.0.1

2 release files

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page