internal/infra/pg/auditstore.go
internal/infra/pg · 423 lines · 15 declarations · source
Declarations
type AuditStore
type AuditStore struct {
pool *pgxpool.Pool
schema Schema
}
AuditStore appends to the ledger.
Why writing here never fails a request
An audit write that could fail an operation would make the ledger a second thing that has to be working for memory to work — and the failure would arrive as a customer's write being refused for a reason that has nothing to do with their data. A ledger that takes the system down is one an operator eventually switches off.
So a failed append is logged and swallowed by the caller. That is a real weakness and it is the right one: the alternative is a durability guarantee bought with availability, on a table whose value is completeness over time rather than atomicity per row. When the ledger becomes something a third party verifies — see the open question about a tamper-evident log — that trade has to be revisited, and this comment is where it will be found.
func NewAuditStore
func NewAuditStore(pool *pgxpool.Pool, schema Schema) *AuditStore
method AuditStore.Append
func (s *AuditStore) Append(ctx context.Context, entry domain.AuditEntry) error
Append records one operation.
It takes a domain.AuditEntry rather than a spread of arguments so that adding a field is a change the compiler shows every caller, rather than one that silently defaults at three call sites and not the fourth.
method AuditStore.RecentIn
func (s *AuditStore) RecentIn(ctx context.Context, project string, limit int) ([]domain.AuditEntry, error)
RecentIn is Recent narrowed to one project, or to none when project is empty.
The filter is an equality on a column the ledger already holds, served by the time index it already has: an operator asking what happened in one project reads the same newest-first page, with every other project's rows left out.
type Activity
type Activity struct {
Since, Until time.Time
Bucket time.Duration
// Current is this window; Previous the window of the same length before it, so a number can be
// read against the one it replaced. HasPrevious is false when the window is the whole ledger,
// which has nothing before it.
Current, Previous ActivityTotals
HasPrevious bool
// Series is the window cut into equal buckets, oldest first, every bucket present — an empty
// hour is a zero, not a gap somebody reads as missing data.
Series []ActivityBucket
// Operations is this window by operation, busiest first.
Operations []OperationActivity
// Projects is the busiest projects in this window, when the window is not already one project's.
Projects []ProjectActivity
}
Activity is what the ledger says happened in a window: counted, never listed.
Everything here is an aggregate of rows that already hold no words — how many operations, of which kinds, allowed or refused, and in which project — so the console can draw an instance's activity without reading anything anybody said.
type ActivityTotals
type ActivityTotals struct {
Operations, Refused int64
// TurnsStored is the turns observations wrote; Recalls the recall and context requests served;
// Erasures the erasures performed and ErasedRows what they removed. Each counts allowed rows
// only — a refused recall served nothing.
TurnsStored, Recalls, Erasures, ErasedRows int64
}
ActivityTotals are the counts an operator reads first.
type ActivityBucket
type ActivityBucket struct {
Start time.Time
Allowed, Refused int64
}
ActivityBucket is one slice of a window.
type OperationActivity
type OperationActivity struct {
Operation string
Allowed, Refused, Magnitude int64
}
OperationActivity is one operation's share of a window.
type ProjectActivity
type ProjectActivity struct {
Project string
Operations, Refused int64
}
ProjectActivity is one project's share of a window.
const activityProjects
const activityProjects = 6
activityProjects bounds the busiest-projects list. A console shows the few worth looking at; the rest are one filter away.
method ActivityTotals.add
func (t *ActivityTotals) add(operation, outcome string, n, magnitude int64)
method AuditStore.Activity
func (s *AuditStore) Activity(ctx context.Context, project string, since, until time.Time, buckets int) (Activity, error)
Activity counts the ledger over [since, until), for one project or, with project empty, for all of them, cut into the given number of buckets.
A zero since means the whole ledger: it starts at the earliest row (or an hour before until, for an empty ledger), and it has no previous window. Three statements, each bounded by the window and served by the time index, rather than one that returns every row for the caller to count: the work is proportional to the window's rows and happens where they are.
method AuditStore.Recent
func (s *AuditStore) Recent(ctx context.Context, limit int) ([]domain.AuditEntry, error)
Recent reads the ledger back, newest first.
The reader an operator needs, and the one that proves the ledger is not write-only — a record nobody can read is a record nobody checks.
method AuditStore.Seal
func (s *AuditStore) Seal(ctx context.Context) (domain.AuditSeal, error)
Seal covers every entry written since the last seal, and chains to it.
Batched rather than per-entry, deliberately. Chaining each row to the one before it would put a serialisation point on the read path — every recall writes an audit row, and each would queue behind every other. That is a cost paid on every call to protect against a rare case, which is the trade rule 9 says to refuse.
The window between seals is the exposure: an entry written after the last seal is not covered, and that is a real hole rather than a rounding error. It is bounded by how often this runs.
method AuditStore.Verify
func (s *AuditStore) Verify(ctx context.Context) (domain.AuditVerification, error)
Verify recomputes the chain and reports the first place it disagrees.
The operator's own check, run against their own instance. It answers "has anything been altered since it was sealed" — and it distinguishes an altered ENTRY from a re-linked CHAIN, because those are different accusations and a verifier that says only "invalid" tells nobody what happened.