Documentation Index
Fetch the complete documentation index at: https://mintlify.com/raystack/salt/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Salt’s audit logging system provides comprehensive tracking of user actions with support for custom metadata, actor extraction, and multiple storage backends including PostgreSQL.
Core Concepts
- Actor: The user or service performing the action
- Action: A string identifier for the operation being performed
- Data: Arbitrary data associated with the action
- Metadata: Additional context information (e.g., IP address, user agent)
- Timestamp: When the action occurred
Service
Creating an Audit Service
func New(opts ...AuditOption) *Service
Creates a new audit logging service with configurable options.
Parameters:
opts - Variable number of AuditOption functions to configure the service
Returns: Configured *Service instance
Example: Basic Setup
import (
"context"
"database/sql"
"log"
"github.com/raystack/salt/auth/audit"
"github.com/raystack/salt/auth/audit/repositories"
_ "github.com/lib/pq"
)
func setupAuditLogging() *audit.Service {
// Connect to PostgreSQL
db, err := sql.Open("postgres", "postgresql://user:pass@localhost/db")
if err != nil {
log.Fatal(err)
}
// Create repository
repo := repositories.NewPostgresRepository(db)
// Initialize database schema
if err := repo.Init(context.Background()); err != nil {
log.Fatal(err)
}
// Create audit service
return audit.New(
audit.WithRepository(repo),
)
}
Logging Actions
Log Method
func (s *Service) Log(
ctx context.Context,
action string,
data interface{},
) error
Parameters:
ctx - Context containing actor and metadata information
action - String identifier for the action (e.g., “user.login”, “resource.delete”)
data - Any data to associate with the action
Returns: Error if logging fails
Example: Logging User Actions
import (
"context"
"github.com/raystack/salt/auth/audit"
)
func handleUserLogin(auditSvc *audit.Service, userID string) error {
ctx := context.Background()
// Add actor to context
ctx = audit.WithActor(ctx, userID)
// Log the login action
return auditSvc.Log(ctx, "user.login", map[string]interface{}{
"method": "oauth2",
"ip": "192.168.1.100",
})
}
func handleResourceDeletion(auditSvc *audit.Service, userID, resourceID string) error {
ctx := audit.WithActor(context.Background(), userID)
return auditSvc.Log(ctx, "resource.delete", map[string]interface{}{
"resource_id": resourceID,
"resource_type": "dataset",
})
}
Context Management
WithActor
Adds actor information to the context.
func WithActor(ctx context.Context, actor string) context.Context
Parameters:
ctx - Parent context
actor - Actor identifier (e.g., user ID, service name)
Returns: New context with actor information
Adds or appends metadata to the context.
func WithMetadata(
ctx context.Context,
md map[string]interface{},
) (context.Context, error)
Parameters:
ctx - Parent context
md - Metadata map to add
Returns: New context with metadata and error if existing metadata is invalid
Example: Adding Context Information
import (
"context"
"net/http"
"github.com/raystack/salt/auth/audit"
)
func auditHTTPRequest(auditSvc *audit.Service, r *http.Request) error {
ctx := context.Background()
// Add actor
ctx = audit.WithActor(ctx, r.Header.Get("X-User-ID"))
// Add request metadata
ctx, err := audit.WithMetadata(ctx, map[string]interface{}{
"ip": r.RemoteAddr,
"user_agent": r.UserAgent(),
"method": r.Method,
"path": r.URL.Path,
})
if err != nil {
return err
}
// Log the request
return auditSvc.Log(ctx, "http.request", nil)
}
Configuration Options
WithRepository
Configures the storage backend for audit logs.
func WithRepository(r repository) AuditOption
Example:
repo := repositories.NewPostgresRepository(db)
auditSvc := audit.New(
audit.WithRepository(repo),
)
Configures automatic metadata extraction from context.
func WithMetadataExtractor(
fn func(context.Context) map[string]interface{},
) AuditOption
Example:
// Extract metadata from custom context
metadataExtractor := func(ctx context.Context) map[string]interface{} {
return map[string]interface{}{
"tenant_id": getTenantID(ctx),
"region": getRegion(ctx),
"trace_id": getTraceID(ctx),
}
}
auditSvc := audit.New(
audit.WithRepository(repo),
audit.WithMetadataExtractor(metadataExtractor),
)
Configures custom actor extraction logic.
func WithActorExtractor(
fn func(context.Context) (string, error),
) AuditOption
Example:
// Extract actor from JWT claims
actorExtractor := func(ctx context.Context) (string, error) {
claims, ok := ctx.Value("jwt_claims").(map[string]interface{})
if !ok {
return "", nil
}
if sub, ok := claims["sub"].(string); ok {
return sub, nil
}
return "", nil
}
auditSvc := audit.New(
audit.WithRepository(repo),
audit.WithActorExtractor(actorExtractor),
)
Data Models
Log Structure
From audit/model.go:5-11:
type Log struct {
Timestamp time.Time `json:"timestamp"`
Action string `json:"action"`
Actor string `json:"actor"`
Data interface{} `json:"data"`
Metadata interface{} `json:"metadata"`
}
Example Log Entry
{
"timestamp": "2026-03-04T08:00:00Z",
"action": "user.login",
"actor": "user-123",
"data": {
"method": "oauth2",
"provider": "google"
},
"metadata": {
"ip": "192.168.1.100",
"user_agent": "Mozilla/5.0..."
}
}
PostgreSQL Repository
NewPostgresRepository
func NewPostgresRepository(db *sql.DB) *PostgresRepository
Creates a PostgreSQL-backed audit log repository.
Parameters:
db - PostgreSQL database connection
Returns: *PostgresRepository instance
Database Schema
From repositories/postgres.go:36-48:
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);
Example: Full PostgreSQL Setup
import (
"context"
"database/sql"
"log"
"github.com/raystack/salt/auth/audit"
"github.com/raystack/salt/auth/audit/repositories"
_ "github.com/lib/pq"
)
func main() {
// Connect to database
connStr := "postgresql://user:password@localhost:5432/myapp?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create and initialize repository
repo := repositories.NewPostgresRepository(db)
if err := repo.Init(context.Background()); err != nil {
log.Fatal(err)
}
// Create audit service with custom extractors
auditSvc := audit.New(
audit.WithRepository(repo),
audit.WithMetadataExtractor(func(ctx context.Context) map[string]interface{} {
return map[string]interface{}{
"app_version": "1.0.0",
"environment": "production",
}
}),
)
// Use the service
ctx := audit.WithActor(context.Background(), "admin-user")
if err := auditSvc.Log(ctx, "system.startup", nil); err != nil {
log.Fatal(err)
}
log.Println("Audit log recorded successfully")
}
Advanced Usage
Middleware Pattern
import (
"net/http"
"time"
"github.com/raystack/salt/auth/audit"
)
func AuditMiddleware(auditSvc *audit.Service) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Create audit context
ctx := audit.WithActor(r.Context(), r.Header.Get("X-User-ID"))
ctx, _ = audit.WithMetadata(ctx, map[string]interface{}{
"ip": r.RemoteAddr,
"user_agent": r.UserAgent(),
"method": r.Method,
"path": r.URL.Path,
})
// Call next handler
next.ServeHTTP(w, r.WithContext(ctx))
// Log request
_ = auditSvc.Log(ctx, "http.request", map[string]interface{}{
"duration_ms": time.Since(start).Milliseconds(),
})
})
}
}
Bulk Logging Pattern
func bulkImport(auditSvc *audit.Service, userID string, items []string) error {
ctx := audit.WithActor(context.Background(), userID)
// Log start of bulk operation
if err := auditSvc.Log(ctx, "bulk.import.start", map[string]interface{}{
"item_count": len(items),
}); err != nil {
return err
}
// Process items...
successCount := 0
for _, item := range items {
// Process item
successCount++
}
// Log completion
return auditSvc.Log(ctx, "bulk.import.complete", map[string]interface{}{
"total": len(items),
"success": successCount,
"failed": len(items) - successCount,
})
}
Action Naming Conventions
Follow a consistent naming pattern for actions:
// Resource-based actions
"user.create"
"user.update"
"user.delete"
"user.login"
"user.logout"
// System actions
"system.startup"
"system.shutdown"
"config.update"
// Data operations
"data.export"
"data.import"
"data.query"
Querying Audit Logs
-- Find all actions by a specific user
SELECT * FROM audit_logs
WHERE actor = 'user-123'
ORDER BY timestamp DESC
LIMIT 100;
-- Find all delete operations
SELECT * FROM audit_logs
WHERE action LIKE '%.delete'
ORDER BY timestamp DESC;
-- Find actions in a time range
SELECT * FROM audit_logs
WHERE timestamp BETWEEN '2026-03-01' AND '2026-03-31'
ORDER BY timestamp DESC;
-- Query JSON data
SELECT * FROM audit_logs
WHERE data->>'resource_type' = 'dataset'
AND action = 'resource.delete';
Best Practices
- Consistent Action Names: Use a hierarchical naming scheme (e.g.,
resource.action)
- Include Context: Always add relevant metadata for debugging and compliance
- Async Logging: Consider async logging for high-throughput applications
- Data Retention: Implement log retention policies based on compliance requirements
- PII Handling: Be careful not to log sensitive personal information
- Error Handling: Log audit failures separately to ensure visibility
- Indexing: The PostgreSQL repository creates indexes on
timestamp, action, and actor
- Batch Writes: Consider batching writes for high-volume scenarios
- Partitioning: Use table partitioning for large audit log tables
- Archiving: Implement archiving strategies for historical logs
References
- Source:
~/workspace/source/auth/audit/audit.go
- Model:
~/workspace/source/auth/audit/model.go
- PostgreSQL Repository:
~/workspace/source/auth/audit/repositories/postgres.go