Story AUDIT-003: Compliance-report stub (degraded CSV export) + ingest metrics¶
Module: audit-log Slice: AUDIT-003 (Architecture §11) Side: [BACKEND] + [ADMIN] Version target: V0.1.0 Priority: 6 Depends on: AUDIT-001a (audit_log + audit_export tables exist), AUDIT-001b (outbox/builder for the self-event + reconciliation), AUDIT-002 (real data exists to export) Can develop concurrently with: None Merge order: After AUDIT-002 Estimated complexity: M PRD User Stories: AUDIT-US-04 (export — degraded V0 subset), AUDIT-US-08 (self-auditing — export part), AUDIT-US-09 (ingest health — metrics-only stub) Wireframe: N/A — backend only (no UI; platform admin calls the API directly)
Objective¶
Put down the V0.1.0 compliance-report stub: a POST /api/control/audit/exports endpoint that produces a degraded export — a flat CSV per tenant + date range, streamed to MinIO and returned as a presigned URL — plus a GET to poll status and download. No manifest, no SHA-256, no JSONL in V0 (that is the V1 full version, AUDIT-006). Also expose the Prometheus ingest-health metrics and emit the AUDIT_RECONCILED self-event when the outbox drains. Alerting rules are NOT wired here (V1, AUDIT-009) — V0 is log-only/metrics-only.
Backend Scope¶
Entities¶
- Fills
audit_exportrows (table created in AUDIT-001a): status flows PENDING → RUNNING → COMPLETED|FAILED, plusevent_count,jsonl_object_key(reused for the CSV key in V0),error_message, timestamps.
Migrations¶
None new (the audit_export table ships in AUDIT-001a).
Service Layer¶
All under root package com.altarys.papillon.pcs.controlplane.audit:
- AuditExportController (...audit.web) — POST /api/control/audit/exports (returns 202 + exportId + pollUrl) and GET /api/control/audit/exports/{id} (status + presigned download URL). Auth: Keycloak JWT role papillon-platform-admin.
- CsvExportWorker (...audit.internal) — async job. Cursor-scans audit_log for the requested tenant_id + date range (using the (tenant_id, business_committed_at DESC) index), streams a flat CSV to the MinIO audit-exports bucket, flips audit_export.status = COMPLETED, and fires the AUDIT_EXPORT_COMPLETED self-event. On failure: status FAILED + error_message; no partial bundle persisted (ops watcher GC; Architecture §5.4).
- AuditMetrics (...audit.internal) — Micrometer instruments (Architecture §10.4): audit_outbox_incomplete_count (gauge), audit_oldest_incomplete_age_seconds (gauge), audit_freshness_seconds (histogram by producer_module), audit_ingest_failures_total (counter), audit_export_duration_seconds, audit_export_bytes_total. Scrapeable by Prometheus.
- Reconciliation self-event: when the spring-modulith republisher (wired in AUDIT-001b) drains the incomplete-count back to 0, emit AUDIT_RECONCILED with the drained count.
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
POST |
/api/control/audit/exports |
{tenantId (req), dateRangeStart (req), dateRangeEnd (req), eventTypes?, correlationId?, caseKey?} |
202 {exportId, status:"PENDING", pollUrl} |
papillon-platform-admin |
GET |
/api/control/audit/exports/{id} |
— | 200 {exportId, status, scope, eventCount, download:{csvUrl, expiresAt}} |
papillon-platform-admin |
Validation Rules¶
- AC-04.3:
tenantId,dateRangeStart,dateRangeEndare required; no export without atenantId(no unbounded cross-tenant export). - Presigned URL TTL =
AUDIT_EXPORT_PRESIGN_TTL_SECONDS(default 900 s / 15 min); a fresh GET regenerates a fresh URL. - V0 degradation is explicit: CSV only, no
manifest.json, nojsonl_sha256. The response must not claim integrity verification it does not provide.
Multi-Tenant Considerations¶
- Export is strictly tenant-scoped via the required
tenantId(FR-04). The cursor scan filters ontenant_id; no cross-tenant rows can appear in a bundle. - Works identically for SHARED and BYO tenants because all audit rows live on the platform DB (AUDIT-ADR-01).
Audit & Logging¶
- Emits
AUDIT_EXPORT_COMPLETED(PRD §B.1, AC-04.5 / AC-08.2) when an export completes, carrying{export_id, scope, event_count, requested_by}(nojsonl_sha256in V0). - Emits
AUDIT_RECONCILED(AC-01.4 / AC-09.3) when the outbox drains to zero. - (V0 does NOT emit
AUDIT_QUERY_EXECUTED— the query API is V1.)
Frontend Scope¶
N/A — backend only. Platform admin invokes the API directly; no operator/admin console screen in V0 (PRD §C.5).
Acceptance Criteria¶
AC-04.5 / AC-08.2: Export is itself audited
Given a platform admin triggers an export and it completes
When the worker finishes writing the bundle
Then AUDIT writes one AUDIT_EXPORT_COMPLETED self-event with {export_id, scope, event_count, requested_by}
AC-003.1: Degraded CSV export round-trip (derived from Architecture §11 AUDIT-003 acceptance)
Given a tenant with audit rows and a date range
When the admin POSTs an export and polls the GET until COMPLETED
Then a flat CSV is downloadable via a presigned URL, the URL expires 15 minutes after issuance, and the row count matches the scope
And an export of 1 000 events completes within 5 seconds
AC-01.4: Reconciliation drains and is recorded
Given entries queued during an ingest-degradation window
When AUDIT recovers and the republisher drains the queue to zero
Then AUDIT emits AUDIT_RECONCILED with the drained count
And audit_outbox_incomplete_count is scrapeable by Prometheus and returns to 0
Compliance Rules¶
- CIMA Reg. 01-24 §16.5 + CI fiscal retention — the export is the mechanism by which platform admin hands a regulator/lawyer a slice of the proof base. V0 ships a degraded (non-integrity-verified) CSV; the integrity-verifiable JSONL+manifest+SHA-256 version is V1 (AUDIT-006) — this limitation must be documented so the §16.5 claim is not over-stated (AC-05.4 honest scope).
- AUDIT-US-09 alerting — V0 exposes metrics only; the Grafana alert rules + on-call routing are V1.
[LEGAL-REVIEW]not newly triggered; retention-durationPRÉLIMINAIREnote from memo F5 still applies to any fixed log duration.
Standards & Conventions¶
docs/standards/api-guidelines.md— response envelope, cursor pagination,X-API-Versionheader convention.docs/standards/java-spring-guidelines.md— async worker, controller, named-interface beans.docs/standards/multi-datasource-patterns.md— platform-DB scan for all tenants.docs/standards/critical-rules.md— rule 1 (tenant scoping on the export scan), rule 3 (catalog:AUDIT_EXPORT_COMPLETED,AUDIT_RECONCILED).
Testing Requirements¶
Unit Tests¶
AuditExportControllerrejects a POST missingtenantId/ date range (AC-04.3).CsvExportWorkerflips status correctly on success and on failure.
Integration Tests¶
- AC-003.1: POST an export for 1 000 events → poll GET until COMPLETED (within 5 s) → download CSV via presigned URL → assert row count → assert URL is dead after 15 min.
- AC-04.5: assert exactly one
AUDIT_EXPORT_COMPLETEDrow after a completed export. - AC-01.4: simulate an ingest-degradation window, queue entries, resume, assert the queue drains and an
AUDIT_RECONCILEDrow appears with the right count; assertaudit_outbox_incomplete_countreturns to 0. - Tenant isolation: an export scoped to tenant A contains zero tenant-B rows.
What QA Will Validate¶
- Degraded CSV export works end-to-end and is tenant-scoped.
- Export and reconciliation self-events are written.
- Ingest metrics are scrapeable (alert rules deliberately absent in V0).
Out of Scope¶
- Full JSONL +
manifest.json+ SHA-256 integrity export (V1, AUDIT-006). - Filtered query API +
AUDIT_QUERY_EXECUTEDdedup self-event (V1, AUDIT-004). - By-case lookup + correlation expansion (V1, AUDIT-005).
- Active
AUDIT_INGEST_DEGRADEDalerting + Grafana rules + on-call routing (V1, AUDIT-009). papillon_audit_readerrole consumption by a read pool (V1, AUDIT-007).- Any UI / console screen (V1+; tenant-side viewer is V2).
- Automatic 10-year purge (V2).
Definition of Done¶
- [ ]
POST /api/control/audit/exports(202) +GET /exports/{id}implemented underpapillon-platform-admin - [ ] Degraded CSV streamed to the
audit-exportsMinIO bucket; presigned URL expires after 15 min - [ ] Export of 1 000 events completes within 5 s (AC-003.1)
- [ ]
AUDIT_EXPORT_COMPLETEDself-event written on completion (AC-04.5 / AC-08.2) - [ ]
AUDIT_RECONCILEDemitted on drain-to-zero; ingest metrics scrapeable by Prometheus (AC-01.4) - [ ] Export rejects a missing
tenantId/ date range (AC-04.3) - [ ] Tenant isolation verified (export scan scoped by
tenant_id; both V1 DB profiles) - [ ] V0 degradation documented (no manifest/SHA-256) so the §16.5 claim is not over-stated
- [ ] No silent failures: a worker crash flips status to FAILED with an error message, no partial bundle served