企業向けのシステム開発をしていると、コンプライアンス上の理由からシステムの操作記録やデータの変更内容を記録したいという要件はよくあります。そういったときに役立つDjango拡張django-auditlogを紹介します。
django-auditlogは、名前の通りaudit log(監査ログ)を記録するためのDjango拡張です。django-auditlogを導入すると、モデル操作の履歴を記録できます。標準にも似たような機能がありますが、主に以下の違いがあります。
標準機能 | django-auditlog | |
---|---|---|
いつ記録するか | adminからの操作があった場合 | Djangoのモデルを使った機能すべて |
記録対象の操作 | 登録・更新・削除 | 登録・更新・削除・閲覧 |
変更の差分を記録するか | 記録しない | 記録する |
django-auditlogのメンテナは、Djangoユーザーにはお馴染みのDjango Debug Toolbarを開発しているコミュニティ「Jazzband」です。
以下でdjango-auditlogの導入方法と使い方について説明します。
なお、本記事のサンプルコードはdjango-auditlog 2.3.0で動作確認しています。
導入方法
まず、以下コマンドを実行してdjango-auditlogをインストールします。
$ pip install django-auditlog
次に、Djangoのsettings.py
をエディタで開き、INSTALLED_APPS
に"auditlog"
を加えます。
INSTALLED_APPS = [
# 省略
"auditlog",
# 省略
]
次に、settings.py
のMIDDLEWARE
に"auditlog.middleware.AuditlogMiddleware"
を加えます。リクエストを変更するミドルウェアは"auditlog.middleware.AuditlogMiddleware"
より上に書いてください。
MIDDLEWARE = [
# 省略(リクエストを変更するミドルウェアはここに書く)
"auditlog.middleware.AuditlogMiddleware",
# 省略
]
使い方
ここでは、books
アプリケーションに以下のモデルを定義している前提でdjango-auditlogの使い方について説明します。
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=50)
price = models.IntegerField()
class Meta:
verbose_name = "book"
verbose_name_plural = "books"
def __str__(self):
return self.title
本記事で紹介するサンプルコードを含むDjangoプロジェクトは以下GitHubリポジトリに置いているので、実際に動かしてみたい場合は参照してください。
https://github.com/ryu22e/django_auditlog_example
モデルをdjango-auditlogに登録する
django-auditlogはデフォルトではモデルの操作履歴を記録しません。
記録できるようにする方法はいくつかありますが、今回は一番簡単な方法を紹介します。settings.py
に以下の設定を加えるだけです。
AUDITLOG_INCLUDE_ALL_MODELS = True
上記の設定で、すべてのモデルがdjango-auditlogに登録され、登録・更新・削除の操作が記録されるようになります。なお、閲覧についてはビューのコードを変更する必要があります。具体的な方法については後述します。
モデルの登録・更新・削除の記録を確認する
adminにログインすると、画面上部に「AUDIT LOG」という項目が表示されています。これがdjango-auditlogの監査ログです。
admin上でBook
モデルの登録・更新・削除を行ってから、http://127.0.0.1:8000/admin/auditlog/logentry/
を開いてください。モデルの操作履歴が表示されているはずです。
一部のモデルやフィールドの操作を記録したくない場合は、AUDITLOG_EXCLUDE_TRACKING_FIELDS
とAUDITLOG_EXCLUDE_TRACKING_FIELDS
を追記します。
# 記録しないモデル
AUDITLOG_EXCLUDE_TRACKING_MODELS = (
"accounts.User",
)
# 記録しないフィールド
AUDITLOG_EXCLUDE_TRACKING_FIELDS = (
"created",
"modified"
)
モデルの閲覧記録を確認する
閲覧記録の場合は、ビューのコードに手を加える必要があります。
以下のようにクラスビューにauditlog.mixins.LogAccessMixin
クラスを継承させます。
from auditlog.mixins import LogAccessMixin
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.detail import DetailView
from .models import Book
class DetailView(LogAccessMixin, LoginRequiredMixin, DetailView):
model = Book
LogAccessMixin
クラスはrender_to_response()
メソッドをオーバーライドするので、render_to_response()
メソッドを定義していないクラスビューには使えません。つまり、Django REST frameworkのビューの場合は、LogAccessMixin
クラスは使えません。
Django REST frameworkの場合は、以下のようにget_object()
メソッドをオーバーライドして、取得したモデルのインスタンスをauditlog.signals.accessed.send()
メソッドに渡します。
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from auditlog.signals import accessed
from .models import Book
from .serializers import BookSerializer
class BookRetrieveViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = (IsAuthenticated,)
# ↓これを追加
def get_object(self):
obj = super().get_object()
# ↓ここで閲覧の記録を取れる
accessed.send(sender=obj.__class__, instance=obj)
return obj
django-auditlogで提供していない機能
以下の機能はdjango-auditlogでは提供していないので、必要なら自分で仕組みを実装してください。
監査ログのレポート出力
django-auditlogではadmin上でログを確認する手段しか提供していません。別の形式でレポートを出力したい場合は、以下のようにauditlog.models.LogEntryクラスを使って監査ログを取得してからレポートを出力するコードを書くといいでしょう。
from pathlib import Path
from datetime import timezone
from django.core.management.base import BaseCommand
from dateutil.parser import parse
from auditlog.models import LogEntry
class Command(BaseCommand):
help = "Output audit log report"
def _generate_row(self, log_entry):
return f"{log_entry.timestamp} {log_entry.get_action_display()} {log_entry.content_type} {log_entry.action} {log_entry.changes}"
def add_arguments(self, parser):
parser.add_argument("--start_datetime", "-s", type=str, help="Start datetime")
parser.add_argument("--end_datetime", "-e", type=str, help="End datetime")
parser.add_argument("--output_file", "-o", type=str, help="Output file")
def handle(self, *args, **options):
start_datetime = parse(options["start_datetime"]).astimezone(timezone.utc)
end_datetime = parse(options["end_datetime"]).astimezone(timezone.utc)
output_file = options["output_file"]
p = Path(output_file)
for log_entry in LogEntry.objects.filter(
timestamp__gte=start_datetime, timestamp__lte=end_datetime
):
row = self._generate_row(log_entry)
p.write_text(f"{row}\n")
self.stdout.write(self.style.SUCCESS(f"Report written to {output_file}"))
古い監査ログの削除
保存された監査ログは有効期限がないので溜まっていく一方です。以下のようなコマンドを実装し、定期実行して古い監査ログを削除するといいでしょう。
from django.core.management.base import BaseCommand
from django.utils import timezone
from dateutil.relativedelta import relativedelta
from auditlog.models import LogEntry
class Command(BaseCommand):
help = "Cleanup auditlog entries"
def _generate_row(self, log_entry):
return f"{log_entry.timestamp} {log_entry.get_action_display()} {log_entry.content_type} {log_entry.action} {log_entry.changes}"
def add_arguments(self, parser):
parser.add_argument(
"--retention_days", "-r", type=int, default=1825, help="Retention days"
)
def handle(self, *args, **options):
retention_days = options["retention_days"]
result = LogEntry.objects.filter(
timestamp__date__lt=(
timezone.now() - relativedelta(days=retention_days)
).date()
).delete()
c = result[0]
self.stdout.write(self.style.SUCCESS(f"Deleted {c} rows"))
バージョン3系にアップデートするにあたっての注意点
2024/03/17現在、django-auditlogの最新バージョンは2.3.0です。バージョン3からはデータの扱いが変わるため、以下の手順でデータを変換する必要があります。
-
settings.py
に以下の設定を追加AUDITLOG_TWO_STEP_MIGRATION = True
AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT = True
- django-auditlogをバージョン3系にアップデートしてデプロイ
-
django-admin auditlogmigratejson
コマンドで2系のデータを3系のデータに変換 -
1.
で加えた設定をすべて削除するか、値をFalse
に変更してデプロイ
メンテナンス時間を置かずに上記作業を実施する場合は、古いデータは LogEntry.changes_text
でアクセスするコードに変更してください。
バージョン3系への移行についての詳細は、以下公式ドキュメントも参照してください。
Upgrading to version 3 — django-auditlog 3.0b4.post6+gac720cd documentation