Operations¶
The library gives you the maintenance functions; scheduling them is up to you (cron, a Kubernetes CronJob, Celery beat, APScheduler, ...). There is no command-line tool: a maintenance job is a short script.
A maintenance job¶
Use an AuditTrail on an engine with the maintenance role, with the same schema, severities and indexes as your application's:
"""Audit maintenance, run daily."""
import logging
import sys
from datetime import timedelta
from sqlalchemy import create_engine
from audit_trail import AuditTrail, Severity
from audit_trail.maintenance import PartitionLockTimeoutError
RETENTION = {
Severity.INFO: timedelta(days=90),
Severity.NOTICE: timedelta(days=365),
Severity.WARNING: timedelta(days=3 * 365),
Severity.CRITICAL: None, # kept forever
}
def main() -> int:
logging.basicConfig(level=logging.INFO)
audit = AuditTrail(create_engine(MAINTENANCE_DATABASE_URL), events=[])
try:
audit.maintenance.ensure_partitions(months_ahead=3)
audit.maintenance.drop_expired(RETENTION)
except PartitionLockTimeoutError:
logging.warning("a lock was not granted in time; the next run retries")
report = audit.maintenance.health()
if not report.ok:
logging.error("audit partitions need attention: %s", report)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
ensure_partitions¶
Run it at least daily, with months_ahead=3 (the default). A missed run is harmless while partitions exist for months ahead; health() flags coverage dropping below min_months_ahead (2 by default) long before writes would fail. Running it at application start as well is fine: it is idempotent, and concurrent calls are serialized. It needs the maintenance role, though, so an application connecting as app_user should leave it to the job.
drop_expired¶
Run it daily or weekly. Partitions are monthly, so a partition is dropped once its whole month is older than the retention. Schedule it outside peak hours: each detach waits for transactions that are using the audit tables, and a long-running transaction makes the call stop with PartitionLockTimeoutError after lock_timeout (5 seconds by default). That is expected; what was dropped stays dropped, and the next run continues. ensure_partitions and drop_expired take the same advisory lock, so they never run at the same time.
Every severity you use should have a key in the retention mapping, with None for "keep forever"; one that is missing is kept, with a warning in the log.
health¶
health() only reads the catalog, so any role that can see the audit tables can run it, for example a monitoring probe that alerts when report.ok is false:
- coverage below the threshold:
ensure_partitionshas not been running; pending_detach: adrop_expiredwas interrupted; the next run finishes it;orphaned: a detached table was left behind by a crash between the detach and the drop. Check the name and drop it by hand.
Logging¶
The library logs on audit_trail and its child loggers. Alert on errors from:
audit_trail.writer: audit entries that were not written, for example because a partition was missing (withon_error="log", the business transaction went on without them);audit_trail.listener: warnings about bulkUPDATE/DELETEstatements that bypassed the audit trail;audit_trail.diff:label,scopeortargetoptions that read attributes that were not loaded.
Shutdown¶
If the library built the durable engine (you did not pass durable_engine), close its pool on shutdown with audit.dispose(), or await audit.adispose() for an async engine.