This release is a pre-release and may not be stable for production use.
django-forge-log
Release candidate.
django-forge-logest en1.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 version1.0.0finale — 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_idest unCharField(pas une FK typée) pour supporter les PK non entières (UUID) sans une table par modèle — même compromis quedjango-reversion/django-guardian. Un index composite(content_type, object_id)compense l'absence de contrainte FK native.track_actionsansget_instancene peut pas capturer une création (pas de PK dans l'URL avant l'exécution de la vue) : utilisezforge_log.drf.AuditViewSetMixinpour un ViewSet DRF, ou fournissezget_instanceexplicitement.- 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é, utilisezon_commitoucelery. - 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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f4e173f9b92b775b2ba94fd10a712335a59c584372b934561d1018d4acaab1c
|
|
| MD5 |
8da625fb141e52a7aecb01771e616238
|
|
| BLAKE2b-256 |
fdb0badf4fe2e4cd1cd4994b8d5fa5b2ed98d2b82c89d00b39fbf5f567b20b61
|
Provenance
The following attestation bundles were made for django_forge_log-1.0.0rc3.tar.gz:
Publisher:
publish.yml on alzeph/django-forge-log
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_forge_log-1.0.0rc3.tar.gz -
Subject digest:
7f4e173f9b92b775b2ba94fd10a712335a59c584372b934561d1018d4acaab1c - Sigstore transparency entry: 2475560189
- Sigstore integration time:
-
Permalink:
alzeph/django-forge-log@3e6470a9c67142448b656e77eeb7a63221002d82 -
Branch / Tag:
refs/tags/v1.0.0rc3 - Owner: https://github.com/alzeph
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3e6470a9c67142448b656e77eeb7a63221002d82 -
Trigger Event:
release
-
Statement type:
File details
Details for the file django_forge_log-1.0.0rc3-py3-none-any.whl.
File metadata
- Download URL: django_forge_log-1.0.0rc3-py3-none-any.whl
- Upload date:
- Size: 30.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05aa48dcf2883c4334b8a6e82fac922bce3c18ae2d31258b374583ae85ecf679
|
|
| MD5 |
587e3a7a8002be2aded6194752d4fc53
|
|
| BLAKE2b-256 |
40aeee355f1bf4dabefba12b51a9ecafe36e785e337bebf56e16a33db5abc4f8
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_forge_log-1.0.0rc3-py3-none-any.whl -
Subject digest:
05aa48dcf2883c4334b8a6e82fac922bce3c18ae2d31258b374583ae85ecf679 - Sigstore transparency entry: 2475560213
- Sigstore integration time:
-
Permalink:
alzeph/django-forge-log@3e6470a9c67142448b656e77eeb7a63221002d82 -
Branch / Tag:
refs/tags/v1.0.0rc3 - Owner: https://github.com/alzeph
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3e6470a9c67142448b656e77eeb7a63221002d82 -
Trigger Event:
release
-
Statement type: