Skip to content
GitHub

Architecture

Internal design of the lexigram-audit package.


flowchart BT
    App[Application Layer<br/>Controllers · CLI · Admin Panels]
    Audit[lexigram-audit<br/>AuditLogger · AuditVerifier · AuditPurger<br/>PolicyBasedRetention · AuditScheduler]
    Contracts[lexigram-contracts<br/>AuditLoggerProtocol · AuditStoreProtocol<br/>AuditVerifierProtocol · RetentionPolicyProtocol<br/>AuditEntry · AuditQuery · AuditEventSeverity]
    Core[lexigram Core<br/>DI · Config · Events · Hooks · Scheduling]
    Backends[Backend Stores<br/>SqlAuditStore · InMemoryAuditStore]

    App -->|resolves / injects| Audit
    Audit -->|implements| Contracts
    Audit -->|persists to| Backends
    Contracts --> Core
    Backends -->|DatabaseProviderProtocol| Core

Dependency direction: Arrows point toward the dependency. Audit consumes contracts and core; the application consumes audit. Backend stores are swappable implementations behind AuditStoreProtocol.


AuditEntry is the canonical event shape defined in lexigram.contracts.audit.types:

@dataclass(frozen=True)
class AuditEntry:
action: str # "user.update"
actor_id: str # "user-abc123"
resource_type: str # "User"
resource_id: str # "user-abc123"
outcome: str # "success" | "failure" | "partial"
severity: AuditEventSeverity # LOW | MEDIUM | HIGH | CRITICAL
occurred_at: datetime # UTC timestamp
metadata: dict[str, Any] # Arbitrary context
old_values: dict | None # Before snapshot
new_values: dict | None # After snapshot
source: str # "sql", "admin", "ai", ...
tenant_id: str | None # Multi-tenant scope
SeverityMeaningDefault Retention
LOWRoutine operations (read, list)365 days
MEDIUMNotable events (create, update)365 days
HIGHSecurity-sensitive (auth failure, data access)1095 days (3 yr)
CRITICALCompliance-critical (delete, impersonation)2555 days (7 yr)
PrefixDomain
user.*User management
auth.*Authentication / authorization
data.*Data lifecycle
config.*Configuration changes
audit.*Meta-audit (purge, verification)
admin.*Administrative operations

sequenceDiagram
    participant App as Application
    participant Logger as AuditLogger
    participant Store as AuditStore (SQL/Memory)
    participant Verifier as AuditVerifier
    participant Purger as AuditPurger

    App->>Logger: log(entry)
    Logger->>Logger: Apply retention expiry
    Logger->>Store: append(entry)
    Store-->>Logger: OK (fire-tolerant)
    Logger-->>App: None (never raises)

    App->>Logger: query(query)
    Logger->>Store: query(query)
    Store-->>Logger: list[AuditEntry]
    Logger-->>App: list[AuditEntry]

    App->>Verifier: verify_recent(limit=100)
    alt HMAC key configured
        Verifier->>Store: query(limit)
        Store-->>Verifier: entries
        Verifier->>Verifier: recompute checksums
        Verifier-->>App: list[AuditMismatch]
    else No HMAC key
        Verifier-->>App: [] (no-op)
    end

    Purger->>Purger: purge_expired()
    Purger->>Store: query(all)
    Purger->>Purger: evaluate each entry vs policy
    Purger->>Logger: meta-audit entry
    Purger-->>App: purged count

Fire-tolerance: AuditLogger.log() catches every exception and emits a warning. Audit failure must never block the operation that triggered it — this is enforced at the protocol level.


BackendClassDependenciesPersistenceChecksumsInitialization
SQLSqlAuditStoreDatabaseProviderProtocolRelational DBHMAC-SHA256 (optional)Auto-creates table + indexes
MemoryInMemoryAuditStoreNoneBounded deque (10k entries)N/ANone

The backend is selected at configuration time via AuditConfig.store_backend ("sql" or "memory"). The AuditCoreProvider registers the appropriate implementation:

if config.store_backend == "sql":
try:
container.singleton(AuditStoreProtocol, SqlAuditStore)
except ImportError:
container.singleton(AuditStoreProtocol, InMemoryAuditStore)
else:
container.singleton(AuditStoreProtocol, InMemoryAuditStore)
ColumnTypePurpose
idSERIAL / INTEGER PKAuto-increment ID
table_nameVARCHAR(255)Resource type
entity_idVARCHAR(255)Resource ID
actionVARCHAR(50)Action verb
old_valuesTEXTBefore-state JSON
new_valuesTEXTAfter-state JSON
changed_byVARCHAR(255)Actor ID
changed_atTIMESTAMPEvent timestamp
metadataTEXTArbitrary JSON
checksumVARCHAR(64)HMAC-SHA256 hex digest
severityVARCHAR(20)Severity level
sourceVARCHAR(100)Originating subsystem
outcomeVARCHAR(50)Success / failure
tenant_idVARCHAR(255)Multi-tenant scope

Three indexes: (table_name, entity_id), (changed_at), (checksum).


sequenceDiagram
    participant App as Application
    participant Bundle as AuditBundleProvider
    participant Core as AuditCoreProvider
    participant Retention as AuditRetentionProvider
    participant Verifier as AuditVerifierProvider
    participant Sched as AuditSchedulingProvider
    participant Admin as AuditAdminProvider

    App->>Bundle: register(container)
    activate Bundle
    Bundle->>Core: register(container)
    Core->>Core: singletons: AuditConfig, AuditStoreProtocol, AuditLoggerProtocol
    Bundle->>Retention: register(container)
    Retention->>Retention: singletons: RetentionPolicyProtocol, AuditPurger
    Bundle->>Verifier: register(container)
    alt HMAC key set
        Verifier->>Verifier: singleton: AuditVerifierProtocol
    else No HMAC key
        Verifier-->>Verifier: skip (verifier unavailable)
    end
    Bundle->>Sched: register(container)
    Sched->>Sched: singleton: AuditVerifierSchedulerProtocol
    Bundle->>Admin: register(container) [if enabled]
    Admin->>Admin: singleton: AuditAdminContributor
    deactivate Bundle

    App->>Bundle: boot(container)
    activate Bundle
    Bundle->>Core: boot(container)
    Core->>Core: store.initialize() (creates tables)
    deactivate Bundle
Sub-providerPriorityregister() bindsboot()
AuditCoreProviderINFRASTRUCTUREAuditConfig, AuditStoreProtocol (SQL/Memory), AuditLoggerProtocolstore.initialize()
AuditRetentionProviderINFRASTRUCTURERetentionPolicyProtocol, AuditPurger
AuditVerifierProviderINFRASTRUCTUREAuditVerifierProtocol (only if hmac_key is set)
AuditSchedulingProviderINFRASTRUCTUREAuditVerifierSchedulerProtocol
AuditAdminProviderLOWAuditAdminContributor

All protocols and shared types live in lexigram.contracts.audit:

ContractTypeMethods / Fields
AuditLoggerProtocolProtocollog(entry), query(query)
AuditStoreProtocolProtocolappend(entry), query(query), count(query)
AuditVerifierProtocolProtocolverify_recent(*, limit), verify_entry(entry_id)
RetentionPolicyProtocolProtocolevaluate(entry), get_expiry(entry)
AuditVerifierSchedulerProtocolProtocolregister_handler(task_provider), schedule(task_provider, ...)
AuditEntryDataclassFrozen event record with action, actor, severity, metadata
AuditQueryDataclassComposable filters (actor_id, action, severity, time range, etc.)
AuditEventSeverityStrEnumLOW, MEDIUM, HIGH, CRITICAL
AuditMismatchDataclassChecksum mismatch record
RetentionDecisionStrEnumRETAIN, RETAIN_UNTIL, ARCHIVE, PURGE
RetentionPolicyDataclassRetention rules: defaults, severity/source overrides

Additional framework contracts used internally:

ContractUse
DatabaseProviderProtocolSQL store executes queries through this
BaseAdminContributorAdmin panel integration
EventBusProtocolDomain event emission (AuditEntryLoggedEvent, etc.)
HookRegistryProtocolLifecycle hook payloads
ContainerRegistrarProtocol / BootContainerProtocolDI lifecycle

