apistockdocs
v0.4 GitHub apistock.dev
Technical/Module packages

modules/auditpg

import "apistock.dev/modules/auditpg"Source on GitHub

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, …#

go
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#

go
const DefaultMaxMetadataBytes = 16 << 10

DefaultMaxMetadataBytes bounds an event's metadata after redaction.

Variables#

var ErrEventNotFound, …#

go
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#

go
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#

go
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#

go
type Option interface {
	// contains filtered or unexported methods
}

An Option configures NewStore.

func WithMaxMetadataBytes#

go
func WithMaxMetadataBytes(n int) Option

WithMaxMetadataBytes sets the largest metadata stored, as JSON after redaction. Larger metadata is replaced with {"metadata_dropped": "too_large"}. Default: DefaultMaxMetadataBytes.

func WithRedactedKeys#

go
func WithRedactedKeys(names ...string) Option

WithRedactedKeys 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#

go
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#

go
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#

go
type StatsCount struct {
	Key   string
	Count int64
}

StatsCount is one group's count. Key is empty for events without a resource type.

type StatsFilter#

go
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#

go
type StatsGroup string

StatsGroup is what Store.Stats counts events by.

const StatsByAction, …#

go
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#

go
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#

go
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#

go
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#

go
func (s *Store) Get(ctx context.Context, id int64) (StoredEvent, error)

Get returns one event, or ErrEventNotFound.

func (*Store) List#

go
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#

go
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#

go
func (s *Store) Record(ctx context.Context, e audit.Event) error

Record 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#

go
func (s *Store) RecordTx(ctx context.Context, db postgres.DBTX, e audit.Event) error

RecordTx stores e through db, usually a pgx.Tx, so the event commits or rolls back with the change it describes.

func (*Store) Stats#

go
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#

go
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.

v0.4
esc
↑↓ move↵ openesc close