DRF Audit Logger
Audit logging for Django REST Framework with multilingual support.
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
changesdiff - ✅ Multilingual messages via Django's
gettext(add a language by dropping a.pofile) - ✅ 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:
AuditLogMiddlewaremust be placed afterAuthenticationMiddleware. If you have custom middleware that checksrequest.user(like a role-based middleware), placeAuditLogMiddlewarebefore 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(_()) forverbose_nameand 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
AuditLogMiddlewareauthenticates the request using DRF's configured authenticators (JWT, Token, Session, etc.).- It stores
request.user,IP, anduser-agentin thread-local storage. - Django's
pre_save,post_save, andpost_deletesignals trigger the audit service. - The service detects changes, masks sensitive fields, and stores raw data.
- 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
- Check that
LocaleMiddlewareis enabled inMIDDLEWARE. - Check that
LANGUAGESis set insettings.py. - Compile the
.pofiles withdjango-admin compilemessagesormsgfmt. - Restart the Django server (translations are loaded at startup).
- Verify
.mofiles exist next to.pofiles.
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:
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes (
git commit -m "Add my feature") - Push to the branch (
git push origin feature/my-feature) - 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)
| File | Size | Uploaded | |
|---|---|---|---|
| drf_audit_logger-1.0.0.tar.gz | 28.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|