Enhanced Logging Guide¶
ObjectFS provides a comprehensive structured logging system with support for log rotation, debug tracing, and component-specific log levels.
Table of Contents¶
Quick Start¶
Basic Structured Logger¶
import "github.com/scttfrdmn/objectfs/pkg/utils"
// Create logger with default configuration
config := utils.DefaultStructuredLoggerConfig()
logger, err := utils.NewStructuredLogger(config)
if err != nil {
log.Fatal(err)
}
defer logger.Close()
// Log messages
logger.Info("Application started")
logger.Error("Failed to connect", map[string]interface{}{
"host": "localhost",
"port": 8080,
"error": err.Error(),
})
With Log Rotation¶
config := &utils.StructuredLoggerConfig{
Level: utils.INFO,
Format: utils.FormatJSON,
IncludeCaller: true,
Rotation: &utils.RotationConfig{
Filename: "/var/log/objectfs/app.log",
MaxSize: 100, // MB
MaxAge: 7, // days
MaxBackups: 10,
Compress: true,
},
}
logger, err := utils.NewStructuredLogger(config)
if err != nil {
log.Fatal(err)
}
defer logger.Close()
Structured Logging¶
Log Levels¶
ObjectFS supports six log levels (from least to most severe):
- TRACE: Very detailed information for deep debugging
- DEBUG: Detailed information for debugging
- INFO: General informational messages
- WARN: Warning messages
- ERROR: Error messages
- FATAL: Critical errors that cause program exit
Logging with Fields¶
Add structured fields to log messages for better searchability and analysis:
// Single field
logger.Info("User logged in", map[string]interface{}{
"user_id": 12345,
"ip": "192.168.1.1",
})
// Using WithField for context
userLogger := logger.WithField("user_id", 12345)
userLogger.Info("Session started")
userLogger.Info("File uploaded")
// Both messages will include user_id=12345
// Using WithFields for multiple context fields
requestLogger := logger.WithFields(map[string]interface{}{
"request_id": "req-abc-123",
"user_id": 12345,
"session": "sess-xyz-789",
})
requestLogger.Info("Processing request")
requestLogger.Debug("Validating input")
Component-Specific Logging¶
Different components can have different log levels:
logger.SetLevel(utils.INFO) // Global level
// Set DEBUG level only for storage component
logger.SetComponentLevel("storage", utils.DEBUG)
// Create component loggers
storageLogger := logger.WithComponent("storage")
cacheLogger := logger.WithComponent("cache")
// Storage will log debug messages
storageLogger.Debug("Cache miss, fetching from S3") // Logged
// Cache will not log debug messages (uses global INFO level)
cacheLogger.Debug("Evicting expired entries") // Not logged
Output Formats¶
Text Format (Human-Readable)¶
Output example:
JSON Format (Machine-Parseable)¶
Output example:
{
"timestamp": "2025-10-16T10:30:45.123Z",
"level": "INFO",
"message": "User logged in",
"fields": {
"user_id": 12345,
"ip": "192.168.1.1"
},
"caller": "app.go:42"
}
Formatted Messages¶
Use format string methods for convenience:
logger.Infof("Connected to %s:%d", host, port)
logger.Errorf("Failed to read file %s: %v", filename, err)
logger.Debugf("Cache hit rate: %.2f%%", hitRate * 100)
Log Rotation¶
Configuration¶
rotation := &utils.RotationConfig{
Filename: "/var/log/objectfs/app.log",
MaxSize: 100, // Maximum size in MB before rotation
MaxAge: 30, // Maximum age in days (0 = no age limit)
MaxBackups: 10, // Maximum number of old files to keep (0 = keep all)
Compress: true, // Compress rotated files with gzip
LocalTime: true, // Use local time for backup timestamps
}
Rotation Triggers¶
Logs are automatically rotated when:
- File size exceeds
MaxSizemegabytes - File age exceeds
MaxAgedays
Backup File Naming¶
Rotated files are named with timestamps:
Manual Rotation¶
Force rotation programmatically:
if rotator, ok := logger.(*utils.LogRotator); ok {
err := rotator.ForceRotate()
if err != nil {
logger.Error("Failed to rotate logs", map[string]interface{}{
"error": err.Error(),
})
}
}
Cleanup Policy¶
Old log files are automatically cleaned up based on:
MaxBackups: Keeps only N most recent backup filesMaxAge: Deletes files older than N days
Both policies work together - a file is deleted if it exceeds either limit.
Debug Mode¶
Debug mode provides advanced tracing and profiling capabilities for troubleshooting.
Starting a Debug Session¶
import "github.com/scttfrdmn/objectfs/pkg/utils"
// Get the debug manager
dm := utils.GetDebugManager()
// Set the logger for debug events
dm.SetLogger(logger)
// Start a debug session
sessionID := "troubleshoot-slow-reads"
components := []string{"storage", "cache"} // Only track these components
maxEvents := 10000 // Maximum events to record
session := dm.StartSession(sessionID, components, maxEvents)
Recording Events¶
// Manual event recording
dm.RecordEvent("storage", "read", "Reading file from S3", map[string]interface{}{
"bucket": "my-bucket",
"key": "/path/to/file.txt",
"size": 1024,
})
// Using debug traces for operation timing
trace := utils.StartTrace(sessionID, "storage", "write", map[string]interface{}{
"path": "/test/file.txt",
})
// Perform operation...
time.Sleep(100 * time.Millisecond)
// End trace with success
trace.End("Write completed successfully")
// Or end with error
if err != nil {
trace.EndWithError(err)
}
Using Context for Automatic Tracing¶
// Add session ID to context
ctx := utils.WithContext(context.Background(), sessionID)
// Pass context through your call stack
processFile(ctx, "/path/to/file")
// Extract session ID in functions
func processFile(ctx context.Context, path string) error {
sessionID := utils.FromContext(ctx)
if sessionID != "" {
trace := utils.StartTrace(sessionID, "storage", "process_file", map[string]interface{}{
"path": path,
})
defer func() {
if err != nil {
trace.EndWithError(err)
} else {
trace.End("Processing completed")
}
}()
}
// ... actual processing ...
}
Analyzing Debug Data¶
// Get all recorded events
events := session.GetEvents()
for _, event := range events {
fmt.Printf("[%s] %s.%s: %s (took %v)\n",
event.Timestamp.Format(time.RFC3339),
event.Component,
event.Operation,
event.Message,
event.Duration,
)
}
// Get events for specific component
storageEvents := session.GetEventsByComponent("storage")
// Get session statistics
stats := session.GetStats()
fmt.Printf("Session ID: %s\n", stats["id"])
fmt.Printf("Total events: %d\n", stats["event_count"])
fmt.Printf("Duration: %v\n", stats["duration"])
if eventsByComp, ok := stats["events_by_component"].(map[string]int); ok {
for comp, count := range eventsByComp {
fmt.Printf(" %s: %d events\n", comp, count)
}
}
Capturing Runtime Profiles¶
// Enable runtime profiling
utils.EnableRuntimeProfiling()
defer utils.DisableRuntimeProfiling()
// Capture profiles during debug session
session.CaptureProfile("goroutine")
session.CaptureProfile("heap")
session.CaptureProfile("block")
session.CaptureProfile("mutex")
// Retrieve profile data
goroutineProfile := session.GetProfile("goroutine")
// Write to file or analyze
os.WriteFile("goroutine.prof", goroutineProfile, 0644)
Stopping Debug Sessions¶
// Stop session and get final results
finalSession := dm.StopSession(sessionID)
// Session data remains available after stopping
events := finalSession.GetEvents()
stats := finalSession.GetStats()
Configuration¶
Environment-Based Configuration¶
import "os"
func setupLogger() (*utils.StructuredLogger, error) {
levelStr := os.Getenv("LOG_LEVEL")
if levelStr == "" {
levelStr = "INFO"
}
level, err := utils.ParseLogLevel(levelStr)
if err != nil {
level = utils.INFO
}
format := utils.FormatText
if os.Getenv("LOG_FORMAT") == "json" {
format = utils.FormatJSON
}
config := &utils.StructuredLoggerConfig{
Level: level,
Format: format,
IncludeCaller: os.Getenv("LOG_CALLER") == "true",
IncludeStack: os.Getenv("LOG_STACK") == "true",
}
// Add rotation for file logging
if logFile := os.Getenv("LOG_FILE"); logFile != "" {
config.Rotation = &utils.RotationConfig{
Filename: logFile,
MaxSize: 100,
MaxAge: 7,
MaxBackups: 10,
Compress: true,
}
}
return utils.NewStructuredLogger(config)
}
YAML Configuration¶
# config.yaml
logging:
level: INFO
format: json
caller: true
stack: false
rotation:
filename: /var/log/objectfs/app.log
max_size_mb: 100
max_age_days: 7
max_backups: 10
compress: true
type LoggingConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
Caller bool `yaml:"caller"`
Stack bool `yaml:"stack"`
Rotation struct {
Filename string `yaml:"filename"`
MaxSize int64 `yaml:"max_size_mb"`
MaxAge int `yaml:"max_age_days"`
MaxBackups int `yaml:"max_backups"`
Compress bool `yaml:"compress"`
} `yaml:"rotation"`
}
func loadConfig(filename string) (*utils.StructuredLogger, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var cfg LoggingConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
level, _ := utils.ParseLogLevel(cfg.Level)
format := utils.FormatText
if cfg.Format == "json" {
format = utils.FormatJSON
}
config := &utils.StructuredLoggerConfig{
Level: level,
Format: format,
IncludeCaller: cfg.Caller,
IncludeStack: cfg.Stack,
}
if cfg.Rotation.Filename != "" {
config.Rotation = &utils.RotationConfig{
Filename: cfg.Rotation.Filename,
MaxSize: cfg.Rotation.MaxSize,
MaxAge: cfg.Rotation.MaxAge,
MaxBackups: cfg.Rotation.MaxBackups,
Compress: cfg.Rotation.Compress,
}
}
return utils.NewStructuredLogger(config)
}
Best Practices¶
1. Use Appropriate Log Levels¶
// TRACE: Very detailed, temporary debugging
logger.Trace("Entering function", map[string]interface{}{
"args": args,
})
// DEBUG: Development/troubleshooting info
logger.Debug("Cache lookup", map[string]interface{}{
"key": key,
"hit": true,
})
// INFO: General operational messages
logger.Info("Server started", map[string]interface{}{
"port": 8080,
"version": version,
})
// WARN: Something unexpected but not critical
logger.Warn("Slow operation detected", map[string]interface{}{
"operation": "s3_read",
"duration_ms": 5000,
})
// ERROR: Errors that need attention
logger.Error("Failed to write file", map[string]interface{}{
"path": path,
"error": err.Error(),
})
// FATAL: Critical errors, program exits
logger.Fatal("Cannot connect to database", map[string]interface{}{
"error": err.Error(),
})
2. Use Structured Fields Instead of String Formatting¶
// Bad: Information lost in formatted string
logger.Info(fmt.Sprintf("User %d logged in from %s", userID, ip))
// Good: Structured and searchable
logger.Info("User logged in", map[string]interface{}{
"user_id": userID,
"ip": ip,
})
3. Create Context Loggers for Related Operations¶
// Create logger with context for entire request
requestLogger := logger.WithFields(map[string]interface{}{
"request_id": requestID,
"user_id": userID,
"ip": req.RemoteAddr,
})
// All logs in request handling will include context
requestLogger.Info("Processing upload")
requestLogger.Debug("Validating file")
requestLogger.Info("Upload complete")
4. Handle Errors Consistently¶
func processFile(path string, logger *utils.StructuredLogger) error {
file, err := os.Open(path)
if err != nil {
logger.Error("Failed to open file", map[string]interface{}{
"path": path,
"error": err.Error(),
})
return fmt.Errorf("open file: %w", err)
}
defer file.Close()
// Process file...
return nil
}
5. Use Debug Mode for Complex Issues¶
// Enable debug session for specific request
if strings.HasPrefix(requestID, "debug-") {
dm := utils.GetDebugManager()
session := dm.StartSession(requestID, []string{"storage", "cache"}, 1000)
defer dm.StopSession(requestID)
ctx = utils.WithContext(ctx, requestID)
}
// Normal request processing with optional tracing
processRequest(ctx, req)
6. Flush Logs on Shutdown¶
func main() {
logger, err := setupLogger()
if err != nil {
log.Fatal(err)
}
// Ensure logs are flushed on exit
defer func() {
logger.Sync()
logger.Close()
}()
// Run application...
}
Examples¶
Example 1: HTTP Server with Request Logging¶
func loggingMiddleware(logger *utils.StructuredLogger) 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()
requestID := generateRequestID()
// Create request logger
reqLogger := logger.WithFields(map[string]interface{}{
"request_id": requestID,
"method": r.Method,
"path": r.URL.Path,
"remote_addr": r.RemoteAddr,
})
reqLogger.Info("Request started")
// Process request
next.ServeHTTP(w, r)
// Log completion
reqLogger.Info("Request completed", map[string]interface{}{
"duration_ms": time.Since(start).Milliseconds(),
})
})
}
}
Example 2: Background Job with Rotation¶
func runBackgroundJob(logger *utils.StructuredLogger) {
jobLogger := logger.WithComponent("background_job")
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
start := time.Now()
jobLogger.Info("Job started")
if err := performWork(); err != nil {
jobLogger.Error("Job failed", map[string]interface{}{
"error": err.Error(),
"duration": time.Since(start),
})
continue
}
jobLogger.Info("Job completed", map[string]interface{}{
"duration": time.Since(start),
})
}
}
Example 3: Debug Session for Performance Analysis¶
func analyzePerformance() {
// Start debug session
dm := utils.GetDebugManager()
sessionID := fmt.Sprintf("perf-analysis-%d", time.Now().Unix())
session := dm.StartSession(sessionID, nil, 10000)
ctx := utils.WithContext(context.Background(), sessionID)
// Run operations with tracing
for i := 0; i < 100; i++ {
trace := utils.StartTrace(sessionID, "storage", "read", map[string]interface{}{
"iteration": i,
})
performRead()
trace.End("Read completed")
}
// Analyze results
finalSession := dm.StopSession(sessionID)
events := finalSession.GetEvents()
// Calculate statistics
var totalDuration time.Duration
for _, event := range events {
totalDuration += event.Duration
}
avgDuration := totalDuration / time.Duration(len(events))
fmt.Printf("Average read duration: %v\n", avgDuration)
// Find slowest operations
sort.Slice(events, func(i, j int) bool {
return events[i].Duration > events[j].Duration
})
fmt.Println("\nTop 10 slowest operations:")
for i := 0; i < 10 && i < len(events); i++ {
fmt.Printf("%d. %v - %s\n", i+1, events[i].Duration, events[i].Message)
}
}
Troubleshooting¶
Log File Permission Issues¶
// Ensure log directory exists and is writable
logDir := filepath.Dir(config.Rotation.Filename)
if err := os.MkdirAll(logDir, 0755); err != nil {
return fmt.Errorf("create log directory: %w", err)
}
Rotation Not Working¶
Check that:
MaxSizeorMaxAgeis set (both 0 means no rotation)- Log file path is writable
- Sufficient disk space available
// Test rotation manually
if rotator, ok := logger.(*utils.LogRotator); ok {
if err := rotator.ForceRotate(); err != nil {
fmt.Printf("Rotation test failed: %v\n", err)
}
}
High Memory Usage in Debug Mode¶
Limit event recording:
// Set smaller max events
session := dm.StartSession(sessionID, components, 1000) // Instead of 10000
// Or filter specific components
components := []string{"storage"} // Only track storage
session := dm.StartSession(sessionID, components, 5000)
Missing Log Messages¶
Check log levels: