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 ¶
- func Log(ctx context.Context, event Event) error
- func Middleware(l Logger, logRequests ...bool) func(http.Handler) http.Handler
- func WithLogger(ctx context.Context, l Logger) context.Context
- type DBLogger
- type DBLoggerOption
- type Event
- type EventBuilder
- func (b *EventBuilder) Build() Event
- func (b *EventBuilder) Log(ctx context.Context) error
- func (b *EventBuilder) Logf(ctx context.Context) string
- func (b *EventBuilder) WithAction(action string) *EventBuilder
- func (b *EventBuilder) WithIP(ip string) *EventBuilder
- func (b *EventBuilder) WithMeta(key string, value any) *EventBuilder
- func (b *EventBuilder) WithRequestID(id string) *EventBuilder
- func (b *EventBuilder) WithResource(resource string) *EventBuilder
- type EventMapper
- type EventType
- type JSONLogger
- type Logger
- type MultiLogger
- type NOOPLogger
- type Result
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Middleware ¶
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))
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
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
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).
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
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.
type Logger ¶
Logger is the sink for audit events.
func FromContext ¶
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.
type NOOPLogger ¶
type NOOPLogger struct{}
NOOPLogger discards all events. Useful in tests or when audit logging is optional.