Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

django-forge-log

CI PyPI License: MIT Python 3.12+

Release candidate. django-forge-log est en 1.0.0rc3 : l'API est considérée figée mais n'a pas encore été éprouvée par un usage réel en dehors de ce dépôt. Les retours (issues, cas d'usage, bugs) sont les bienvenus avant de tagger la version 1.0.0 finale — voir RELEASING.md.

Un audit trail léger et automatique — Qui, Quoi, Quand, Où, et le Diff avant/après — pour les vues Django (FBV, CBV, DRF ViewSets) et l'Admin, stocké dans une seule table JSON centrale.

Le problème

Le LogEntry natif de django.contrib.admin ne trace que ce qui passe par l'Admin. Dès qu'une mutation passe par une API DRF, une vue classique ou un script, plus rien n'est journalisé. django-simple-history/django-reversion résolvent ça en dupliquant une table par modèle suivi — lourd, et ça n'explique pas qui a fait le changement ni depuis où. django-forge-log vise une piste d'audit unique et compacte, avec un typage strict du diff.

Installation

Le cœur (middleware, décorateur, moteur de diff, Admin) ne dépend que de Django et Pydantic. Les intégrations DRF, django-signals-all et Celery sont des extras à activer séparément, à combiner selon les besoins :

# Cœur seul
uv add django-forge-log

# Avec l'intégration DRF (forge_log.drf.AuditViewSetMixin)
uv add "django-forge-log[drf]"

# Avec l'intégration bulk django-signals-all (bulk_create/bulk_update/.update())
uv add "django-forge-log[signals]"

# Avec le backend d'écriture Celery (WRITE_BACKEND="celery")
uv add "django-forge-log[celery]"

# Plusieurs extras à la fois
uv add "django-forge-log[drf,signals,celery]"

Avec pip, mêmes combinaisons :

pip install django-forge-log
pip install "django-forge-log[drf,signals,celery]"

Une release candidate n'étant pas une version finale, PyPI ne l'installe pas par défaut avec pip install django-forge-log — utilisez --pre ou fixez la version exacte tant que 1.0.0 n'est pas taggé :

uv add "django-forge-log==1.0.0rc3"
pip install "django-forge-log==1.0.0rc3"
# settings.py
INSTALLED_APPS = [
    ...,
    "django.contrib.contenttypes",
    "forge_log",
]

MIDDLEWARE = [
    ...,
    "forge_log.middleware.RequestContextMiddleware",
]
python manage.py migrate forge_log

Démarrage rapide

Vue générique (FBV/CBV)

from forge_log.decorators import track_action
from .models import Article


@track_action(Article)
def update_article(request, pk):
    article = Article.objects.get(pk=pk)
    article.status = "published"
    article.save()
    return HttpResponse(...)

Sans configuration, l'instance suivie est retrouvée via le PK présent dans les kwargs de la vue (pk, ou le nom du champ clé primaire du modèle) — le cas courant /resource/<pk>/. Pour une création (pas de PK dans l'URL avant l'exécution de la vue), fournissez get_instance explicitement, ou préférez le mixin DRF ci-dessous.

Les vues async def sont détectées automatiquement (get_instance, le calcul du diff et l'écriture basculent alors sur sync_to_async) :

@track_action(Article)
async def update_article(request, pk):
    article = await Article.objects.aget(pk=pk)
    article.status = "published"
    await article.asave()
    return JsonResponse(...)

Avec WRITE_BACKEND="asyncio", ce chemin s'exécute dans le thread de l'executor plutôt que sur l'event loop : l'écriture retombe alors sur le mode synchrone d'AsyncTaskWriter plutôt que sur asyncio.create_task() — toujours correct, mais sans le gain de perf attendu. Préférez thread ou celery pour des vues suivies async à fort trafic.

ViewSet DRF

from rest_framework.viewsets import ModelViewSet
from forge_log.drf import AuditViewSetMixin


class ArticleViewSet(AuditViewSetMixin, ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

Capture nativement create/update/partial_update/destroy, y compris les créations (contrairement à track_action, qui a besoin d'un PK dans l'URL). Nécessite l'extra django-forge-log[drf].

Admin

from django.contrib import admin
from forge_log.admin import AuditModelAdminMixin


@admin.register(Article)
class ArticleAdmin(AuditModelAdminMixin, admin.ModelAdmin):
    pass

La table ActionLog elle-même est consultable (lecture seule) dans l'Admin via forge_log.admin.ActionLogAdmin.

Opérations en masse (bulk_create, bulk_update, .update())

Ces opérations contournent Model.save() et aucune des trois intégrations ci-dessus ne peut les voir depuis la vue. En branchant django-signals-all (extra django-forge-log[signals]), les mutations bulk émises via son BulkSignalManager sont journalisées automatiquement, sans code supplémentaire :

# models.py
from django.db import models
from django_signals_all.orm.manager import BulkSignalManager


class Article(models.Model):
    ...
    objects = BulkSignalManager()
# settings.py
INSTALLED_APPS = [..., "django_signals_all", "forge_log"]

.filter(...).update(...) ne charge pas les instances modifiées : une seule entrée agrégée est journalisée (action="BULK_UPDATE", object_id=None), avec la liste des PK impactés dans metadata. bulk_create() et Manager.bulk_update() journalisent en revanche une entrée par instance.

Historique d'un objet

ActionLog.objects.for_object(article)          # QuerySet, du plus récent au plus ancien
ActionLog.objects.for_object(article).filter(action="UPDATE")

Ou directement depuis l'instance, en ajoutant le mixin au modèle suivi :

from forge_log.relations import ForgeLogRelationMixin


class Article(ForgeLogRelationMixin, models.Model):
    ...


article.forge_log_entries.all()  # équivalent à ActionLog.objects.for_object(article)

ForgeLogRelationMixin est une classe abstraite (aucun champ concret, donc aucune migration requise), volontairement pas un GenericRelation : GenericRelation impose on_delete=CASCADE de façon non configurable côté Django, ce qui supprimerait tout l'historique d'audit d'un objet au moment même où il est supprimé — l'inverse de ce qu'un audit trail doit garantir.

Écriture asynchrone (WRITE_BACKEND)

Écrire dans ActionLog a un coût. Cinq backends, sélectionnables par projet ou pour un test :

Backend Comportement Durabilité Coût sur la requête
sync Écrit immédiatement Maximale, mais journalise même une transaction annulée Élevé
on_commit transaction.on_commit() Jamais loggé si rollback Élevé (toujours avant la réponse)
thread (défaut) File en mémoire + thread démon, bulk_create par lots Fenêtre de perte (~200 ms) si le process est tué Quasi nul (queue.put_nowait)
asyncio asyncio.create_task() (vues async def) Idem thread ; nécessite un event loop actif Quasi nul
celery Dispatch total via une tâche Celery Robuste (persiste dans le broker) Quasi nul
FORGE_LOG = {
    "WRITE_BACKEND": "thread",  # "sync" | "on_commit" | "thread" | "asyncio" | "celery"
}

celery nécessite l'extra django-forge-log[celery] et un broker déjà configuré côté projet.

Sécurité et PII

Le diff peut exposer des données sensibles s'il n'est pas configuré :

FORGE_LOG = {
    # Champs jamais journalisés (avant *et* après), par motif regex.
    "EXCLUDED_FIELDS": [r".*secret.*", r".*token.*", r".*_key$", r"credit_card", r"ssn"],
    # Champs journalisés comme "modifiés" sans exposer les valeurs :
    # {"password": {"masked": true}} plutôt que {"before": ..., "after": ...}.
    "MASKED_FIELDS": ["password"],
}

Ces réglages peuvent aussi être fixés par modèle, ce qui prime sur la config globale :

class Article(models.Model):
    ...
    class ForgeLogMeta:
        excluded_fields = ["internal_note"]
        masked_fields = []

Les FileField/ImageField ne journalisent jamais de contenu binaire, seulement le chemin (.name).

Rétention

python manage.py forgelog_purge --days 90         # supprime les entrées de plus de 90 jours
python manage.py forgelog_purge --days 90 --dry-run

Ou via FORGE_LOG["RETENTION_DAYS"] pour ne pas avoir à passer --days à chaque appel (à brancher sur un cron applicatif — forgelog_purge ne s'exécute jamais tout seul).

Configuration complète

FORGE_LOG = {
    "ENABLED": True,
    "WRITE_BACKEND": "thread",
    "EXCLUDED_FIELDS": [r".*secret.*", r".*token.*", r".*_key$", r"credit_card", r"ssn"],
    "MASKED_FIELDS": ["password"],
    "RETENTION_DAYS": None,
    "THREAD_FLUSH_INTERVAL": 0.2,     # secondes, backend "thread"
    "THREAD_MAX_BATCH_SIZE": 50,      # backend "thread"
    "THREAD_MAX_QUEUE_SIZE": 10_000,  # backend "thread"
}

Limitations connues

  • object_id est un CharField (pas une FK typée) pour supporter les PK non entières (UUID) sans une table par modèle — même compromis que django-reversion/django-guardian. Un index composite (content_type, object_id) compense l'absence de contrainte FK native.
  • track_action sans get_instance ne peut pas capturer une création (pas de PK dans l'URL avant l'exécution de la vue) : utilisez forge_log.drf.AuditViewSetMixin pour un ViewSet DRF, ou fournissez get_instance explicitement.
  • Backend thread : les entrées en file d'attente sont perdues si le process est tué avant le prochain flush (~THREAD_FLUSH_INTERVAL). Pour une garantie stricte de durabilité, utilisez on_commit ou celery.
  • Le diff ne compare que les champs concrets du modèle (_meta.concrete_fields) ou la liste explicite passée à fields= — pas les relations M2M implicites.

Développement

uv sync --group dev

uv run ruff check src tests
uv run ruff format --check src tests
uv run mypy

# SQLite (par défaut, pas de dépendance externe)
uv run pytest --cov=forge_log --cov-report=term-missing

# PostgreSQL et MySQL (nécessite Docker)
docker compose up -d
FORGE_LOG_TEST_DB=postgres uv run pytest
FORGE_LOG_TEST_DB=mysql uv run pytest

Voir CONTRIBUTING.md pour contribuer, CHANGELOG.md pour l'historique des versions, et RELEASING.md pour le processus de publication.

Licence

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_forge_log-1.0.0rc3.tar.gz (100.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_forge_log-1.0.0rc3-py3-none-any.whl (30.4 kB view details)

Uploaded Python 3

File details

Details for the file django_forge_log-1.0.0rc3.tar.gz.

File metadata

  • Download URL: django_forge_log-1.0.0rc3.tar.gz
  • Upload date:
  • Size: 100.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for django_forge_log-1.0.0rc3.tar.gz
Algorithm Hash digest
SHA256 7f4e173f9b92b775b2ba94fd10a712335a59c584372b934561d1018d4acaab1c
MD5 8da625fb141e52a7aecb01771e616238
BLAKE2b-256 fdb0badf4fe2e4cd1cd4994b8d5fa5b2ed98d2b82c89d00b39fbf5f567b20b61

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_forge_log-1.0.0rc3.tar.gz:

Publisher: publish.yml on alzeph/django-forge-log

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_forge_log-1.0.0rc3-py3-none-any.whl.

File metadata

File hashes

Hashes for django_forge_log-1.0.0rc3-py3-none-any.whl
Algorithm Hash digest
SHA256 05aa48dcf2883c4334b8a6e82fac922bce3c18ae2d31258b374583ae85ecf679
MD5 587e3a7a8002be2aded6194752d4fc53
BLAKE2b-256 40aeee355f1bf4dabefba12b51a9ecafe36e785e337bebf56e16a33db5abc4f8

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_forge_log-1.0.0rc3-py3-none-any.whl:

Publisher: publish.yml on alzeph/django-forge-log

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.0.0rc3 This release

2 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