3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Djangoのモデル操作を記録できるdjango-auditlogの紹介

Last updated at Posted at 2024-03-17

企業向けのシステム開発をしていると、コンプライアンス上の理由からシステムの操作記録やデータの変更内容を記録したいという要件はよくあります。そういったときに役立つ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.pyMIDDLEWARE"auditlog.middleware.AuditlogMiddleware"を加えます。リクエストを変更するミドルウェアは"auditlog.middleware.AuditlogMiddleware"より上に書いてください。

MIDDLEWARE = [
    # 省略(リクエストを変更するミドルウェアはここに書く)
    "auditlog.middleware.AuditlogMiddleware",
    # 省略
]

使い方

ここでは、booksアプリケーションに以下のモデルを定義している前提でdjango-auditlogの使い方について説明します。

books/models.py
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の監査ログです。

Site_administration___Django_site_admin.jpg

admin上でBookモデルの登録・更新・削除を行ってから、http://127.0.0.1:8000/admin/auditlog/logentry/を開いてください。モデルの操作履歴が表示されているはずです。

Select_log_entry_to_change___Django_site_admin_と_ryu22e_macbook-pro-4__Users_ryu22e_development_django_auditlog_example.jpg

一部のモデルやフィールドの操作を記録したくない場合は、AUDITLOG_EXCLUDE_TRACKING_FIELDSAUDITLOG_EXCLUDE_TRACKING_FIELDSを追記します。

# 記録しないモデル
AUDITLOG_EXCLUDE_TRACKING_MODELS = (
    "accounts.User",
)
# 記録しないフィールド
AUDITLOG_EXCLUDE_TRACKING_FIELDS = (
    "created",
    "modified"
)

モデルの閲覧記録を確認する

閲覧記録の場合は、ビューのコードに手を加える必要があります。
以下のようにクラスビューにauditlog.mixins.LogAccessMixinクラスを継承させます。

books/views.py
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()メソッドに渡します。

books/views.py
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からはデータの扱いが変わるため、以下の手順でデータを変換する必要があります。

  1. settings.pyに以下の設定を追加
    1. AUDITLOG_TWO_STEP_MIGRATION = True
    2. AUDITLOG_USE_TEXT_CHANGES_IF_JSON_IS_NOT_PRESENT = True
  2. django-auditlogをバージョン3系にアップデートしてデプロイ
  3. django-admin auditlogmigratejsonコマンドで2系のデータを3系のデータに変換
  4. 1.で加えた設定をすべて削除するか、値をFalseに変更してデプロイ

メンテナンス時間を置かずに上記作業を実施する場合は、古いデータは LogEntry.changes_text でアクセスするコードに変更してください。

バージョン3系への移行についての詳細は、以下公式ドキュメントも参照してください。
Upgrading to version 3 — django-auditlog 3.0b4.post6+gac720cd documentation

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?