modules/auditpg
Package auditpg stores audit events in PostgreSQL and queries them for operator APIs (ADR-0036). Store implements audit.Recorder:
store, err := auditpg.NewStore(pool) settingsStore, err := settings.NewStore(ctx, pool, reg, store)
Events are append-only. Before storing, the store fills actor, request and trace fields from the context, validates the event, redacts metadata under sensitive keys such as "password" or "token", and bounds text lengths and metadata size, so one bad event never blocks the rest.
Use Store.RecordTx to commit an event with the change it describes.
Stability: pre-1.0 (ADR-0015).
Constants#
const DefaultStatsWindow, …#
const (
DefaultStatsWindow = 7 * 24 * time.Hour
MaxStatsWindow = 90 * 24 * time.Hour
// MaxStatsGroups is the most groups returned, largest first; the rest are
// counted in Stats.Other. Days are never cut: a window has at most 91.
MaxStatsGroups = 50
)Bounds of a stats window.
const DefaultMaxMetadataBytes#
const DefaultMaxMetadataBytes = 16 << 10DefaultMaxMetadataBytes bounds an event's metadata after redaction.
Variables#
var ErrEventNotFound, …#
var (
// ErrEventNotFound reports an event ID that doesn't exist, or was
// removed by retention.
ErrEventNotFound = errors.New("auditpg: audit event not found")
// ErrInvalidCursor reports a cursor that wasn't returned by [Store.List].
ErrInvalidCursor = errors.New("auditpg: invalid cursor")
// ErrInvalidFilter reports a [Filter] with an unknown outcome, a
// malformed action prefix or an empty time range. The wrapping error
// says which.
ErrInvalidFilter = errors.New("auditpg: invalid filter")
)Errors returned by Store methods. Check them with errors.Is.
var Migrations#
var Migrations fs.FS = mustSub(migrationFiles, "migrations")Migrations holds the module's goose migrations: the audit_events table. Apps copy them into db/migrations; tests can apply them directly with pgtest.
Types#
type Filter#
type Filter struct {
ActorKind actor.Kind
ActorID string
// Action matches one action exactly; ActionPrefix matches every action
// starting with it, such as "jobs." or "auth.session.".
Action string
ActionPrefix string
ResourceType string
ResourceID string
OrgID string
Outcome audit.Outcome
RequestID string
// From and To bound OccurredAt: From inclusive, To exclusive.
From time.Time
To time.Time
// Limit is clamped to 1–100; 0 means 50.
Limit int
// Cursor is Page.NextCursor from the previous page.
Cursor string
}Filter selects events to list. Empty fields match everything.
type Option#
type Option interface {
// contains filtered or unexported methods
}An Option configures NewStore.
func WithMaxMetadataBytes#
func WithMaxMetadataBytes(n int) OptionWithMaxMetadataBytes sets the largest metadata stored, as JSON after redaction. Larger metadata is replaced with {"metadata_dropped": "too_large"}. Default: DefaultMaxMetadataBytes.
func WithRedactedKeys#
func WithRedactedKeys(names ...string) OptionWithRedactedKeys adds metadata keys whose values are replaced with "[REDACTED]", on top of the defaults (password, secret, token, cookie, authorization, api_key, private_key, otp, credential, recovery_code, verification_code). A key matches when it equals a name or contains it as a whole snake_case segment: "token" matches "refresh_token" and "accessToken" but not "tokenizer".
type Page#
type Page struct {
Events []StoredEvent
// NextCursor fetches the next page; empty on the last page.
NextCursor string
}Page is a page of events, newest first.
type Stats#
type Stats struct {
From, To time.Time
GroupBy StatsGroup
Total int64
// Groups are the largest groups, most events first, or every day in
// order for StatsByDay.
Groups []StatsCount
// Other counts events in groups beyond MaxStatsGroups.
Other int64
}Stats are event counts in a window.
type StatsCount#
type StatsCount struct {
Key string
Count int64
}StatsCount is one group's count. Key is empty for events without a resource type.
type StatsFilter#
type StatsFilter struct {
Filter
GroupBy StatsGroup
}StatsFilter selects the events to count: Filter's fields except Limit and Cursor. From and To default to the DefaultStatsWindow ending now and may be at most MaxStatsWindow apart.
type StatsGroup#
type StatsGroup stringStatsGroup is what Store.Stats counts events by.
const StatsByAction, …#
const (
StatsByAction StatsGroup = "action"
StatsByOutcome StatsGroup = "outcome"
StatsByActorKind StatsGroup = "actor_kind"
StatsByResourceType StatsGroup = "resource_type"
// StatsByDay counts events per UTC day, keyed YYYY-MM-DD.
StatsByDay StatsGroup = "day"
)Groupings for Store.Stats.
type Store#
type Store struct {
// contains filtered or unexported fields
}Store records audit events in the audit_events table and lists them. It is safe for concurrent use.
func NewStore#
func NewStore(pool *pgxpool.Pool, opts ...Option) (*Store, error)NewStore returns a store on pool. The audit_events table comes from Migrations; apply them first.
func (*Store) DeleteBefore#
func (s *Store) DeleteBefore(ctx context.Context, before time.Time, limit int) (int64, error)DeleteBefore deletes up to limit events that occurred before before, oldest first, and returns how many it deleted. Retention calls it until it deletes fewer than limit (ADR-0051).
func (*Store) Get#
func (s *Store) Get(ctx context.Context, id int64) (StoredEvent, error)Get returns one event, or ErrEventNotFound.
func (*Store) List#
func (s *Store) List(ctx context.Context, f Filter) (Page, error)List returns events matching f, newest first. It returns an error wrapping ErrInvalidFilter, or ErrInvalidCursor.
func (*Store) Oldest#
func (s *Store) Oldest(ctx context.Context) (oldest time.Time, ok bool, err error)Oldest returns when the oldest stored event occurred; ok is false when there are none.
func (*Store) Record#
func (s *Store) Record(ctx context.Context, e audit.Event) errorRecord stores e. Empty actor, organisation, request and trace fields are filled from ctx, and a zero OccurredAt becomes now. It returns an error for an invalid event or when the event can't be stored.
func (*Store) RecordTx#
func (s *Store) RecordTx(ctx context.Context, db postgres.DBTX, e audit.Event) errorRecordTx stores e through db, usually a pgx.Tx, so the event commits or rolls back with the change it describes.
func (*Store) Stats#
func (s *Store) Stats(ctx context.Context, f StatsFilter) (Stats, error)Stats counts events matching f by f.GroupBy. It returns an error wrapping ErrInvalidFilter for an unknown grouping or a window over MaxStatsWindow.
type StoredEvent#
type StoredEvent struct {
// ID increases in recording order.
ID int64
// RecordedAt is when the database stored the event, by its clock;
// OccurredAt is when the action happened, by the recording instance's.
// Both are UTC.
RecordedAt time.Time
audit.Event
}StoredEvent is a recorded audit event.