audit

package
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 1, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package audit provides structured security event logging for authentication and authorisation actions. It is designed for compliance requirements such as SOC 2, ISO 27001, and GDPR audit trails.

Quick start:

logger := audit.NewJSONLogger(os.Stdout)
logger.Log(ctx, audit.Event{
    Type:    audit.EventLogin,
    Subject: "user-123",
    Result:  audit.ResultAllow,
    IP:      "1.2.3.4",
})

Middleware integration:

mux.Handle("/", audit.Middleware(logger)(handler))

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Log

func Log(ctx context.Context, event Event) error

Log is a convenience shorthand for FromContext(ctx).Log(ctx, event).

func Middleware

func Middleware(l Logger, logRequests ...bool) func(http.Handler) http.Handler

Middleware injects the logger into the request context and, optionally, logs every request automatically when logRequests is true. The logged event type is EventAccessGranted or EventAccessDenied based on the response status code (status ≥ 400 is treated as denied).

Usage — inject only (log manually inside handlers):

mux.Handle("/", audit.Middleware(logger)(handler))

Usage — auto-log every request:

mux.Handle("/", audit.Middleware(logger, true)(handler))

func WithLogger

func WithLogger(ctx context.Context, l Logger) context.Context

WithLogger stores a Logger in the context so handlers can retrieve it without explicit dependency injection.

Types

type DBLogger added in v1.0.1

type DBLogger struct {
	// contains filtered or unexported fields
}

DBLogger writes audit events to a SQL database table using database/sql. It is compatible with any database driver (PostgreSQL, MySQL, SQLite, etc.).

Default schema

Run this migration before using DBLogger with the default column layout. For PostgreSQL use JSONB for meta; for MySQL/SQLite use TEXT.

CREATE TABLE audit_logs (
    id         BIGSERIAL PRIMARY KEY,            -- BIGINT AUTO_INCREMENT for MySQL
    event_id   TEXT,
    type       TEXT        NOT NULL,
    timestamp  TIMESTAMPTZ NOT NULL,             -- DATETIME for MySQL, TEXT for SQLite
    subject    TEXT,
    resource   TEXT,
    action     TEXT,
    result     TEXT,
    ip         TEXT,
    user_agent TEXT,
    request_id TEXT,
    meta       JSONB                             -- TEXT for MySQL/SQLite
);

CREATE INDEX audit_logs_type_idx      ON audit_logs(type);
CREATE INDEX audit_logs_subject_idx   ON audit_logs(subject);
CREATE INDEX audit_logs_timestamp_idx ON audit_logs(timestamp);

Custom schema

If your database has a different audit table layout, supply your own INSERT statement and a mapper via WithCustomSchema — no migration needed.

Usage

db, _ := sql.Open("pgx", os.Getenv("DATABASE_URL"))
logger := audit.NewDBLogger(db)

// Alongside the JSON logger:
combined := audit.NewMultiLogger(audit.NewJSONLogger(os.Stdout), logger)

func NewDBLogger added in v1.0.1

func NewDBLogger(db *sql.DB, opts ...DBLoggerOption) *DBLogger

NewDBLogger creates a DBLogger that writes to db. The database connection must already be open and the schema must exist.

func (*DBLogger) Log added in v1.0.1

func (l *DBLogger) Log(ctx context.Context, e Event) error

Log inserts the event into the database. It honours the context deadline/ cancellation so a slow database does not block the request indefinitely.

When WithCustomSchema was used, Log delegates entirely to the caller's INSERT statement and EventMapper. Otherwise the default 11-column schema is used.

func (*DBLogger) Ping added in v1.0.1

func (l *DBLogger) Ping(ctx context.Context) error

Ping checks that the database is reachable and the audit_logs table exists. Call this at startup to fail fast on misconfiguration. Note: Ping uses the configured table name; it is not available when WithCustomSchema is set (the table name is embedded in the caller's SQL).

func (*DBLogger) Recent added in v1.0.1

func (l *DBLogger) Recent(ctx context.Context, subject string, limit int) ([]Event, error)

Recent returns up to limit events from the table in reverse-chronological order. Optionally filter by subject (pass "" to return all subjects). Useful for building an admin audit trail view.

type DBLoggerOption added in v1.0.1

type DBLoggerOption func(*DBLogger)

DBLoggerOption configures a DBLogger.

func WithCustomSchema added in v1.0.2

func WithCustomSchema(insertSQL string, mapper EventMapper) DBLoggerOption

WithCustomSchema replaces the default INSERT statement and column mapping with the caller's own. Use this when your database has a different audit_logs schema than the SDK's default.

insertSQL must be a complete INSERT statement with positional placeholders ($1, $2, … for PostgreSQL; ?, ?, … for MySQL/SQLite). mapper must return a slice whose length matches the number of placeholders.

When WithCustomSchema is set, WithTableName is ignored because the table name is embedded in the caller-supplied SQL.

Example (PostgreSQL, project-specific schema):

const insertSQL = `INSERT INTO audit_logs
    (id, user_id, action, entity_type, entity_id, changes, ip_address, user_agent)
    VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`

mapper := func(e audit.Event) []any {
    return []any{
        e.ID,
        e.Subject,            // user_id
        e.Action,
        e.Resource,           // entity_type
        e.Meta["entity_id"],  // entity_id
        metaToJSON(e.Meta),   // changes
        e.IP,
        e.UserAgent,
    }
}

logger := audit.NewDBLogger(db, audit.WithCustomSchema(insertSQL, mapper))

func WithTableName added in v1.0.1

func WithTableName(name string) DBLoggerOption

WithTableName sets the target table name. Default: "audit_logs". This option is ignored when WithCustomSchema is set, because the table name is embedded in the caller's INSERT statement.

type Event

type Event struct {
	// ID is a unique identifier for this event (set automatically by JSONLogger
	// if left empty — applications may set their own).
	ID string `json:"id,omitempty"`

	// Type classifies the event.
	Type EventType `json:"type"`

	// Timestamp is when the event occurred. Defaults to time.Now() when zero.
	Timestamp time.Time `json:"timestamp"`

	// Subject is the entity that performed the action (user ID, service name, etc.).
	Subject string `json:"subject,omitempty"`

	// Resource is the object that was acted upon (e.g. "posts/42", "users").
	Resource string `json:"resource,omitempty"`

	// Action is the operation that was attempted (e.g. "read", "delete").
	Action string `json:"action,omitempty"`

	// Result is the outcome of the event.
	Result Result `json:"result,omitempty"`

	// IP is the client IP address.
	IP string `json:"ip,omitempty"`

	// UserAgent is the client User-Agent string.
	UserAgent string `json:"user_agent,omitempty"`

	// RequestID correlates the event with an HTTP request trace.
	RequestID string `json:"request_id,omitempty"`

	// Meta holds arbitrary additional fields.
	Meta map[string]any `json:"meta,omitempty"`
}

Event represents a single auditable security action.

type EventBuilder

type EventBuilder struct {
	// contains filtered or unexported fields
}

EventBuilder provides a fluent API for constructing Events.

func Eventf

func Eventf(t EventType, subject string, result Result) *EventBuilder

Eventf is a convenience constructor for quickly building events.

audit.Eventf(audit.EventLogin, "user-123", audit.ResultAllow).WithIP("1.2.3.4")

func (*EventBuilder) Build

func (b *EventBuilder) Build() Event

Build returns the constructed Event.

func (*EventBuilder) Log

func (b *EventBuilder) Log(ctx context.Context) error

Log sends the event to the logger in ctx and returns any error.

func (*EventBuilder) Logf

func (b *EventBuilder) Logf(ctx context.Context) string

Logf is a shorthand: build and log in one call, returning a formatted error string if logging fails (safe to ignore in non-critical paths).

func (*EventBuilder) WithAction

func (b *EventBuilder) WithAction(action string) *EventBuilder

func (*EventBuilder) WithIP

func (b *EventBuilder) WithIP(ip string) *EventBuilder

func (*EventBuilder) WithMeta

func (b *EventBuilder) WithMeta(key string, value any) *EventBuilder

func (*EventBuilder) WithRequestID

func (b *EventBuilder) WithRequestID(id string) *EventBuilder

func (*EventBuilder) WithResource

func (b *EventBuilder) WithResource(resource string) *EventBuilder

type EventMapper added in v1.0.2

type EventMapper func(e Event) []any

EventMapper converts an audit.Event into the positional SQL parameters for the caller's custom INSERT statement. The returned slice must have exactly as many elements as there are placeholders in the INSERT statement.

type EventType

type EventType string

EventType classifies a security-relevant action.

const (
	// Authentication events.
	EventLogin       EventType = "auth.login"
	EventLoginFailed EventType = "auth.login_failed"
	EventLogout      EventType = "auth.logout"
	EventMFASuccess  EventType = "auth.mfa_success"
	EventMFAFailed   EventType = "auth.mfa_failed"

	// Token lifecycle.
	EventTokenIssued    EventType = "token.issued"
	EventTokenVerified  EventType = "token.verified"
	EventTokenRefreshed EventType = "token.refreshed"
	EventTokenRevoked   EventType = "token.revoked"
	EventTokenExpired   EventType = "token.expired"
	EventTokenInvalid   EventType = "token.invalid"

	// API key lifecycle.
	EventAPIKeyIssued  EventType = "apikey.issued"
	EventAPIKeyUsed    EventType = "apikey.used"
	EventAPIKeyRevoked EventType = "apikey.revoked"
	EventAPIKeyInvalid EventType = "apikey.invalid"

	// Authorisation decisions.
	EventAccessGranted EventType = "authz.access_granted"
	EventAccessDenied  EventType = "authz.access_denied"

	// Rate limiting.
	EventRateLimited EventType = "rate.limited"

	// Account management.
	EventPasswordChanged EventType = "account.password_changed"
	EventPasswordReset   EventType = "account.password_reset"
	EventAccountLocked   EventType = "account.locked"
	EventAccountUnlocked EventType = "account.unlocked"
)

type JSONLogger

type JSONLogger struct {
	// contains filtered or unexported fields
}

JSONLogger writes newline-delimited JSON events to an io.Writer. Each event is a single JSON object terminated by a newline — compatible with log aggregators such as Loki, Datadog, CloudWatch, and Splunk.

func NewJSONLogger

func NewJSONLogger(w io.Writer) *JSONLogger

NewJSONLogger creates a JSONLogger that writes to w.

func (*JSONLogger) Log

func (l *JSONLogger) Log(_ context.Context, e Event) error

Log writes event as a JSON line. It auto-populates Timestamp when zero.

type Logger

type Logger interface {
	Log(ctx context.Context, event Event) error
}

Logger is the sink for audit events.

func FromContext

func FromContext(ctx context.Context) Logger

FromContext retrieves the Logger from the context. Returns NOOPLogger when no logger is present, so callers never need to nil-check.

type MultiLogger

type MultiLogger struct {
	// contains filtered or unexported fields
}

MultiLogger fans out events to multiple loggers. All loggers are called even when one returns an error; the first error is returned.

func NewMultiLogger

func NewMultiLogger(loggers ...Logger) *MultiLogger

NewMultiLogger creates a MultiLogger that writes to all provided loggers.

func (*MultiLogger) Log

func (m *MultiLogger) Log(ctx context.Context, e Event) error

Log sends the event to every configured logger.

type NOOPLogger

type NOOPLogger struct{}

NOOPLogger discards all events. Useful in tests or when audit logging is optional.

func (NOOPLogger) Log

func (NOOPLogger) Log(_ context.Context, _ Event) error

Log does nothing and returns nil.

type Result

type Result string

Result is the outcome of a security event.

const (
	ResultAllow   Result = "allow"
	ResultDeny    Result = "deny"
	ResultUnknown Result = "unknown"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL