audit

package module
v0.0.0-...-b64b7ad Latest Latest
Warning

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

Go to latest
Published: Jan 18, 2026 License: MIT Imports: 2 Imported by: 0

README

audit

A lightweight, thread-safe audit logging library for Go that tracks entity changes and events with field-level precision.

Go Reference Go Report Card Coverage Status

Features

  • Simple API for entity audit logging (create, update, delete)
  • Field-level change tracking with before/after values
  • Sensitive data masking for passwords and tokens
  • Thread-safe concurrent operations
  • Pluggable storage interface (in-memory default)
  • Slog integration for automatic audit from standard logs
  • Zero dependencies in core package

Installation

go get github.com/w0rng/audit

Quick Start

logger := audit.New()

// Log entity creation
logger.Create("order:123", "john.doe", "Order created", map[string]audit.Value{
    "status": audit.PlainValue("pending"),
    "token":  audit.HiddenValue(), // Masked as "***"
})

// Log update
logger.Update("order:123", "admin", "Order approved", map[string]audit.Value{
    "status": audit.PlainValue("approved"),
})

// Get change history
changes := logger.Logs("order:123")
for _, change := range changes {
    fmt.Printf("%s by %s\n", change.Description, change.Author)
    for _, field := range change.Fields {
        fmt.Printf("  %s: %v -> %v\n", field.Field, field.From, field.To)
    }
}

Usage

Creating a Logger
// Default in-memory storage
logger := audit.New()

// With custom storage
logger := audit.New(audit.WithStorage(customStorage))
Logging Events
// Create, update, delete
logger.Create(key, author, description, payload)
logger.Update(key, author, description, payload)
logger.Delete(key, author, description, payload)

// Payload with hidden fields
payload := map[string]audit.Value{
    "email":    audit.PlainValue("user@example.com"),
    "password": audit.HiddenValue(), // Shows as "***"
}
Retrieving Events
// All events for entity
events := logger.Events("order:123")

// Filter by fields
statusEvents := logger.Events("order:123", "status", "total")

// Change history with state reconstruction
changes := logger.Logs("order:123")

Custom Storage

Implement the Storage interface for custom backends (Redis, PostgreSQL, etc.):

type Storage interface {
    Store(key string, event Event)
    Get(key string) []Event
    Has(key string) bool
    Clear(key string)
}

// Use it
storage := NewMyStorage()
logger := audit.New(audit.WithStorage(storage))

See examples/custom_storage for JSON file storage implementation.

Slog Integration

Automatically create audit logs from standard slog logs:

import auditslog "github.com/w0rng/audit/slog"

handler := auditslog.NewHandler(auditLogger, auditslog.HandlerOptions{
    KeyExtractor: auditslog.AttrExtractor(auditslog.AttrEntity),
    ShouldAudit: func(r slog.Record) bool {
        return r.Level >= slog.LevelInfo
    },
})

logger := slog.New(handler)
logger.Info("User created",
    auditslog.AttrEntity, "user:123",
    auditslog.AttrAction, "create",
    "email", "user@example.com",
)

Available attribute constants: AttrEntity, AttrAction, AttrAuthor, AttrUser.

See examples/slog_integration for complete example.

Examples

Run examples to see the library in action:

go run examples/basic/main.go              # Basic usage
go run examples/custom_storage/main.go     # Custom storage
go run examples/slog_integration/main.go   # Slog integration

Testing

go test ./...              # Run all tests
go test -race ./...        # With race detector
go test -cover ./...       # Check coverage
go test -bench=. -benchmem # Run benchmarks

License

MIT License - see LICENSE file for details.

Documentation

Overview

Package audit provides a simple, thread-safe audit logging library for tracking entity changes and events in Go applications.

The library supports:

  • Creating, updating, and deleting entity audit logs
  • Tracking field-level changes with before/after values
  • Hiding sensitive data (e.g., passwords, tokens)
  • Concurrent access with sync.RWMutex
  • Filtering events by payload fields

Basic usage:

logger := audit.New()
logger.Create("user:123", "admin", "User created", map[string]audit.Value{
    "email": audit.PlainValue("user@example.com"),
    "password": audit.HiddenValue(),
})

See the examples directory for complete usage examples.

Example
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	// Create a new audit logger
	logger := audit.New()

	// Log entity creation
	logger.Create("order:12345", "john.doe", "Order created", map[string]audit.Value{
		"status": audit.PlainValue("pending"),
		"total":  audit.PlainValue(99.99),
	})

	// Log entity update
	logger.Update("order:12345", "jane.smith", "Order approved", map[string]audit.Value{
		"status": audit.PlainValue("approved"),
	})

	// Get change history
	changes := logger.Logs("order:12345")
	fmt.Printf("Total changes: %d\n", len(changes))
	fmt.Printf("Last change: %s\n", changes[len(changes)-1].Description)
}
Output:

