Package: django-partitioned-audit
Creates an audit log of the modifications made to models, by using triggers.Only PostgreSql supportedModels to audit are configured inAppConfigTriggers are created automatically when runningpython manage.py migrateThe whole row is saved by the triggerTwo different implementations for audit tables (seeSimpleAuditTableManagerandAdvancedAuditTableManager)The audit table is partitionedThere are 3 partition strategies: one partition per month, one per week, one per dayManagement commandmanage_partition_tablescreates required partitionsQuick start (SimpleAuditTableManager)Add “django_partitioned_audit” to your INSTALLED_APPS setting like this:INSTALLED_APPS = [
    ...
    'django_partitioned_audit',

    'your.app1.App1Config',
    'your.app2.App2Config',
    'your.app3.App3Config',
]Runpython manage.py migrateto create the partitioned table.There is no real migration to create the partitioned table. Instead, apost_migratesignal is used.Runmanage.py manage_partition_tables create--extra-days=60to create the partitions:$ manage.py manage_partition_tables create --extra-days=60
+----------------------------------+--------------+--------------+---------------+
|  table_name                      |  from_date   |  to_date     |  status       |
+----------------------------------+--------------+--------------+---------------+
|  audit_simple_20220201_20220301  |  2022-02-01  |  2022-03-01  |  ⚠ to create  |
|  audit_simple_20220301_20220401  |  2022-03-01  |  2022-04-01  |  ⚠ to create  |
|  audit_simple_20220401_20220501  |  2022-04-01  |  2022-05-01  |  ⚠ to create  |
+----------------------------------+--------------+--------------+---------------+You should run that command periodically, to make sure the database always contains a partition where to insert data.Register the models you want to audit in yourAppConfiginstances:from django.apps import AppConfig
class MyAppConfig(AppConfig):
    name = "myapp"
    trigger_audit_models = (
        'Model1',
        'Model2',
        'Model3',
    )Runpython manage.py migrateto create the triggers.Quick start (AdvancedAuditTableManager)There is a second implementation of the audit table that can be used, based on:https://wiki.postgresql.org/wiki/Audit_trigger_91plushttps://github.com/2ndQuadrant/audit-triggerThe code can be seen atdjango_partitioned_audit.audit_table.advanced_audit_table_manager.AdvancedAuditTableManager.To use this implementation, you can reference that class insettings.py:DPA_AUDIT_TABLE_MANAGER = "django_partitioned_audit.audit_table.advanced_audit_table_manager.AdvancedAuditTableManager"and follow the steps described above, atQuick start (SimpleAuditTableManager).This implementation also auditTRUNCATEoperations.Security of audit entriesDBA should correctly configure permissions, in a way that the user used to
connect to the database from Django has permissions to INSERT rows in thetrigger_audit_entriestable, but NO permissions to UPDATE / DELETE them.How it worksA trigger is executed with each insert/update/delete operationthe database trigger is created bypython manage.py migrate.only for the tables associated to the models that are explicitly
specified in yourAppConfig.The trigger creates a new row intrigger_audit_entriestable containing:object_table: table where the modification happened (one of your models)object_payload: JSON representation of the whole row (after modification)audit_entry_created: timestampaudit_txid_current: PostgreSql TXID in which the modification occurredaudit_operation: operation:INSERT,UPDATE,DELETETriggerThe solution is very simple in terms of code running in PostgreSQL: just a trigger that calls a function.The trigger just invokes the function for eachINSERT,UPDATE,DELETECREATE TRIGGER trigger_audit_entry_creator_trigger
    AFTER INSERT OR UPDATE OR DELETE ON {table_name}
    FOR EACH ROW EXECUTE FUNCTION trigger_audit_entry_creator_func_v2();The function just serializes the row into a JSON and insert it in the audit table:CREATE FUNCTION trigger_audit_entry_creator_func_v2() RETURNS TRIGGER AS $scr$
    DECLARE
        object_payload  varchar;
    BEGIN
        IF (TG_OP = 'INSERT') THEN
            object_payload  = row_to_json(NEW);
        ELSIF (TG_OP = 'UPDATE') THEN
            object_payload  = row_to_json(NEW);
        ELSIF (TG_OP = 'DELETE') THEN
            object_payload  = row_to_json(OLD);
        ELSE
            RAISE EXCEPTION 'Unexpected TG_OP = %', TG_OP;
        END IF;

        INSERT INTO audit_simple (
                object_table,
                object_payload,
                audit_entry_created,
                audit_txid_current,
                audit_operation
            )
            SELECT
                TG_TABLE_NAME,
                object_payload,
                now(),
                txid_current(),
                TG_OP;
        RETURN NULL;
    END;
