symfonic.core.observability.audit¶
audit ¶
Audit logger — pluggable hook for destructive-operation telemetry.
Gate 3 of v5.5.0. Provides a process-wide hook that the FastAPI
routers invoke after every destructive operation (delete, update,
bulk-delete, export, erase). Matches the patterns already established
by :func:~symfonic.agent.fastapi.dependencies.set_tenant_auth_verifier
(5.4.47) and :func:~symfonic.core.observability.budget.set_budget_tracker
(5.4.49):
- Library-level default is "no logger registered" — zero overhead, zero behavioural change for pre-existing agents.
- Scaffolded apps register a Postgres-backed implementation in the lifespan hook so every destructive op is captured for GDPR / SOC 2 audit trails.
- Fire-and-forget dispatch from hot paths — audit store failure MUST NOT crash the user's request.
Why a separate file (not budget.py)? Single-responsibility + the
300-LOC cap. Audit and budget share the hook pattern but otherwise
have nothing in common.
AuditLogEntry ¶
Bases: BaseModel
Immutable audit event emitted for every destructive operation.
Fields intentionally kept small so both in-memory and Postgres
implementations can store them without a schema migration dance.
metadata is the escape hatch for per-action context (counts,
previous value, confirmation token, ...).
action
class-attribute
instance-attribute
¶
Free-form action code. Convention: snake_case verb +
optional resource suffix, e.g. delete_memory, bulk_delete,
export_data, erase_all.
resource_type
class-attribute
instance-attribute
¶
E.g. memory_node, edge, procedure, tenant.
to_dict ¶
Return a JSON-safe dict representation.
Source code in src/symfonic/core/observability/audit.py
AuditLogger ¶
Bases: Protocol
Destination for audit events.
Implementations MAY persist synchronously or asynchronously. The contract is "record this entry, or raise" — retry / batching is an implementation detail. Callers in the router layer shield failures (fire-and-forget) so a sick audit store never 500s a user request.
InMemoryAuditLogger ¶
Default in-process logger — keeps entries in an in-memory list.
Primarily for unit tests and local development. Production
deployments should register a durable implementation (e.g. the
scaffolded Postgres logger). The list is bounded by
max_entries to prevent unbounded growth when no one is draining
it.
Source code in src/symfonic/core/observability/audit.py
drain
async
¶
Return all buffered entries, clearing the buffer.
Tests use this to assert what was emitted during a request.
Source code in src/symfonic/core/observability/audit.py
entries ¶
Return a non-draining snapshot (sync, for inspection).
Not guaranteed to be consistent under concurrent record()
calls — prefer drain() in assertions.
Source code in src/symfonic/core/observability/audit.py
record
async
¶
Append entry to the in-memory buffer (drops oldest when full).
Source code in src/symfonic/core/observability/audit.py
emit_audit_event ¶
Schedule an audit write without blocking the caller.
Never raises — failures are logged. If no logger is registered the call is a complete no-op (zero allocations beyond the entry itself).
The FastAPI routers use this directly in handlers so a slow / sick audit backend can never 500 a user request. The task is scheduled on the currently-running loop; when called outside an async context (defensive guard) we log a warning and drop the entry — no audit logger is expected to live that far up the stack.
Source code in src/symfonic/core/observability/audit.py
get_audit_logger ¶
set_audit_logger ¶
Register (or clear) the process-wide audit logger.
Call once at app startup. Passing None disables audit emission
— useful for tests that registered a logger and want to tear down.