Disciplines · Operations

Log Retention Policy

Only document overrides here; the tier above is the default.

5sections3 minread

On this page

Owner: platform team Last reviewed: 2026-05-28

This doc specifies how long logs are kept by tier and by domain, and what triggers a purge. It exists because:

  • Compliance audits want to know retention windows up front.
  • On-call needs to know how far back they can grep when triaging long-tail issues.
  • DBAs need to know what storage to provision for log indexes.

Tiers#

Tier Retention Storage Notes
Application (info+) 7 days CloudWatch / Loki hot Service-level events, healthy operation
Application (debug) 24 hours CloudWatch / Loki hot Only on when a debug flag is toggled
Error 30 days CloudWatch / Loki hot + S3 cold copy at 7d Stack traces, 5xx responses, unhandled exceptions
Audit (security) 90 days hot, 7 years cold Elasticsearch / S3 (Glacier) Tamper-evident; never editable in place
Access (request) 14 days CloudWatch / Loki hot HTTP request line, status, duration
Audit (admin) 365 days hot, 7 years cold Elasticsearch / S3 (Glacier) Privileged-user actions, immutable
Privacy events (consent, deletion) 7 years Elasticsearch / S3 (Glacier) GDPR Article 30 record-of-processing

Per-domain retention overrides#

Only document overrides here; the tier above is the default.

Domain Override Why
aphrodite/payment Audit logs at 10 years hot/cold PCI scope, financial records
lilith/svc-auth Login audit kept 180 days hot Investigations on suspicious-login chains often span >90 days
iris/api memory snapshots Per-user-controlled retention via consent records Users opt-in to a retention window between 24h and 12mo

Purge mechanism#

  • CloudWatch / Loki hot tier — automatic by log-group retention setting. Set via Terraform; never via console.
  • S3 cold tier — lifecycle rule per bucket: transition to Glacier at the retention boundary; delete at the audit boundary.
  • Elasticsearch — ILM policy per index alias, deletes the closed index when older than retention.
  • Postgres audit_log / audit_logs tables (bellona, hathor, isis, sophia, yemaya) — the sweeper at scripts/retention/audit-log-retention.ts deletes rows older than the configured retention window. Defaults to the "Audit (security)" 90-day tier above; per-domain overrides go through <DOMAIN>_AUDIT_RETENTION_DAYS (e.g. LILITH_AUDIT_RETENTION_DAYS=180 for the svc-auth override). Cron schedule: 15 3 * * * npx tsx scripts/retention/audit-log-retention.ts --apply. The script defaults to --dry-run; the cron expression must include --apply explicitly. It batches DELETEs (10_000 rows / batch with a 100ms pause) so it never holds a long table lock, and exits non-zero if any configured domain hit a SQL error so the cron alert routing fires.
  • Oshun BFF admin_audit_event — retained for the canonical 2,555-day audit_event compliance window. The BFF evaluates expiry on startup and at least daily, and defers deletion while a referenced actor, target, subject, tenant, or owner has an active legal hold or retention exception. Successful reads require admin:*, write and durably flush an audit.access.* event, and are HMAC-SHA-256 chained with a production key held outside PostgreSQL. The operator-readable contract is GET /v1/admin/audit-log/policy; the machine-reviewed control record is docs/audits/eve-sota-retention-access-policy/2026-09-15.json.
  • Tombstone audit events (oshun_v1_tombstone_audit_events) — these are governed by the privacy-event tier (7 years) and pruned by the privacy-team workflow, not by the retention cron above. Consent records are never deleted by the sweeper.

What is NOT a "log" under this policy#

  • Database row-level audit columns (created_at, updated_at, deleted_at) — these are part of the schema retention policy, not log retention.
  • User-generated content (posts, recordings, profile fields) — governed by the per-domain data-retention rules in docs/compliance/data-retention.md (TBD).
  • Backup files — governed by docs/runbooks/database-backups.md (Postgres) and docs/runbooks/object-storage-backups.md (TBD, task #108).

Adding a new log stream#

  1. Tag every event with the tier name (app.info, audit.security, etc.) at emit time so the routing layer can apply the right retention.
  2. If the stream needs an override, add it to the table above in the same PR that introduces the stream.
  3. If the override is more permissive than the default (longer retention), confirm with the platform team — the budget impact is real.