Documentation
¶
Overview ¶
Package audit provides a unified audit logging toolkit for Go services. It supports multiple storage backends (file, database, Redis) and async writing.
Index ¶
- Constants
- func MaskDestination(dest string, channel string) string
- func MaskEmail(email string) string
- func MaskIP(ip string) string
- func MaskPhone(phone string) string
- func MaskString(s string, keepChars int) string
- type Config
- type DatabaseConfig
- type DatabaseStorage
- type EventType
- type FileStorage
- type Logger
- func (l *Logger) GetStats() *Stats
- func (l *Logger) Log(ctx context.Context, record *Record)
- func (l *Logger) LogAccess(ctx context.Context, eventType EventType, userID, resource string, ...)
- func (l *Logger) LogAuth(ctx context.Context, eventType EventType, userID string, result Result, ...)
- func (l *Logger) LogChallenge(ctx context.Context, eventType EventType, challengeID, userID string, ...)
- func (l *Logger) Query(ctx context.Context, filter *QueryFilter) ([]*Record, error)
- func (l *Logger) SetLogCallback(fn func(record *Record))
- func (l *Logger) Stop() error
- type MultiStorage
- type NoopStorage
- type QueryFilter
- func (f *QueryFilter) Normalize()
- func (f *QueryFilter) WithChallengeID(challengeID string) *QueryFilter
- func (f *QueryFilter) WithChannel(channel string) *QueryFilter
- func (f *QueryFilter) WithEventType(eventType string) *QueryFilter
- func (f *QueryFilter) WithIP(ip string) *QueryFilter
- func (f *QueryFilter) WithLimit(limit int) *QueryFilter
- func (f *QueryFilter) WithOffset(offset int) *QueryFilter
- func (f *QueryFilter) WithResult(result string) *QueryFilter
- func (f *QueryFilter) WithSessionID(sessionID string) *QueryFilter
- func (f *QueryFilter) WithTimeRange(startTime, endTime int64) *QueryFilter
- func (f *QueryFilter) WithUserID(userID string) *QueryFilter
- type Record
- func (r *Record) Copy() *Record
- func (r *Record) SetTimestamp(ts int64) *Record
- func (r *Record) ToJSON() ([]byte, error)
- func (r *Record) WithChallengeID(challengeID string) *Record
- func (r *Record) WithChannel(channel string) *Record
- func (r *Record) WithDestination(destination string) *Record
- func (r *Record) WithDuration(durationMS int64) *Record
- func (r *Record) WithIP(ip string) *Record
- func (r *Record) WithMetadata(key string, value interface{}) *Record
- func (r *Record) WithProvider(provider, messageID string) *Record
- func (r *Record) WithPurpose(purpose string) *Record
- func (r *Record) WithReason(reason string) *Record
- func (r *Record) WithRequestID(requestID string) *Record
- func (r *Record) WithResource(resource string) *Record
- func (r *Record) WithSessionID(sessionID string) *Record
- func (r *Record) WithTraceID(traceID string) *Record
- func (r *Record) WithUserAgent(ua string) *Record
- func (r *Record) WithUserID(userID string) *Record
- type RecordOption
- func WithRecordChannel(channel string) RecordOption
- func WithRecordDestination(dest string) RecordOption
- func WithRecordIP(ip string) RecordOption
- func WithRecordMetadata(key string, value interface{}) RecordOption
- func WithRecordProvider(provider, messageID string) RecordOption
- func WithRecordPurpose(purpose string) RecordOption
- func WithRecordReason(reason string) RecordOption
- func WithRecordRequestID(requestID string) RecordOption
- func WithRecordTraceID(traceID string) RecordOption
- func WithRecordUserAgent(ua string) RecordOption
- type RedisConfig
- type RedisStorage
- func (s *RedisStorage) Cleanup(ctx context.Context) (int64, error)
- func (s *RedisStorage) Client() *redis.Client
- func (s *RedisStorage) Close() error
- func (s *RedisStorage) KeyPrefix() string
- func (s *RedisStorage) Query(ctx context.Context, filter *QueryFilter) ([]*Record, error)
- func (s *RedisStorage) TTL() time.Duration
- func (s *RedisStorage) Write(ctx context.Context, record *Record) error
- type Result
- type Stats
- type Storage
- type StorageOptions
- type StorageType
- type Writer
- type WriterConfig
Constants ¶
const MaxRecordJSONSize = 1 << 20
MaxRecordJSONSize is the maximum allowed size for RecordFromJSON input (1MB). Larger payloads are rejected to avoid DoS from unbounded unmarshaling.
Variables ¶
This section is empty.
Functions ¶
func MaskDestination ¶
MaskDestination masks a destination (phone or email) based on channel
func MaskIP ¶
MaskIP masks an IP address, keeping the first and last IPv4 octet (192.168.1.5 -> 192.***.5).
Note this is pseudonymisation, not anonymisation: with the first and last octet retained an address is narrowed to 1 of 65536, and often far fewer in practice. Use MaskString if a stronger reduction is required.
func MaskString ¶
MaskString masks a string, keeping first and last n characters. A negative keepChars is treated as 0.
Types ¶
type Config ¶
type Config struct {
// Enabled controls whether audit logging is enabled
Enabled bool
// MaskDestination controls whether destinations (phone/email) should be masked
MaskDestination bool
// TTL for Redis/cache storage (0 means use storage default)
TTL time.Duration
// Writer configuration (for async writing)
Writer *WriterConfig
// OnEnqueueFailed, if set, is invoked when an async record is dropped
// because the writer queue is full. Callers can use it to increment a
// drop-counter metric or emit an alert so silent audit loss is observable.
OnEnqueueFailed func(record *Record)
// OnWriteFailed, if set, is invoked when a record fails to persist to the
// backing storage.
OnWriteFailed func(record *Record, err error)
}
Config holds configuration for the audit logger
type DatabaseConfig ¶
type DatabaseConfig struct {
TableName string // Custom table name (default: "audit_logs")
}
DatabaseConfig holds configuration for database storage
func DefaultDatabaseConfig ¶
func DefaultDatabaseConfig() *DatabaseConfig
DefaultDatabaseConfig returns default database configuration
type DatabaseStorage ¶
type DatabaseStorage struct {
// contains filtered or unexported fields
}
DatabaseStorage implements Storage interface for database-based audit logging Supports PostgreSQL and MySQL
func NewDatabaseStorage ¶
func NewDatabaseStorage(databaseURL string) (*DatabaseStorage, error)
NewDatabaseStorage creates a new database storage instance
func NewDatabaseStorageFromDB ¶
func NewDatabaseStorageFromDB(db *sql.DB, dbType string, config *DatabaseConfig) (*DatabaseStorage, error)
NewDatabaseStorageFromDB creates a new database storage from existing *sql.DB
func NewDatabaseStorageWithConfig ¶
func NewDatabaseStorageWithConfig(databaseURL string, config *DatabaseConfig) (*DatabaseStorage, error)
NewDatabaseStorageWithConfig creates a new database storage instance with config
func (*DatabaseStorage) Close ¶
func (s *DatabaseStorage) Close() error
Close closes the database connection
func (*DatabaseStorage) DB ¶
func (s *DatabaseStorage) DB() *sql.DB
DB returns the underlying database connection
func (*DatabaseStorage) DBType ¶
func (s *DatabaseStorage) DBType() string
DBType returns the database type
func (*DatabaseStorage) Query ¶
func (s *DatabaseStorage) Query(ctx context.Context, filter *QueryFilter) ([]*Record, error)
Query queries audit records from the database
type EventType ¶
type EventType string
EventType represents the type of audit event
const ( // Challenge lifecycle events EventChallengeCreated EventType = "challenge_created" EventChallengeVerified EventType = "challenge_verified" EventChallengeRevoked EventType = "challenge_revoked" EventChallengeExpired EventType = "challenge_expired" // Send events EventSendSuccess EventType = "send_success" EventSendFailed EventType = "send_failed" // Verification events EventVerificationSuccess EventType = "verification_success" EventVerificationFailed EventType = "verification_failed" // Authentication events EventLoginSuccess EventType = "login_success" EventLoginFailed EventType = "login_failed" EventLogout EventType = "logout" EventSessionCreate EventType = "session_create" EventSessionExpire EventType = "session_expire" // Authorization events EventAccessGranted EventType = "access_granted" EventAccessDenied EventType = "access_denied" // User management events EventUserCreated EventType = "user_created" EventUserUpdated EventType = "user_updated" EventUserDeleted EventType = "user_deleted" EventUserLocked EventType = "user_locked" EventUserUnlocked EventType = "user_unlocked" // Rate limit events EventRateLimited EventType = "rate_limited" // Generic events EventCustom EventType = "custom" )
Common event types for OTP/Authentication services
type FileStorage ¶
type FileStorage struct {
// contains filtered or unexported fields
}
FileStorage implements Storage interface for file-based audit logging Uses JSON Lines format (one JSON object per line)
func NewFileStorage ¶
func NewFileStorage(filePath string) (*FileStorage, error)
NewFileStorage creates a new file storage instance. filePath must come from trusted configuration only; do not pass user-controlled paths (path traversal or symlinks could write audit logs to unintended locations).
func (*FileStorage) Close ¶
func (s *FileStorage) Close() error
Close closes the file and releases resources
func (*FileStorage) FilePath ¶
func (s *FileStorage) FilePath() string
FilePath returns the file path
func (*FileStorage) Query ¶
func (s *FileStorage) Query(ctx context.Context, filter *QueryFilter) ([]*Record, error)
Query reads audit records from the file matching the filter Note: File storage query is simple and may be slow for large files For production use, consider using database storage
func (*FileStorage) Rotate ¶
func (s *FileStorage) Rotate() error
Rotate rotates the audit log file Creates a new file with timestamp suffix and reopens the main file
type Logger ¶
type Logger struct {
// contains filtered or unexported fields
}
Logger handles audit logging with optional async writing
func NewLoggerWithWriter ¶
NewLoggerWithWriter creates a new audit logger with async writer
func (*Logger) Log ¶
Log records an audit event. The provided record is never modified; a copy is made for masking and writing, so the caller may safely reuse the record.
func (*Logger) LogAccess ¶
func (l *Logger) LogAccess(ctx context.Context, eventType EventType, userID, resource string, result Result, opts ...RecordOption)
LogAccess logs an access control event
func (*Logger) LogAuth ¶
func (l *Logger) LogAuth(ctx context.Context, eventType EventType, userID string, result Result, opts ...RecordOption)
LogAuth logs an authentication event
func (*Logger) LogChallenge ¶
func (l *Logger) LogChallenge(ctx context.Context, eventType EventType, challengeID, userID string, result Result, opts ...RecordOption)
LogChallenge logs a challenge-related event
func (*Logger) Query ¶
Query queries audit records from storage. Filter is normalized (limit/offset) before being passed to storage so behavior is consistent across backends.
func (*Logger) SetLogCallback ¶
SetLogCallback sets a callback that is called for each log entry Useful for also logging to standard logger
type MultiStorage ¶
type MultiStorage struct {
// contains filtered or unexported fields
}
MultiStorage combines multiple storage backends
func NewMultiStorage ¶
func NewMultiStorage(storages ...Storage) *MultiStorage
NewMultiStorage creates a storage that writes to multiple backends
func (*MultiStorage) Query ¶
func (m *MultiStorage) Query(ctx context.Context, filter *QueryFilter) ([]*Record, error)
Query queries from the first storage backend
func (*MultiStorage) Storages ¶
func (m *MultiStorage) Storages() []Storage
Storages returns all storage backends
type NoopStorage ¶
type NoopStorage struct{}
NoopStorage is a no-op storage that discards all records
func (*NoopStorage) Query ¶
func (s *NoopStorage) Query(ctx context.Context, filter *QueryFilter) ([]*Record, error)
Query returns empty results
type QueryFilter ¶
type QueryFilter struct {
// Filter by event type
EventType string `json:"event_type,omitempty"`
// Filter by subject
UserID string `json:"user_id,omitempty"`
ChallengeID string `json:"challenge_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
// Filter by channel and result
Channel string `json:"channel,omitempty"`
Result string `json:"result,omitempty"`
// Time range filters (Unix timestamps)
StartTime int64 `json:"start_time,omitempty"`
EndTime int64 `json:"end_time,omitempty"`
// Filter by IP
IP string `json:"ip,omitempty"`
// Pagination
Limit int `json:"limit,omitempty"` // Maximum number of records (default: 100)
Offset int `json:"offset,omitempty"` // Offset for pagination (default: 0)
}
QueryFilter defines filter criteria for querying audit records
func DefaultQueryFilter ¶
func DefaultQueryFilter() *QueryFilter
DefaultQueryFilter returns a default query filter with sensible defaults
func (*QueryFilter) Normalize ¶
func (f *QueryFilter) Normalize()
Normalize ensures filter has valid values
func (*QueryFilter) WithChallengeID ¶
func (f *QueryFilter) WithChallengeID(challengeID string) *QueryFilter
WithChallengeID sets the challenge ID filter
func (*QueryFilter) WithChannel ¶
func (f *QueryFilter) WithChannel(channel string) *QueryFilter
WithChannel sets the channel filter
func (*QueryFilter) WithEventType ¶
func (f *QueryFilter) WithEventType(eventType string) *QueryFilter
WithEventType sets the event type filter
func (*QueryFilter) WithIP ¶
func (f *QueryFilter) WithIP(ip string) *QueryFilter
WithIP sets the IP filter
func (*QueryFilter) WithLimit ¶
func (f *QueryFilter) WithLimit(limit int) *QueryFilter
WithLimit sets the limit
func (*QueryFilter) WithOffset ¶
func (f *QueryFilter) WithOffset(offset int) *QueryFilter
WithOffset sets the offset
func (*QueryFilter) WithResult ¶
func (f *QueryFilter) WithResult(result string) *QueryFilter
WithResult sets the result filter
func (*QueryFilter) WithSessionID ¶
func (f *QueryFilter) WithSessionID(sessionID string) *QueryFilter
WithSessionID sets the session ID filter
func (*QueryFilter) WithTimeRange ¶
func (f *QueryFilter) WithTimeRange(startTime, endTime int64) *QueryFilter
WithTimeRange sets the time range filter
func (*QueryFilter) WithUserID ¶
func (f *QueryFilter) WithUserID(userID string) *QueryFilter
WithUserID sets the user ID filter
type Record ¶
type Record struct {
// Event identification
EventType EventType `json:"event_type"`
EventID string `json:"event_id,omitempty"` // Unique event identifier
// Subject identification
UserID string `json:"user_id,omitempty"`
ChallengeID string `json:"challenge_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
// Event details
Channel string `json:"channel,omitempty"` // sms, email, push, etc.
Destination string `json:"destination,omitempty"` // May be masked
Purpose string `json:"purpose,omitempty"` // login, reset, bind, etc.
Resource string `json:"resource,omitempty"` // Accessed resource
// Result
Result Result `json:"result"`
Reason string `json:"reason,omitempty"` // Failure reason
// Provider info (for external services)
Provider string `json:"provider,omitempty"`
ProviderMessageID string `json:"provider_message_id,omitempty"`
// Request context
IP string `json:"ip,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
RequestID string `json:"request_id,omitempty"`
TraceID string `json:"trace_id,omitempty"`
// Timing
Timestamp int64 `json:"timestamp"` // Unix timestamp
DurationMS int64 `json:"duration_ms,omitempty"` // Operation duration
// Extensible metadata
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
Record represents an audit log entry
func RecordFromJSON ¶
RecordFromJSON deserializes a record from JSON. Input larger than MaxRecordJSONSize is rejected to prevent memory exhaustion. Metadata is unmarshaled as map[string]interface{} without depth limit; do not pass untrusted JSON with deeply nested metadata.
func (*Record) Copy ¶ added in v1.1.0
Copy returns a shallow copy of the record. The caller's record is unchanged. Used internally so Log() can mask and write without mutating the original.
func (*Record) SetTimestamp ¶
SetTimestamp sets the timestamp (useful for testing)
func (*Record) WithChallengeID ¶
WithChallengeID sets the challenge ID
func (*Record) WithChannel ¶
WithChannel sets the channel (sms, email, etc.)
func (*Record) WithDestination ¶
WithDestination sets the destination (phone, email, etc.)
func (*Record) WithDuration ¶
WithDuration sets the operation duration in milliseconds
func (*Record) WithMetadata ¶
WithMetadata sets custom metadata
func (*Record) WithProvider ¶
WithProvider sets the provider info
func (*Record) WithPurpose ¶
WithPurpose sets the purpose (login, reset, etc.)
func (*Record) WithReason ¶
WithReason sets the failure reason
func (*Record) WithRequestID ¶
WithRequestID sets the request ID
func (*Record) WithResource ¶
WithResource sets the accessed resource
func (*Record) WithSessionID ¶
WithSessionID sets the session ID
func (*Record) WithTraceID ¶
WithTraceID sets the trace ID
func (*Record) WithUserAgent ¶
WithUserAgent sets the user agent
func (*Record) WithUserID ¶
WithUserID sets the user ID
type RecordOption ¶
type RecordOption func(*Record)
RecordOption is a function that modifies a record
func WithRecordChannel ¶
func WithRecordChannel(channel string) RecordOption
WithRecordChannel returns an option that sets the channel
func WithRecordDestination ¶
func WithRecordDestination(dest string) RecordOption
WithRecordDestination returns an option that sets the destination
func WithRecordIP ¶
func WithRecordIP(ip string) RecordOption
WithRecordIP returns an option that sets the IP
func WithRecordMetadata ¶
func WithRecordMetadata(key string, value interface{}) RecordOption
WithRecordMetadata returns an option that sets metadata
func WithRecordProvider ¶
func WithRecordProvider(provider, messageID string) RecordOption
WithRecordProvider returns an option that sets the provider
func WithRecordPurpose ¶
func WithRecordPurpose(purpose string) RecordOption
WithRecordPurpose returns an option that sets the purpose
func WithRecordReason ¶
func WithRecordReason(reason string) RecordOption
WithRecordReason returns an option that sets the reason
func WithRecordRequestID ¶
func WithRecordRequestID(requestID string) RecordOption
WithRecordRequestID returns an option that sets the request ID
func WithRecordTraceID ¶
func WithRecordTraceID(traceID string) RecordOption
WithRecordTraceID returns an option that sets the trace ID
func WithRecordUserAgent ¶
func WithRecordUserAgent(ua string) RecordOption
WithRecordUserAgent returns an option that sets the user agent
type RedisConfig ¶
type RedisConfig struct {
KeyPrefix string // Key prefix (default: "audit:")
TTL time.Duration // Time-to-live for records (default: 7 days)
}
RedisConfig holds configuration for Redis storage
func DefaultRedisConfig ¶
func DefaultRedisConfig() *RedisConfig
DefaultRedisConfig returns default Redis configuration
type RedisStorage ¶
type RedisStorage struct {
// contains filtered or unexported fields
}
RedisStorage implements Storage interface for Redis-based audit logging Suitable for short-term storage and quick access
func NewRedisStorage ¶
func NewRedisStorage(client *redis.Client) *RedisStorage
NewRedisStorage creates a new Redis storage instance
func NewRedisStorageWithConfig ¶
func NewRedisStorageWithConfig(client *redis.Client, config *RedisConfig) *RedisStorage
NewRedisStorageWithConfig creates a new Redis storage instance with config
func (*RedisStorage) Cleanup ¶
func (s *RedisStorage) Cleanup(ctx context.Context) (int64, error)
Cleanup removes expired keys from the index
func (*RedisStorage) Client ¶
func (s *RedisStorage) Client() *redis.Client
Client returns the underlying Redis client
func (*RedisStorage) KeyPrefix ¶
func (s *RedisStorage) KeyPrefix() string
KeyPrefix returns the key prefix
func (*RedisStorage) Query ¶
func (s *RedisStorage) Query(ctx context.Context, filter *QueryFilter) ([]*Record, error)
Query queries audit records from Redis
type Storage ¶
type Storage interface {
// Write writes an audit record to the storage backend
Write(ctx context.Context, record *Record) error
// Query queries audit records based on filter criteria
// Returns records matching the filter, ordered by timestamp (newest first)
Query(ctx context.Context, filter *QueryFilter) ([]*Record, error)
// Close closes the storage connection and releases resources
Close() error
}
Storage defines the interface for audit log storage backends
func NewStorageFromType ¶
func NewStorageFromType(storageType StorageType, opts *StorageOptions) (Storage, error)
NewStorageFromType creates a storage instance based on type
type StorageOptions ¶
type StorageOptions struct {
// File storage options
FilePath string
// Database storage options
DatabaseURL string
TableName string
// Redis storage options
RedisClient *redis.Client
RedisPrefix string
RedisTTL time.Duration
}
StorageOptions holds options for creating storage
type StorageType ¶
type StorageType string
StorageType represents the type of storage backend
const ( StorageTypeFile StorageType = "file" StorageTypeDatabase StorageType = "database" StorageTypeDB StorageType = "db" // Alias for database StorageTypeRedis StorageType = "redis" StorageTypeNone StorageType = "none" )
func ParseStorageType ¶
func ParseStorageType(s string) StorageType
ParseStorageType parses a storage type string
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer handles asynchronous writing of audit records to persistent storage
func NewWriter ¶
func NewWriter(storage Storage, config *WriterConfig) *Writer
NewWriter creates a new asynchronous audit writer
func (*Writer) Enqueue ¶
Enqueue enqueues an audit record for asynchronous writing. Returns false if record is nil, the writer is stopped, or the queue is full (non-blocking). Safe to call after Stop(); will return false instead of panicking.
func (*Writer) OnEnqueueFailed ¶
OnEnqueueFailed sets a callback for when enqueue fails (queue full)
func (*Writer) OnWriteFailed ¶
OnWriteFailed sets a callback for when write fails
func (*Writer) Stop ¶
Stop stops the writer workers gracefully.
Ordering matters here, and used to be wrong in two ways.
The context was cancelled *before* the queue was drained, so every record still queued was written with a dead context and the storage backend rejected it immediately -- the carefully written drain loop lost everything it was draining.
The queue was also closed while an Enqueue could still be mid-send, because Enqueue checked the stopped flag under the lock and then sent after releasing it. That interleaving panics with "send on closed channel", despite Enqueue documenting itself as safe to call after Stop.
Now: mark stopped and wait for in-flight Enqueue calls to finish (the write lock does that), signal the drain, let workers finish with a live context, and only then cancel and close storage. The queue is never closed.
type WriterConfig ¶
type WriterConfig struct {
QueueSize int // Size of the async queue (default: 1000)
Workers int // Number of worker goroutines (default: 2)
StopTimeout time.Duration // Timeout for graceful shutdown (default: 10s)
}
WriterConfig holds configuration for the async writer
func DefaultWriterConfig ¶
func DefaultWriterConfig() *WriterConfig
DefaultWriterConfig returns default writer configuration