Total changes: 2
Last change: Order approved

Index

Examples

Constants

View Source
const (
	ActionCreate Action = "create"
	ActionUpdate Action = "update"
	ActionDelete Action = "delete"

	HideText string = "***"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type Action

type Action string

type Change

type Change struct {
	Fields      []ChangeField
	Description string
	Author      string
	Timestamp   time.Time
}

type ChangeField

type ChangeField struct {
	Field string
	From  any
	To    any
}

type Event

type Event struct {
	Timestamp   time.Time
	Action      Action
	Author      string
	Description string
	Payload     map[string]Value
}

type InMemoryStorage

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

InMemoryStorage provides a thread-safe in-memory storage implementation backed by a map. This is the default storage used by New().

func NewInMemoryStorage

func NewInMemoryStorage() *InMemoryStorage

NewInMemoryStorage creates a new in-memory storage instance.

func (*InMemoryStorage) Clear

func (s *InMemoryStorage) Clear(key string)

Clear removes all events for a given key.

func (*InMemoryStorage) Get

func (s *InMemoryStorage) Get(key string) []Event

Get retrieves all events for a given key. Returns an empty slice if the key doesn't exist.

func (*InMemoryStorage) Has

func (s *InMemoryStorage) Has(key string) bool

Has checks if any events exist for a given key.

func (*InMemoryStorage) Store

func (s *InMemoryStorage) Store(key string, event Event)

Store appends an event to the storage for the given key.

type Logger

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

Logger provides thread-safe audit logging functionality.

func New

func New(opts ...Option) *Logger

New creates a new Logger with the given options. If no options are provided, it uses in-memory storage by default.

Example:

logger := audit.New() // uses in-memory storage
logger := audit.New(audit.WithStorage(customStorage)) // uses custom storage

func (*Logger) Create

func (l *Logger) Create(key, author, description string, payload map[string]Value)
Example
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	logger := audit.New()

	logger.Create(
		"user:123",
		"admin",
		"User account created",
		map[string]audit.Value{
			"email":    audit.PlainValue("user@example.com"),
			"password": audit.HiddenValue(),
		},
	)

	events := logger.Events("user:123")
	fmt.Printf("Created %d event(s)\n", len(events))
}
Output:

Created 1 event(s)

func (*Logger) Delete

func (l *Logger) Delete(key, author, description string, payload map[string]Value)
Example
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	logger := audit.New()

	logger.Create("item:1", "user", "Item created", map[string]audit.Value{
		"name": audit.PlainValue("Widget"),
	})

	logger.Delete("item:1", "admin", "Item deleted", map[string]audit.Value{})

	events := logger.Events("item:1")
	fmt.Printf("Total events: %d\n", len(events))
	fmt.Printf("Last action: %s\n", events[len(events)-1].Action)
}
Output:

Total events: 2
Last action: delete

func (*Logger) Events

func (l *Logger) Events(key string, fields ...string) []Event

Events retrieves audit events for a key, optionally filtering by specific payload fields. If no fields are specified, all events for the key are returned. When fields are provided, only events containing at least one of those fields are returned, with their payloads filtered to include only the requested fields.

Example
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	logger := audit.New()

	logger.Create("order:1", "user", "Order created", map[string]audit.Value{
		"status": audit.PlainValue("pending"),
		"total":  audit.PlainValue(100.50),
	})

	logger.Update("order:1", "user", "Status updated", map[string]audit.Value{
		"status": audit.PlainValue("paid"),
	})

	// Get only status-related events
	events := logger.Events("order:1", "status")
	fmt.Printf("Found %d status events\n", len(events))
}
Output:

Found 2 status events
Example (MultipleFields)
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	logger := audit.New()

	logger.Create("user:1", "admin", "User created", map[string]audit.Value{
		"email":  audit.PlainValue("user@example.com"),
		"role":   audit.PlainValue("editor"),
		"status": audit.PlainValue("active"),
	})

	logger.Update("user:1", "admin", "Role updated", map[string]audit.Value{
		"role": audit.PlainValue("admin"),
	})

	logger.Update("user:1", "system", "Status changed", map[string]audit.Value{
		"status": audit.PlainValue("inactive"),
	})

	// Get events that have either role or status fields
	events := logger.Events("user:1", "role", "status")
	fmt.Printf("Events with role or status: %d\n", len(events))
}
Output:

Events with role or status: 3
Example (NoFilter)
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	logger := audit.New()

	logger.Create("item:1", "user", "Created", map[string]audit.Value{
		"name": audit.PlainValue("Widget"),
	})
	logger.Update("item:1", "user", "Updated", map[string]audit.Value{
		"price": audit.PlainValue(29.99),
	})

	// Get all events (no filter)
	events := logger.Events("item:1")
	fmt.Printf("Total events: %d\n", len(events))
}
Output:

Total events: 2

func (*Logger) LogChange

func (l *Logger) LogChange(key string, action Action, author, description string, payload map[string]Value)

LogChange records a new audit event for the given key with the specified action, author, description, and payload. This is the core logging method used by Create, Update, and Delete convenience methods.

func (*Logger) Logs

func (l *Logger) Logs(key string) []Change

Logs returns the complete change history for a key with field-level state transitions. It reconstructs the state over time, tracking before/after values for each field.

Example
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	logger := audit.New()

	logger.Create("item:1", "alice", "Item created", map[string]audit.Value{
		"color": audit.PlainValue("red"),
	})

	logger.Update("item:1", "bob", "Color changed", map[string]audit.Value{
		"color": audit.PlainValue("blue"),
	})

	changes := logger.Logs("item:1")
	for _, change := range changes {
		fmt.Printf("%s by %s\n", change.Description, change.Author)
		for _, field := range change.Fields {
			fmt.Printf("  %s: %v -> %v\n", field.Field, field.From, field.To)
		}
	}
}
Output:

Item created by alice
  color: <nil> -> red
Color changed by bob
  color: red -> blue

func (*Logger) Update

func (l *Logger) Update(key, author, description string, payload map[string]Value)
Example
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	logger := audit.New()

	logger.Create("order:1", "user", "Order created", map[string]audit.Value{
		"status": audit.PlainValue("pending"),
	})

	logger.Update("order:1", "admin", "Order approved", map[string]audit.Value{
		"status": audit.PlainValue("approved"),
	})

	events := logger.Events("order:1")
	fmt.Printf("Total events: %d\n", len(events))
}
Output:

Total events: 2

type Option

type Option func(*Logger)

Option is a function that configures a Logger.

func WithStorage

func WithStorage(storage Storage) Option

WithStorage sets a custom storage implementation for the logger. If not specified, NewInMemoryStorage() is used by default.

Example
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	// Create a custom storage implementation
	customStorage := audit.NewInMemoryStorage()

	// Use WithStorage option to configure logger
	logger := audit.New(audit.WithStorage(customStorage))

	logger.Create("order:1", "user", "Order created", map[string]audit.Value{
		"status": audit.PlainValue("pending"),
	})

	events := logger.Events("order:1")
	fmt.Printf("Logged %d event(s) with custom storage\n", len(events))
}
Output:

Logged 1 event(s) with custom storage

type Storage

type Storage interface {
	// Store appends an event to the storage for the given key
	Store(key string, event Event)

	// Get retrieves all events for a given key.
	// Returns an empty slice if key doesn't exist.
	Get(key string) []Event

	// Has checks if any events exist for a given key
	Has(key string) bool

	// Clear removes all events for a given key
	Clear(key string)
}

Storage defines the interface for storing and retrieving audit events. Implementations must be safe for concurrent access.

type Value

type Value struct {
	Data   any
	Hidden bool
}

func HiddenValue

func HiddenValue() Value

HiddenValue creates a Value with Hidden=true to mask sensitive data in logs. Hidden values are displayed as "***" to prevent exposure of passwords, tokens, etc.

Example
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	const hideField = "password"
	logger := audit.New()

	logger.Create("user:1", "admin", "User created", map[string]audit.Value{
		"email":   audit.PlainValue("user@example.com"),
		hideField: audit.HiddenValue(),
	})

	changes := logger.Logs("user:1")
	for _, field := range changes[0].Fields {
		if field.Field == hideField {
			fmt.Printf("%s: %v -> %v\n", field.Field, field.From, field.To)
		}
	}
}
Output:

password: *** -> ***

func PlainValue

func PlainValue(v any) Value

PlainValue creates a Value with the given data visible in logs. Use this for non-sensitive data that can be safely logged.

Example
package main

import (
	"fmt"

	"github.com/w0rng/audit"
)

func main() {
	logger := audit.New()

	logger.Create("config:1", "admin", "Config updated", map[string]audit.Value{
		"timeout": audit.PlainValue(30),
		"enabled": audit.PlainValue(true),
		"name":    audit.PlainValue("production"),
	})

	events := logger.Events("config:1")
	fmt.Printf("Logged %d event(s)\n", len(events))
}
Output:

Logged 1 event(s)

Directories

Path Synopsis
examples
basic command
custom_storage command
internal
be
Package be provides minimal assertions for Go tests.
Package be provides minimal assertions for Go tests.
Package slog provides integration between Go's structured logging (slog) and audit logging.
Package slog provides integration between Go's structured logging (slog) and audit logging.

Jump to

Keyboard shortcuts

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