src/lexigram/audit/
├── __init__.py # Lazy-export public surface (12 symbols)
├── module.py # AuditModule — configure() → DynamicModule
├── config.py # AuditConfig — all configuration fields
├── constants.py # __version__, ENV_PREFIX, defaults, StrEnums
├── exceptions.py # AuditError, AuditStoreError, AuditVerificationError, AuditRetentionError
├── protocols.py # Re-exports from contracts
├── types.py # Package-internal types (PurgeResult, VerificationResult, StoreBackend)
├── events.py # Domain events: AuditEntryLoggedEvent, AuditPurgeCompletedEvent, ...
├── hooks.py # Hook payloads: AuditEntryCreatedHook, AuditPurgeScheduledHook, ...
├── decorators.py # @audited decorator for auto-logging
├── di/
│ ├── bundle_provider.py # AuditBundleProvider — composite of 5 sub-providers
│ └── sub_providers/
│ ├── core_provider.py # AuditCoreProvider — store + logger
│ ├── retention_provider.py # AuditRetentionProvider — policy + purger
│ ├── verifier_provider.py # AuditVerifierProvider — conditional on HMAC key
│ ├── scheduling_provider.py # AuditSchedulingProvider — cron verification
│ └── admin_provider.py # AuditAdminProvider — admin panel
├── logging/
│ └── logger.py # AuditLogger — fire-tolerant AuditLoggerProtocol impl
├── store/
│ ├── memory.py # InMemoryAuditStore — bounded deque (10k entries)
│ └── sql.py # SqlAuditStore — relational DB via DatabaseProviderProtocol
├── retention/
│ ├── policy.py # PolicyBasedRetention — severity/source override resolution
│ └── purge.py # AuditPurger — batch expiry purge with meta-audit
├── verification/
│ ├── checksum.py # compute_audit_checksum, verify_audit_checksum (HMAC-SHA256)
│ ├── verifier.py # AuditVerifier — AuditVerifierProtocol impl
│ └── backfill.py # backfill_checksums — populates NULL checksum columns
├── scheduling/
│ └── scheduler.py # AuditScheduler — cron-triggered verification registration
├── admin/
│ └── contributor.py # AuditAdminContributor — admin panel (search, export, verify)
└── cli/
├── contributor.py # AuditCliContributor — commands, health, doctor checks
├── commands.py # `lexigram audit` CLI command group
├── checks.py # Health checks
├── doctor.py # Doctor checks
├── hooks.py # CLI command audit hook
└── shell.py # Interactive shell context

flowchart LR
    subgraph Contracts[lexigram-contracts]
        DE[DomainError]
    end
    subgraph Audit[lexigram-audit]
        AE[AuditError]
        ASE[AuditStoreError]
        AVE[AuditVerificationError]
        ARE[AuditRetentionError]
    end
    subgraph App[Application]
        Handler[Result-based handling]
    end

    DE --> AE
    AE --> ASE
    AE --> AVE
    AE --> ARE
    ASE -->|store.append failed| Handler
    AVE -->|checksum mismatch| Handler
    ARE -->|purge failed| Handler

All audit exceptions are expected, recoverable domain errors. The AuditLogger itself never raises — fire-tolerance is built in at the logger level, not the exception level.


lexigram/audit/di/bundle_provider.py
class AuditBundleProvider(Provider):
config_key = "audit"
def __init__(self, config=None, enable_admin=True):
self._sub_providers = [
AuditCoreProvider(config=config),
AuditRetentionProvider(config=config),
AuditVerifierProvider(config=config),
AuditSchedulingProvider(config=config),
]
if enable_admin:
self._sub_providers.append(AuditAdminProvider(config=config))
async def register(self, container):
for p in self._sub_providers:
await p.register(container)
async def boot(self, container):
for p in self._sub_providers:
await p.boot(container)

Module usage:

from lexigram.audit import AuditModule
app.use(AuditModule.configure(
hmac_key=b"secret",
store_backend="sql",
retention_days=365,
))

constants.py defines:

SymbolDescription
__version__Package version from importlib.metadata
ENV_PREFIXLEX_AUDIT__
ENV_NESTED_DELIMITER__
DEFAULT_STORE_BACKEND"sql"
DEFAULT_TABLE_NAME"audit_log"
DEFAULT_VERIFICATION_SCHEDULE"0 * * * *" (hourly)
DEFAULT_VERIFICATION_BATCH_SIZE100
StoreBackendSQL, MEMORY (StrEnum)
AuditSeverityLOW, MEDIUM, HIGH, CRITICAL (StrEnum)
AuditOutcomeSUCCESS, FAILURE, PARTIAL, UNKNOWN (StrEnum)

PointMechanism
Custom store backendImplement AuditStoreProtocol, register in container
Custom retention policyImplement RetentionPolicyProtocol, register in container
Custom verifierImplement AuditVerifierProtocol, register in container
Event enrichmentSubscribe to AuditEntryCreatedHook via HookRegistryProtocol
Custom middlewareUse @audited() decorator with interceptor pattern
Admin panel customizationSubclass AuditAdminContributor or implement BaseAdminContributor
CLI commandsAdd CommandContribution entries via CLI contributor
Verification schedulingImplement AuditVerifierSchedulerProtocol for custom cron backends