Overview
The Audit package provides comprehensive audit logging functionality for tracking user actions, system events, and security-relevant operations. It supports flexible metadata extraction, custom repositories, and includes a PostgreSQL implementation.
Service
New
Creates a new audit service with optional configuration.
Optional configuration functions:
WithRepository - Custom storage backend
WithMetadataExtractor - Function to extract metadata from context
WithActorExtractor - Function to extract actor from context
Returns a configured audit service
package main
import (
"context"
"database/sql"
"github.com/raystack/salt/auth/audit"
"github.com/raystack/salt/auth/audit/repositories"
)
func main() {
db, _ := sql.Open("postgres", "connection-string")
repo := repositories.NewPostgresRepository(db)
svc := audit.New(
audit.WithRepository(repo),
audit.WithActorExtractor(func(ctx context.Context) (string, error) {
// Extract actor from your auth context
return "[email protected]", nil
}),
audit.WithMetadataExtractor(func(ctx context.Context) map[string]interface{} {
return map[string]interface{}{
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0",
}
}),
)
// Initialize repository (creates tables)
if err := repo.Init(context.Background()); err != nil {
panic(err)
}
}
Log
Records an audit log entry.
Context containing actor and metadata information
Description of the action being logged (e.g., “user.login”, “resource.delete”)
Arbitrary data associated with the action. Will be JSON-serialized.
ctx := context.Background()
ctx = audit.WithActor(ctx, "[email protected]")
ctx, _ = audit.WithMetadata(ctx, map[string]interface{}{
"ip_address": "192.168.1.1",
"resource_id": "resource-123",
})
err := svc.Log(ctx, "resource.create", map[string]interface{}{
"resource_type": "document",
"resource_name": "quarterly-report.pdf",
"size_bytes": 1024000,
})
if err != nil {
// Handle error
}
Context Functions
WithActor
Adds actor information to the context.
Actor identifier (typically user ID or email)
Context with actor information
ctx := context.Background()
ctx = audit.WithActor(ctx, "[email protected]")
// Use ctx in subsequent calls
Adds or merges metadata into the context.
md
map[string]interface{}
required
Metadata key-value pairs to add
Context with merged metadata
Error if existing metadata cannot be cast to map type
ctx := context.Background()
// Add initial metadata
ctx, _ = audit.WithMetadata(ctx, map[string]interface{}{
"ip_address": "192.168.1.1",
})
// Merge additional metadata
ctx, _ = audit.WithMetadata(ctx, map[string]interface{}{
"user_agent": "Mozilla/5.0",
})
// Both metadata values are now in context
Data Models
Log
Represents a single audit log entry.
Description of the action
Identifier of the user or system that performed the action
Action-specific data (JSON-serializable)
Contextual metadata (JSON-serializable)
type Log struct {
Timestamp time.Time `json:"timestamp"`
Action string `json:"action"`
Actor string `json:"actor"`
Data interface{} `json:"data"`
Metadata interface{} `json:"metadata"`
}
Configuration Options
WithRepository
Configures a custom storage backend.
Repository implementation with Init and Insert methods
Configures automatic metadata extraction from context.
fn
func(context.Context) map[string]interface{}
required
Function to extract metadata from request context
svc := audit.New(
audit.WithMetadataExtractor(func(ctx context.Context) map[string]interface{} {
// Extract from HTTP request, gRPC metadata, etc.
return map[string]interface{}{
"ip_address": extractIPFromContext(ctx),
"user_agent": extractUserAgentFromContext(ctx),
"request_id": extractRequestIDFromContext(ctx),
}
}),
)
Configures custom actor extraction from context.
fn
func(context.Context) (string, error)
required
Function to extract actor from context
svc := audit.New(
audit.WithActorExtractor(func(ctx context.Context) (string, error) {
user := getUserFromContext(ctx)
if user == nil {
return "system", nil
}
return user.Email, nil
}),
)
Repository Interface
Implement this interface to create custom storage backends.
type repository interface {
Init(context.Context) error
Insert(context.Context, *Log) error
}
Init
Initializes the repository (e.g., creates tables).
Context for the operation
Error if initialization fails
Insert
Stores an audit log entry.
Context for the operation
PostgreSQL Repository
NewPostgresRepository
Creates a PostgreSQL-backed audit repository.
Repository implementation for PostgreSQL
import (
"database/sql"
_ "github.com/lib/pq"
"github.com/raystack/salt/auth/audit/repositories"
)
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db?sslmode=disable")
if err != nil {
panic(err)
}
repo := repositories.NewPostgresRepository(db)
// Initialize (creates tables and indexes)
if err := repo.Init(context.Background()); err != nil {
panic(err)
}
Database Schema
The PostgreSQL repository creates the following schema:
CREATE TABLE IF NOT EXISTS audit_logs (
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
action TEXT NOT NULL,
actor TEXT NOT NULL,
data JSONB NOT NULL,
metadata JSONB NOT NULL
);
CREATE INDEX IF NOT EXISTS audit_logs_timestamp_idx ON audit_logs (timestamp);
CREATE INDEX IF NOT EXISTS audit_logs_action_idx ON audit_logs (action);
CREATE INDEX IF NOT EXISTS audit_logs_actor_idx ON audit_logs (actor);
Indexes:
timestamp - For time-based queries and retention policies
action - For filtering by action type
actor - For user activity tracking
DB Access
Get the underlying database connection:
repo := repositories.NewPostgresRepository(db)
db := repo.DB()
// Use db for custom queries
Usage Examples
Basic Logging
package main
import (
"context"
"database/sql"
"github.com/raystack/salt/auth/audit"
"github.com/raystack/salt/auth/audit/repositories"
)
func main() {
// Setup
db, _ := sql.Open("postgres", "connection-string")
repo := repositories.NewPostgresRepository(db)
repo.Init(context.Background())
svc := audit.New(audit.WithRepository(repo))
// Log an action
ctx := audit.WithActor(context.Background(), "[email protected]")
svc.Log(ctx, "user.login", map[string]interface{}{
"method": "oauth",
"success": true,
})
}
HTTP Middleware
func AuditMiddleware(svc *audit.Service) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract context
ctx := r.Context()
ctx = audit.WithActor(ctx, getUserEmail(r))
ctx, _ = audit.WithMetadata(ctx, map[string]interface{}{
"ip_address": r.RemoteAddr,
"user_agent": r.UserAgent(),
"method": r.Method,
"path": r.URL.Path,
})
// Log request
svc.Log(ctx, "http.request", map[string]interface{}{
"url": r.URL.String(),
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
gRPC Interceptor
import (
"google.golang.org/grpc"
"github.com/raystack/salt/auth/audit"
)
func UnaryAuditInterceptor(svc *audit.Service) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
ctx = audit.WithActor(ctx, getActorFromGRPCContext(ctx))
ctx, _ = audit.WithMetadata(ctx, map[string]interface{}{
"method": info.FullMethod,
})
resp, err := handler(ctx, req)
svc.Log(ctx, "grpc.call", map[string]interface{}{
"method": info.FullMethod,
"success": err == nil,
})
return resp, err
}
}
Custom Repository
import (
"context"
"github.com/raystack/salt/auth/audit"
)
type CustomRepository struct {
// Your storage backend
}
func (r *CustomRepository) Init(ctx context.Context) error {
// Initialize storage
return nil
}
func (r *CustomRepository) Insert(ctx context.Context, log *audit.Log) error {
// Store log entry
return nil
}
// Use custom repository
svc := audit.New(audit.WithRepository(&CustomRepository{}))
Error Handling
Failed to cast existing metadata to map[string]interface type
ctx, err := audit.WithMetadata(ctx, metadata)
if err != nil {
if errors.Is(err, audit.ErrInvalidMetadata) {
// Handle metadata type error
}
}
Best Practices
Action Naming
Use hierarchical action names for better filtering:
// Good
svc.Log(ctx, "user.login", data)
svc.Log(ctx, "user.logout", data)
svc.Log(ctx, "resource.create", data)
svc.Log(ctx, "resource.delete", data)
svc.Log(ctx, "permission.grant", data)
// Can query by prefix: "user.*", "resource.*"
Structure metadata consistently:
metadata := map[string]interface{}{
// Request context
"ip_address": "192.168.1.1",
"user_agent": "Mozilla/5.0",
"request_id": "req-123",
// Application context
"organization": "org-456",
"workspace": "ws-789",
// System context
"service": "api-server",
"version": "1.0.0",
}
Data Structure
Keep action data focused and JSON-serializable:
data := map[string]interface{}{
"resource_id": "res-123",
"resource_type": "document",
"old_value": "draft",
"new_value": "published",
"changed_fields": []string{"status", "publish_date"},
}
- Use async logging for high-throughput applications
- Implement log buffering for batch inserts
- Set up appropriate PostgreSQL indexes for your query patterns
- Consider data retention policies for the audit_logs table
Security
- Never log sensitive data (passwords, tokens, PII) in plaintext
- Implement access controls on the audit_logs table
- Encrypt sensitive fields if logging is required
- Set up monitoring and alerting on audit log anomalies