Audit Service
Captures every rio-sourced domain event from EventBridge, normalizes it, stores it in DynamoDB and archives an immutable copy to S3.
Service Overview
From its README: an “event-driven audit trail service for the RIO platform. Captures domain events from EventBridge, enriches and normalizes them, stores hot records in DynamoDB, and archives immutable copies to S3.”
This service is a pure sink. It publishes nothing. There is not a single put_events call
anywhere in the repository.
It subscribes to everything
This is the important thing to understand, and it is why the receives list above is empty rather
than long: the audit service does not subscribe to named events. It subscribes with a wildcard.
Two rules, both in infrastructure/modules/lambda/template.yaml:
| Rule | Pattern | Target |
|---|---|---|
${DeployPrefix}-audit-consumer-rule (:83-95) | source: [{ "prefix": "rio" }] — no detail-type filter | ${DeployPrefix}-audit-consumer → DynamoDB |
${DeployPrefix}-archive-consumer-rule (:144-156) | source: [{ "prefix": "rio" }] — no detail-type filter | ${DeployPrefix}-archive-consumer → S3 |
So any event whose source starts with rio is captured, automatically, with no change to this
service. A new event on a new service is audited the day it ships.
EventCatalog’s receives field can only name specific messages, so it cannot express this. Read the
table above as the real contract: every rio.* event in this catalog is also received here.
What it expects the payload to look like
Because it consumes everything, this service defines the platform’s shared event vocabulary. Any event that does not fit fails validation and does not make it into the audit trail.
DomainEvent (shared/models.py:10-39) is a Pydantic model with a mode="before" validator that
lowercases domain, subdomain, entity_type, action, status and actor_type. That is why
rio-identity-service can emit status: "SUCCESS" in uppercase and still be accepted.
The allowed values (shared/constants.py) are:
| Field | Allowed values |
|---|---|
domain | core, commit, learn, act, assess, enrich, notification, platform, opportunity |
subdomain | identity, commit, quota, activity, alerts, ingestion |
entity_type | user, role, opportunity, commit, quota, hierarchy, notification, tenant, crm_sync, etl_batch, external_signal, schema_mapping, glue_job |
action | 40 values, including created, updated, deleted, assigned, submitted, adjusted, finalized, locked, stage_changed, deal_won, deal_lost, sync_started, batch_completed |
status | success, failed, rejected, in_progress, failure |
severity | info, warn, error |
actor_type | user, system, admin, crm, manager |
This is where the commit-service bug bites. The malformed
QuotaAssignedpath described on the Commit Service page producesentity_type: "commit"withaction: "quotaassigned".quotaassignedis not in theactionlist, so that event is rejected here and never reaches the audit trail.
How records are stored
Each accepted event becomes an AuditRecord (shared/models.py:42-76) — the original fields plus an
audit_id, an optional summary, changed_fields, severity, a redacted flag, the S3
archive_key and a ttl_epoch.
DynamoDB keys are built to support four lookup patterns (shared/models.py:79-110):
| Access pattern | Key |
|---|---|
| Everything in a tenant + domain on a day | PK = TENANT#{t}#DOMAIN#{d}#DAY#{YYYYMMDD} |
| History of one record | GSI1_PK = TENANT#{t}#ENTITY#{type}#{id} |
| Everything one person did | GSI2_PK = TENANT#{t}#ACTOR#{actor_id} |
| One traced request end to end | GSI3_PK = TENANT#{t}#CORR#{correlation_id} |
The sort key is TS#{occurred_at}#EVT#{audit_id}, so results come back in time order.
Storage and retention
- DynamoDB
${DeployPrefix}-audit-table— the hot store. On-demand billing, point-in-time recovery on, 90-day TTL viattl_epoch. Three GSIs, allProjectionType: ALL. - S3
${DeployPrefix}-archive— the immutable copy. AES256, versioned, moves to Glacier after 180 days,DeletionPolicy: Retain.
So audit data lives 90 days in DynamoDB for fast queries and indefinitely in S3 for compliance.
It also reads dim_person and dim_tenant_role from RDS, purely to check the caller’s
can_view_audit permission (api/core/dependencies.py:61-64).
HTTP API
| Method | Path | Purpose |
|---|---|---|
GET | /health | Health check |
GET | /tenants/{tenant_id}/audits | Query the audit trail |
The query endpoint accepts domain, day, entity_type, entity_id, actor_id,
correlation_id, action, time_from, time_to, limit (1–1000, default 100), last_key and
direction. It is guarded by validate_tenant_and_user and require_view_audit_permission.
There is no OpenAPI spec file in this repository, so no interactive explorer is attached to this service page.
Each Lambda has an SQS dead-letter queue. These are DLQs for failed invocations, not subscriptions — nothing drains them automatically.