$scr$ LANGUAGE plpgsql;Management of partitionsThe Django custom management commandmanage_partition_tablescan be used to manage the partitions.Sample usageIf you want to have enough partition to handle next 90 days (around 3 months), you can use–extra-days=90.
Because it’s the first time we run the command, no partition exists, and the plan will report that all
partitions need to be created:$ manage.py manage_partition_tables simulate --extra-days=90
+----------------------------------+--------------+--------------+---------------+
|  table_name                      |  from_date   |  to_date     |  status       |
+----------------------------------+--------------+--------------+---------------+
|  audit_simple_20220201_20220301  |  2022-02-01  |  2022-03-01  |  ⚠ to create  |
|  audit_simple_20220301_20220401  |  2022-03-01  |  2022-04-01  |  ⚠ to create  |
|  audit_simple_20220401_20220501  |  2022-04-01  |  2022-05-01  |  ⚠ to create  |
|  audit_simple_20220501_20220601  |  2022-05-01  |  2022-06-01  |  ⚠ to create  |
+----------------------------------+--------------+--------------+---------------+We can also see the plan if no extra days are requested (this way, we’ll only create partitions for
the current month:$ manage.py manage_partition_tables simulate --extra-days=0
+----------------------------------+--------------+--------------+---------------+
|  table_name                      |  from_date   |  to_date     |  status       |
+----------------------------------+--------------+--------------+---------------+
|  audit_simple_20220201_20220301  |  2022-02-01  |  2022-03-01  |  ⚠ to create  |
+----------------------------------+--------------+--------------+---------------+Now let’s create the partitions:$ manage.py manage_partition_tables create --extra-days=0
+----------------------------------+--------------+--------------+---------------+
|  table_name                      |  from_date   |  to_date     |  status       |
+----------------------------------+--------------+--------------+---------------+
|  audit_simple_20220201_20220301  |  2022-02-01  |  2022-03-01  |  ⚠ to create  |
+----------------------------------+--------------+--------------+---------------+If we run the command and we pass–extra-days=90, the partition for the current month already exists, and
only partitions for next months (to cover 90 days) will be created:$ manage.py manage_partition_tables create --extra-days=90
+----------------------------------+--------------+--------------+----------------+
|  table_name                      |  from_date   |  to_date     |  status        |
+----------------------------------+--------------+--------------+----------------+
|  audit_simple_20220201_20220301  |  2022-02-01  |  2022-03-01  |  ✓ exists      |
|  audit_simple_20220301_20220401  |  2022-03-01  |  2022-04-01  |  ❌ to create  |
|  audit_simple_20220401_20220501  |  2022-04-01  |  2022-05-01  |  ❌ to create  |
|  audit_simple_20220501_20220601  |  2022-05-01  |  2022-06-01  |  ❌ to create  |
+----------------------------------+--------------+--------------+----------------+We can uselistto list existing partitions:$ manage.py manage_partition_tables list
+----------------------------------+--------------+--------------+
|  table_name                      |  from_date   |  to_date     |
+----------------------------------+--------------+--------------+
|  audit_simple_20220201_20220301  |  2022-02-01  |  2022-03-01  |
|  audit_simple_20220301_20220401  |  2022-03-01  |  2022-04-01  |
|  audit_simple_20220401_20220501  |  2022-04-01  |  2022-05-01  |
|  audit_simple_20220501_20220601  |  2022-05-01  |  2022-06-01  |
+----------------------------------+--------------+--------------+Partition per weekWe can use one partition per week:$ manage.py manage_partition_tables create --extra-days=30 --time-range-generator=WeeklyTimeRangeGenerator
+----------------------------------+--------------+--------------+---------------+
|  table_name                      |  from_date   |  to_date     |  status       |
+----------------------------------+--------------+--------------+---------------+
|  audit_simple_20220222_20220301  |  2022-02-22  |  2022-03-01  |  ⚠ to create  |
|  audit_simple_20220301_20220308  |  2022-03-01  |  2022-03-08  |  ⚠ to create  |
|  audit_simple_20220308_20220315  |  2022-03-08  |  2022-03-15  |  ⚠ to create  |
|  audit_simple_20220315_20220322  |  2022-03-15  |  2022-03-22  |  ⚠ to create  |
|  audit_simple_20220322_20220329  |  2022-03-22  |  2022-03-29  |  ⚠ to create  |
+----------------------------------+--------------+--------------+---------------+Partition per dayWe can use one partition per day:$ manage.py manage_partition_tables create --extra-days=10 --time-range-generator=DailyTimeRangeGenerator
+----------------------------------+--------------+--------------+---------------+
|  table_name                      |  from_date   |  to_date     |  status       |
+----------------------------------+--------------+--------------+---------------+
|  audit_simple_20220222_20220223  |  2022-02-22  |  2022-02-23  |  ⚠ to create  |
|  audit_simple_20220223_20220224  |  2022-02-23  |  2022-02-24  |  ⚠ to create  |
|  audit_simple_20220224_20220225  |  2022-02-24  |  2022-02-25  |  ⚠ to create  |
|  audit_simple_20220225_20220226  |  2022-02-25  |  2022-02-26  |  ⚠ to create  |
|  audit_simple_20220226_20220227  |  2022-02-26  |  2022-02-27  |  ⚠ to create  |
|  audit_simple_20220227_20220228  |  2022-02-27  |  2022-02-28  |  ⚠ to create  |
|  audit_simple_20220228_20220301  |  2022-02-28  |  2022-03-01  |  ⚠ to create  |
|  audit_simple_20220301_20220302  |  2022-03-01  |  2022-03-02  |  ⚠ to create  |
|  audit_simple_20220302_20220303  |  2022-03-02  |  2022-03-03  |  ⚠ to create  |
|  audit_simple_20220303_20220304  |  2022-03-03  |  2022-03-04  |  ⚠ to create  |
|  audit_simple_20220304_20220305  |  2022-03-04  |  2022-03-05  |  ⚠ to create  |
+----------------------------------+--------------+--------------+---------------+TestTested on:Python 3.8, 3.9, 3.10, 3.11Django 3.2, 4.1, 4.2PostgreSql 12, 13, 14, 15Known issuesJAdvancedAuditTableManager: SONB is used as intermediary format in the view & Django models, should be migrated to HSTOREAudit table and other objects are not managed by using Django migrationsNot tested with psycopg3Coupled to Django (would be nice if Django is supported but possible to use it without Django)Works only on default db schemaLack feature: remove old partitionsTODORefactor responsibilities onPartitionManagerandAuditTableManager