service

Audit Service

Captures every rio-sourced domain event from EventBridge, normalizes it, stores it in DynamoDB and archives an immutable copy to S3.

Service

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:

RulePatternTarget
${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:

FieldAllowed values
domaincore, commit, learn, act, assess, enrich, notification, platform, opportunity
subdomainidentity, commit, quota, activity, alerts, ingestion
entity_typeuser, role, opportunity, commit, quota, hierarchy, notification, tenant, crm_sync, etl_batch, external_signal, schema_mapping, glue_job
action40 values, including created, updated, deleted, assigned, submitted, adjusted, finalized, locked, stage_changed, deal_won, deal_lost, sync_started, batch_completed
statussuccess, failed, rejected, in_progress, failure
severityinfo, warn, error
actor_typeuser, system, admin, crm, manager

This is where the commit-service bug bites. The malformed QuotaAssigned path described on the Commit Service page produces entity_type: "commit" with action: "quotaassigned". quotaassigned is not in the action list, 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 patternKey
Everything in a tenant + domain on a dayPK = TENANT#{t}#DOMAIN#{d}#DAY#{YYYYMMDD}
History of one recordGSI1_PK = TENANT#{t}#ENTITY#{type}#{id}
Everything one person didGSI2_PK = TENANT#{t}#ACTOR#{actor_id}
One traced request end to endGSI3_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 via ttl_epoch. Three GSIs, all ProjectionType: 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

MethodPathPurpose
GET/healthHealth check
GET/tenants/{tenant_id}/auditsQuery 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.

Inbound and Outbound Message Flow

Event-driven architecture documentation: RIO