This guide shows typical production routing patterns for structured logging with mtlog.
Route logs differently based on environment (development vs production).
// Development: Everything to console with colors
// Production: Errors to alerts, everything to file
func SetupLogging(environment string) core.Logger {
if environment == "development" {
return mtlog.New(
mtlog.WithConsoleTheme("dev"),
mtlog.WithMinimumLevel(core.DebugLevel),
)
}
// Production setup
alertSink, _ := sinks.NewFileSink("alerts.log")
allSink, _ := sinks.NewFileSink("app.log")
router := sinks.NewRouterSink(sinks.AllMatch,
sinks.Route{
Name: "critical-alerts",
Predicate: sinks.NewPredicateBuilder().
Level(core.ErrorLevel).
And().Property("Alert").
Build(),
Sink: alertSink,
},
sinks.Route{
Name: "all-events",
Predicate: func(*core.LogEvent) bool { return true },
Sink: allSink,
},
)
return mtlog.New(
mtlog.WithSink(router),
mtlog.WithMinimumLevel(core.InformationLevel),
)
}Separate audit events for compliance while maintaining operational logs.
func SetupComplianceLogging() core.Logger {
// Audit sink with durable buffering
auditSink, _ := sinks.NewFileSink("audit/audit.log")
durableAudit := sinks.NewDurableSink(auditSink, sinks.DurableOptions{
BufferPath: "audit/buffer",
MaxBufferSize: 100 * 1024 * 1024, // 100MB
})
// Regular operations sink
opsSink, _ := sinks.NewRollingFileSink(sinks.RollingFileOptions{
FilePath: "logs/app.log",
RollingInterval: sinks.RollingIntervalDaily,
})
// Router with AllMatch for compliance
router := sinks.NewRouterSink(sinks.AllMatch,
sinks.Route{
Name: "audit",
Predicate: sinks.PropertyPredicate("Audit"),
Sink: durableAudit,
},
sinks.Route{
Name: "operations",
Predicate: func(*core.LogEvent) bool { return true },
Sink: opsSink,
},
)
return mtlog.New(
mtlog.WithSink(router),
mtlog.WithProperty("Application", "ComplianceApp"),
)
}
// Usage
log.With("Audit", true).Information("User {UserId} accessed {Resource}", userId, resource)Route slow operations and metrics to specialized sinks.
func SetupPerformanceLogging() core.Logger {
// Metrics sink (could be Elasticsearch, Splunk, etc.)
metricsSink, _ := sinks.NewElasticsearchSink(
"http://localhost:9200",
sinks.WithElasticsearchIndex("metrics"),
)
// Slow query sink
slowQuerySink, _ := sinks.NewFileSink("performance/slow-queries.log")
// Console for development
consoleSink := sinks.NewConsoleSink()
router := sinks.NewRouterSink(sinks.AllMatch,
// Route metrics to Elasticsearch
sinks.Route{
Name: "metrics",
Predicate: sinks.PropertyPredicate("Metric"),
Sink: metricsSink,
},
// Route slow operations to dedicated file
sinks.Route{
Name: "slow-ops",
Predicate: func(e *core.LogEvent) bool {
if elapsed, ok := e.Properties["Elapsed"].(time.Duration); ok {
return elapsed > 100*time.Millisecond
}
return false
},
Sink: slowQuerySink,
},
// Everything to console for monitoring
sinks.Route{
Name: "console",
Predicate: func(*core.LogEvent) bool { return true },
Sink: consoleSink,
},
)
return mtlog.New(mtlog.WithSink(router))
}
// Usage
start := time.Now()
// ... perform operation ...
elapsed := time.Since(start)
log.With("Elapsed", elapsed, "Metric", true).
Information("Query executed in {Elapsed}", elapsed)Route logs based on tenant for isolation and billing.
func SetupMultiTenantLogging() core.Logger {
// Create tenant-specific sinks
tenantSinks := map[string]core.LogEventSink{
"tenant-a": createTenantSink("tenant-a"),
"tenant-b": createTenantSink("tenant-b"),
"tenant-c": createTenantSink("tenant-c"),
}
// Default sink for unknown tenants
defaultSink, _ := sinks.NewFileSink("logs/unknown-tenant.log")
// Create routes for each tenant
var routes []sinks.Route
for tenantId, sink := range tenantSinks {
tid := tenantId // Capture for closure
routes = append(routes, sinks.Route{
Name: fmt.Sprintf("tenant-%s", tid),
Predicate: sinks.PropertyValuePredicate("TenantId", tid),
Sink: sink,
})
}
router := sinks.NewRouterSinkWithDefault(
sinks.FirstMatch,
defaultSink,
routes...,
)
return mtlog.New(mtlog.WithSink(router))
}
func createTenantSink(tenantId string) core.LogEventSink {
sink, _ := sinks.NewFileSink(fmt.Sprintf("logs/tenants/%s.log", tenantId))
// Could also route to tenant-specific Seq instance, S3 bucket, etc.
return sink
}
// Usage
tenantLogger := log.With("TenantId", request.TenantId)
tenantLogger.Information("Processing request for tenant")Route errors with intelligent escalation based on severity and frequency.
func SetupErrorAlerting() core.Logger {
// Immediate alerts for critical errors
pagingSink := &PagerDutySink{} // Custom sink for PagerDuty
// Slack for warnings
slackSink := &SlackSink{} // Custom sink for Slack
// File sink for all errors
errorFile, _ := sinks.NewFileSink("errors/all-errors.log")
router := sinks.NewRouterSink(sinks.AllMatch,
// Critical errors -> PagerDuty
sinks.Route{
Name: "pager",
Predicate: sinks.NewPredicateBuilder().
Level(core.ErrorLevel).
And().Property("Critical").
Build(),
Sink: pagingSink,
},
// Errors with Alert flag -> Slack
sinks.Route{
Name: "slack",
Predicate: sinks.NewPredicateBuilder().
Level(core.ErrorLevel).
And().Property("Alert").
Build(),
Sink: slackSink,
},
// All errors -> File
sinks.Route{
Name: "error-file",
Predicate: sinks.LevelPredicate(core.ErrorLevel),
Sink: errorFile,
},
)
return mtlog.New(mtlog.WithSink(router))
}
// Usage
log.With("Critical", true, "Alert", true).
Error("Database connection lost: {Error}", err)Use route statistics for monitoring and debugging.
func SetupLoggingWithStats() (*mtlog.Logger, *sinks.RouterSink) {
router := sinks.NewRouterSink(sinks.AllMatch,
sinks.Route{
Name: "errors",
Predicate: sinks.LevelPredicate(core.ErrorLevel),
Sink: createErrorSink(),
},
sinks.Route{
Name: "audit",
Predicate: sinks.PropertyPredicate("Audit"),
Sink: createAuditSink(),
},
sinks.Route{
Name: "metrics",
Predicate: sinks.PropertyPredicate("Metric"),
Sink: createMetricsSink(),
},
)
// Periodically log statistics
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
stats := router.GetStats()
fmt.Printf("Routing Stats - Total: %d, Errors: %d, Audit: %d, Metrics: %d, Dropped: %d\n",
stats.TotalEvents,
stats.RouteHits["errors"],
stats.RouteHits["audit"],
stats.RouteHits["metrics"],
stats.DroppedEvents,
)
}
}()
logger := mtlog.New(mtlog.WithSink(router))
return logger, router
}
// Test routing configuration
func TestRoutingConfig(router *sinks.RouterSink) {
testEvents := []struct {
name string
event *core.LogEvent
}{
{
name: "Error Event",
event: &core.LogEvent{
Level: core.ErrorLevel,
Properties: map[string]any{},
},
},
{
name: "Audit Event",
event: &core.LogEvent{
Level: core.InformationLevel,
Properties: map[string]any{"Audit": true},
},
},
}
for _, test := range testEvents {
matches := router.TestEvent(test.event)
fmt.Printf("%s would route to: %v\n", test.name, matches)
}
}Use explicit priority values to control route evaluation order for optimal performance.
func SetupPriorityRouting() core.Logger {
router := sinks.NewRouterSink(sinks.FirstMatch,
// Priority 10: Critical alerts (check first)
sinks.NewRoute("critical").
WithPriority(10).
When(sinks.NewPredicateBuilder().
Level(core.FatalLevel).
Or().Property("Critical").
Build()).
To(pagerDutySink),
// Priority 20: Errors
sinks.NewRoute("errors").
WithPriority(20).
When(sinks.LevelPredicate(core.ErrorLevel)).
To(errorSink),
// Priority 100: Info and above (default)
sinks.NewRoute("general").
WithPriority(100).
When(sinks.LevelPredicate(core.InformationLevel)).
To(generalSink),
// Priority 200: Debug (check last)
sinks.NewRoute("debug").
WithPriority(200).
When(sinks.LevelPredicate(core.DebugLevel)).
To(debugSink),
)
return mtlog.New(mtlog.WithSink(router))
}Group related routes together for better management and dynamic configuration.
func SetupGroupedRouting() (*mtlog.Logger, *sinks.RouterSink) {
router := sinks.NewRouterSink(sinks.AllMatch)
// Security-related routes
securityGroup := sinks.NewRouteGroup("security",
sinks.Route{
Name: "auth-failures",
Priority: 10,
Predicate: sinks.PropertyValuePredicate("EventType", "AuthFailure"),
Sink: securitySink,
},
sinks.Route{
Name: "access-violations",
Priority: 10,
Predicate: sinks.PropertyPredicate("AccessViolation"),
Sink: securitySink,
},
)
// Performance monitoring routes
performanceGroup := sinks.NewRouteGroup("performance",
sinks.Route{
Name: "slow-queries",
Priority: 50,
Predicate: func(e *core.LogEvent) bool {
if duration, ok := e.Properties["Duration"].(time.Duration); ok {
return duration > 1*time.Second
}
return false
},
Sink: performanceSink,
},
sinks.Route{
Name: "metrics",
Priority: 100,
Predicate: sinks.PropertyPredicate("Metric"),
Sink: metricsSink,
},
)
// Add groups to router
router.AddRouteGroup(securityGroup)
router.AddRouteGroup(performanceGroup)
// Dynamically enable/disable groups based on environment
if os.Getenv("DISABLE_PERFORMANCE_MONITORING") == "true" {
router.RemoveRouteGroup("performance")
}
return mtlog.New(mtlog.WithSink(router)), router
}
// Runtime group management
func ToggleSecurityLogging(router *sinks.RouterSink, enable bool) {
if enable {
router.AddRouteGroup(getSecurityGroup())
} else {
router.RemoveRouteGroup("security")
}
}Sample verbose logs in production while keeping all errors.
func SetupSampledLogging() core.Logger {
// Always log errors
errorSink, _ := sinks.NewFileSink("errors.log")
// Sample 10% of debug logs
debugSink, _ := sinks.NewFileSink("debug.log")
sampledDebugSink := sinks.NewConditionalSink(
func(e *core.LogEvent) bool {
// Simple sampling: hash of message
return e.Level <= core.DebugLevel &&
hash(e.MessageTemplate)%10 == 0
},
debugSink,
)
// All info and above
infoSink, _ := sinks.NewFileSink("app.log")
router := sinks.NewRouterSink(sinks.FirstMatch,
sinks.Route{
Name: "errors",
Predicate: sinks.LevelPredicate(core.ErrorLevel),
Sink: errorSink,
},
sinks.Route{
Name: "sampled-debug",
Predicate: func(e *core.LogEvent) bool { return e.Level <= core.DebugLevel },
Sink: sampledDebugSink,
},
sinks.Route{
Name: "info",
Predicate: func(*core.LogEvent) bool { return true },
Sink: infoSink,
},
)
return mtlog.New(mtlog.WithSink(router))
}Export router statistics to Prometheus for monitoring and alerting.
func SetupMetricsExport() {
// Create routers
mainRouter := sinks.NewRouterSink(sinks.FirstMatch, routes...)
auditRouter := sinks.NewRouterSink(sinks.AllMatch, auditRoutes...)
// Create metrics exporter
exporter := sinks.NewRouterMetricsExporter()
exporter.RegisterRouter("main", mainRouter)
exporter.RegisterRouter("audit", auditRouter)
// Start metrics server
go func() {
if err := sinks.StartMetricsServer(":9090", exporter); err != nil {
log.Printf("Failed to start metrics server: %v", err)
}
}()
}
// Prometheus queries for monitoring:
// - Rate of events per route: rate(mtlog_router_route_hits_total[5m])
// - Dropped events alert: mtlog_router_dropped_events_total > 0
// - Route distribution: mtlog_router_route_hits_total / mtlog_router_events_total
// Import the Grafana dashboard from dashboards/grafana-mtlog-dashboard.jsonImplement resilient logging with health checks and circuit breakers for unreliable sinks.
func SetupResilientLogging() core.Logger {
// Wrap unreliable sinks with circuit breakers
dbSink := createDatabaseSink()
protectedDbSink := sinks.NewCircuitBreakerSinkWithOptions(dbSink, sinks.CircuitBreakerOptions{
Name: "database",
FailureThreshold: 5, // Open after 5 failures
SuccessThreshold: 3, // Close after 3 successes in half-open
ResetTimeout: 30 * time.Second,
FallbackSink: sinks.NewFileSink("fallback.log"), // Fallback when open
OnStateChange: func(from, to sinks.CircuitState) {
log.Printf("Circuit breaker state changed: %s -> %s", from, to)
},
})
// Create router with health-checkable sinks
router := sinks.NewRouterSink(sinks.FirstMatch,
sinks.Route{
Name: "database",
Priority: 10,
Predicate: sinks.PropertyPredicate("Audit"),
Sink: protectedDbSink,
},
sinks.Route{
Name: "file",
Priority: 100,
Predicate: func(*core.LogEvent) bool { return true },
Sink: sinks.NewFileSink("app.log"),
},
)
// Start periodic health checks
ctx := context.Background()
router.PeriodicHealthCheck(ctx, 1*time.Minute, func(results map[string]sinks.HealthStatus) {
for route, status := range results {
if !status.Healthy {
log.Printf("Route %s unhealthy: %v", route, status.Error)
// Could trigger alerts or take corrective action
}
}
})
return mtlog.New(mtlog.WithSink(router))
}
// Implement HealthCheckable for custom sinks
type CustomSink struct {
client *SomeClient
}
func (cs *CustomSink) HealthCheck(ctx context.Context) error {
// Test connectivity
if err := cs.client.Ping(ctx); err != nil {
return fmt.Errorf("ping failed: %w", err)
}
return nil
}
// Manual health check endpoint for monitoring
func HealthCheckHandler(router *sinks.RouterSink) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
results := router.CheckHealth(ctx)
allHealthy := true
for _, status := range results {
if !status.Healthy {
allHealthy = false
break
}
}
if allHealthy {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(results)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
json.NewEncoder(w).Encode(results)
}
}
}When routes are mutually exclusive, use FirstMatch mode for better performance:
// Good: FirstMatch when routes don't overlap
router := sinks.NewRouterSink(sinks.FirstMatch,
sinks.ErrorRoute("errors", errorSink),
sinks.Route{
Name: "warnings",
Predicate: sinks.LevelPredicate(core.WarningLevel),
Sink: warningSink,
},
)When events need multiple destinations (audit, metrics, alerts), use AllMatch:
// Good: AllMatch for compliance and monitoring
router := sinks.NewRouterSink(sinks.AllMatch,
sinks.AuditRoute("audit", auditSink),
sinks.MetricRoute("metrics", metricsSink),
sinks.Route{
Name: "all",
Predicate: func(*core.LogEvent) bool { return true },
Sink: archiveSink,
},
)Prevent losing events by having a default sink or catch-all route:
// Good: Default sink for unmatched events
router := sinks.NewRouterSinkWithDefault(
sinks.FirstMatch,
defaultSink, // Catches everything that doesn't match
routes...,
)
// Or: Catch-all route
routes = append(routes, sinks.Route{
Name: "catch-all",
Predicate: func(*core.LogEvent) bool { return true },
Sink: defaultSink,
})For readable complex predicates, use the fluent builder:
// Good: Clear, readable predicate
predicate := sinks.NewPredicateBuilder().
Level(core.ErrorLevel).
And().Property("Critical").
And().Not().Property("Retry").
Build()
// Instead of: Hard to read nested functions
predicate := sinks.AndPredicate(
sinks.LevelPredicate(core.ErrorLevel),
sinks.PropertyPredicate("Critical"),
sinks.NotPredicate(sinks.PropertyPredicate("Retry")),
)In production, monitor routing statistics to ensure proper configuration:
// Expose metrics endpoint
http.HandleFunc("/metrics/logging", func(w http.ResponseWriter, r *http.Request) {
stats := router.GetStats()
json.NewEncoder(w).Encode(stats)
})Use TestEvent to verify routing logic:
// In tests
func TestLoggingRoutes(t *testing.T) {
router := setupProductionRouter()
criticalError := &core.LogEvent{
Level: core.ErrorLevel,
Properties: map[string]any{"Critical": true},
}
matches := router.TestEvent(criticalError)
assert.Contains(t, matches, "pagerduty")
assert.Contains(t, matches, "error-archive")
}In FirstMatch mode, put most likely matches first:
// Good: Common cases first
router := sinks.NewRouterSink(sinks.FirstMatch,
sinks.Route{
Name: "info", // 80% of events
Predicate: sinks.LevelPredicate(core.InformationLevel),
Sink: infoSink,
},
sinks.Route{
Name: "errors", // 15% of events
Predicate: sinks.LevelPredicate(core.ErrorLevel),
Sink: errorSink,
},
sinks.Route{
Name: "debug", // 5% of events
Predicate: sinks.LevelPredicate(core.DebugLevel),
Sink: debugSink,
},
)For expensive predicates, consider caching:
// Expensive predicate with caching
var userCache = &sync.Map{}
func isVIPUser(e *core.LogEvent) bool {
userId, ok := e.Properties["UserId"].(string)
if !ok {
return false
}
// Check cache
if cached, ok := userCache.Load(userId); ok {
return cached.(bool)
}
// Expensive check
isVIP := checkUserVIPStatus(userId)
userCache.Store(userId, isVIP)
return isVIP
}For single-condition filtering, ConditionalSink is more efficient than RouterSink:
// Good: Simple condition with ConditionalSink
errorOnlySink := sinks.NewConditionalSink(
sinks.LevelPredicate(core.ErrorLevel),
targetSink,
)
// Overkill: Router for single condition
router := sinks.NewRouterSink(sinks.FirstMatch,
sinks.ErrorRoute("errors", targetSink),
)