Skip to main content

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.
opts
...AuditOption
Optional configuration functions:
  • WithRepository - Custom storage backend
  • WithMetadataExtractor - Function to extract metadata from context
  • WithActorExtractor - Function to extract actor from context
Service
*Service
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.
ctx
context.Context
required
Context containing actor and metadata information
action
string
required
Description of the action being logged (e.g., “user.login”, “resource.delete”)
data
interface{}
required
Arbitrary data associated with the action. Will be JSON-serialized.
error
error
Error if logging fails
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.
ctx
context.Context
required
Parent context
actor
string
required
Actor identifier (typically user ID or email)
Context
context.Context
Context with actor information
ctx := context.Background()
ctx = audit.WithActor(ctx, "[email protected]")

// Use ctx in subsequent calls

WithMetadata

Adds or merges metadata into the context.
ctx
context.Context
required
Parent context
md
map[string]interface{}
required
Metadata key-value pairs to add
Context
context.Context
Context with merged metadata
error
error
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.
Timestamp
time.Time
required
When the action occurred
Action
string
required
Description of the action
Actor
string
Identifier of the user or system that performed the action
Data
interface{}
Action-specific data (JSON-serializable)
Metadata
interface{}
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.
r
repository
required
Repository implementation with Init and Insert methods
AuditOption
AuditOption
Configuration function

WithMetadataExtractor

Configures automatic metadata extraction from context.
fn
func(context.Context) map[string]interface{}
required
Function to extract metadata from request context
AuditOption
AuditOption
Configuration function
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),
        }
    }),
)

WithActorExtractor

Configures custom actor extraction from context.
fn
func(context.Context) (string, error)
required
Function to extract actor from context
AuditOption
AuditOption
Configuration function
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).
ctx
context.Context
required
Context for the operation
error
error
Error if initialization fails

Insert

Stores an audit log entry.
ctx
context.Context
required
Context for the operation
log
*Log
required
Audit log entry to store
error
error
Error if insertion fails

PostgreSQL Repository

NewPostgresRepository

Creates a PostgreSQL-backed audit repository.
db
*sql.DB
required
Database connection
PostgresRepository
*PostgresRepository
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

ErrInvalidMetadata
error
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.*"

Metadata Organization

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"},
}

Performance Considerations

  • 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

Build docs developers (and LLMs) love