18 KiB
Log
A robust, buffered, rotating file logger for Go applications, configured via the LixenWraith/config package. Designed for performance and reliability with features like disk management, log retention, and asynchronous processing using atomic operations and channels.
Features
- Buffered Asynchronous Logging: Logs are sent non-blockingly to a buffered channel, processed by a dedicated background goroutine for minimal application impact. Uses atomic operations for state management, avoiding mutexes in the logging hot path.
- External Configuration: Fully configured using
github.com/LixenWraith/config, allowing settings via TOML files and CLI overrides managed centrally. - Automatic File Rotation: Rotates log files when they reach a configurable size (
max_size_mb). - Disk Space Management:
- Monitors total log directory size against a limit (
max_total_size_mb). - Monitors available disk space against a minimum requirement (
min_disk_free_mb). - Automatically attempts to delete the oldest log files (by modification time) to stay within limits during periodic checks or when writes fail.
- Temporarily pauses logging if space cannot be freed, logging an error message.
- Monitors total log directory size against a limit (
- Adaptive Disk Check Interval: Optionally adjusts the frequency of disk space checks based on logging load (
enable_adaptive_interval,disk_check_interval_ms,min_check_interval_ms,max_check_interval_ms) to balance performance and responsiveness. - Periodic Flushing: Automatically flushes the log buffer to disk at a configured interval (
flush_interval_ms) using a timer. - Log Retention: Automatically deletes log files older than a configured duration (
retention_period_hrs), checked periodically via a timer (retention_check_mins). Relies on file modification time. - Dropped Log Detection: If the internal buffer fills under high load, logs are dropped, and a summary message indicating the number of drops is logged later.
- Structured Logging: Supports both plain text (
txt) andjsonoutput formats. - Standard Log Levels: Provides
Debug,Info,Warn,Errorlevels (values matchslog). - Function Call Tracing: Optionally include function call traces in logs with configurable depth (
trace_depth) or enable temporarily via*Tracefunctions. - Simplified API: Public logging functions (
log.Info,log.Debug, etc.) do not requirecontext.Context. - Graceful Shutdown:
log.Shutdownsignals the background processor to stop by closing the log channel. It then waits for a brief, fixed duration (best-effort) before closing the file handle. Note: This is a best-effort flush; logs might be lost if flushing takes longer than the internal wait or if the application exits abruptly.
Installation
go get github.com/LixenWraith/log
go get github.com/LixenWraith/config
The config package has its own dependencies which will be fetched automatically.
Basic Usage
package main
import (
"fmt"
"os"
"sync"
"time"
"github.com/LixenWraith/config" // External config package
"github.com/LixenWraith/log" // This logger package
)
const configFile = "app_config.toml"
const logConfigPath = "logging" // Base path for logger settings in TOML/config
// Example app_config.toml content:
/*
[logging]
level = 0 # Info Level (0)
directory = "./app_logs"
format = "json"
extension = "log"
max_size_mb = 50
flush_interval_ms = 100
disk_check_interval_ms = 5000 # Example: Check disk every 5s
enable_adaptive_interval = true
# Other settings will use defaults registered by log.Init
*/
func main() {
// 1. Initialize the main config manager
cfg := config.New()
// Optional: Create a dummy config file if it doesn't exist
if _, err := os.Stat(configFile); os.IsNotExist(err) {
content := fmt.Sprintf("[%s]\n level = 0\n directory = \"./app_logs\"\n", logConfigPath)
os.WriteFile(configFile, []byte(content), 0644)
}
// 2. Load configuration (e.g., from file and/or CLI)
_, err := cfg.Load(configFile, os.Args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to load config file '%s': %v. Using defaults.\n", configFile, err)
}
// 3. Initialize the logger, passing the config instance and base path.
// log.Init registers necessary keys (e.g., "logging.level") with cfg.
err = log.Init(cfg, logConfigPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal: Failed to initialize logger: %v\n", err)
os.Exit(1)
}
fmt.Println("Logger initialized.")
// 4. Optionally save the merged config (defaults + file/CLI overrides)
err = cfg.Save(configFile) // Save back to the file
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: Failed to save config: %v\n", err)
}
// 5. Use the logger
log.Info("Application started", "pid", os.Getpid())
log.Debug("Debugging info", "value", 42) // Might be filtered by level
// Example concurrent logging
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
log.Info("Goroutine task started", "goroutine_id", id)
time.Sleep(time.Duration(id*10) * time.Millisecond)
log.InfoTrace(1, "Goroutine task finished", "goroutine_id", id)
}(i)
}
wg.Wait()
// ... application logic ...
// 6. Shutdown the logger gracefully before exit
fmt.Println("Shutting down...")
// Shutdown timeout is used internally for a brief wait, not a hard deadline for flushing.
shutdownTimeout := 2 * time.Second
err = log.Shutdown(shutdownTimeout) // Pass timeout (used for internal sleep)
if err != nil {
fmt.Fprintf(os.Stderr, "Logger shutdown error: %v\n", err)
}
fmt.Println("Shutdown complete.")
}
Configuration
The log package is configured via keys registered with the config.Config instance passed to log.Init. log.Init expects these keys relative to the basePath argument.
Key (basePath + Key) |
Type | Description | Default Value (Registered by log.Init) |
|---|---|---|---|
level |
int64 |
Minimum log level (-4=Debug, 0=Info, 4=Warn, 8=Error) | 0 (LevelInfo) |
name |
string |
Base name for log files | "log" |
directory |
string |
Directory to store log files | "./logs" |
format |
string |
Log file format ("txt", "json") |
"txt" |
extension |
string |
Log file extension (e.g., "log", "app") |
"log" |
show_timestamp |
bool |
Show timestamp in log entries | true |
show_level |
bool |
Show log level in entries | true |
buffer_size |
int64 |
Channel buffer capacity for log records | 1024 |
max_size_mb |
int64 |
Max size (MB) per log file before rotation | 10 |
max_total_size_mb |
int64 |
Max total size (MB) of log directory (0=unlimited) | 50 |
min_disk_free_mb |
int64 |
Min required free disk space (MB) (0=unlimited) | 100 |
flush_interval_ms |
int64 |
Interval (ms) to force flush buffer to disk via timer | 100 |
trace_depth |
int64 |
Function call trace depth (0=disabled, 1-10) | 0 |
retention_period_hrs |
float64 |
Hours to keep log files (0=disabled) | 0.0 |
retention_check_mins |
float64 |
Minutes between retention checks via timer (if enabled) | 60.0 |
disk_check_interval_ms |
int64 |
Base interval (ms) for periodic disk space checks via timer | 5000 |
enable_adaptive_interval |
bool |
Adjust disk check interval based on load (within min/max bounds) | true |
enable_periodic_sync |
bool |
Periodic sync with disk based on flush interval | false |
min_check_interval_ms |
int64 |
Minimum interval (ms) for adaptive disk checks | 100 |
max_check_interval_ms |
int64 |
Maximum interval (ms) for adaptive disk checks | 60000 |
Example TOML (config.toml)
# Main application settings
app_name = "My Service"
# Logger settings under the 'logging' base path
[logging]
level = -4 # Debug
directory = "/var/log/my_service"
format = "json"
extension = "log"
max_size_mb = 100
max_total_size_mb = 1024 # 1 GB total
min_disk_free_mb = 512 # 512 MB free required
flush_interval_ms = 100
trace_depth = 2
retention_period_hrs = 168.0 # 7 days (7 * 24)
retention_check_mins = 60.0
disk_check_interval_ms = 10000 # Check disk every 10 seconds
enable_adaptive_interval = false # Disable adaptive checks
# Other application settings
[database]
host = "db.example.com"
Your application would then initialize the logger like this:
cfg := config.New()
cfg.Load("config.toml", os.Args[1:]) // Load from file & CLI
log.Init(cfg, "logging") // Use "logging" as base path
cfg.Save("config.toml") // Save merged config
API Reference
Initialization
Init(cfg *config.Config, basePath string) errorInitializes or reconfigures the logger using settings from the providedconfig.Configinstance underbasePath. Registers required keys with defaults if not present. Handles reconfiguration safely, potentially restarting the background processor goroutine (e.g., ifbuffer_sizechanges). Must be called before logging. Thread-safe.InitWithDefaults(overrides ...string) errorInitializes or reconfigures the logger using built-in defaults, applying optional overrides provided as "key=value" strings. Useful for simple setups without a config file. Thread-safe.
Logging Functions
These functions accept ...any arguments, typically used as key-value pairs for structured logging (e.g., "user_id", 123, "status", "active"). They are non-blocking and read configuration/state using atomic operations.
Debug(args ...any): Logs at Debug level.Info(args ...any): Logs at Info level.Warn(args ...any): Logs at Warn level.Error(args ...any): Logs at Error level.
Trace Logging Functions
Temporarily enable function call tracing for a single log entry.
DebugTrace(depth int, args ...any): Logs Debug with trace.InfoTrace(depth int, args ...any): Logs Info with trace.WarnTrace(depth int, args ...any): Logs Warn with trace.ErrorTrace(depth int, args ...any): Logs Error with trace. (depthspecifies the number of stack frames, 0-10).
Other Logging Variants
Log(args ...any): Logs with timestamp, no level (uses Info internally), no trace.Message(args ...any): Logs raw message, no timestamp, no level, no trace.LogTrace(depth int, args ...any): Logs with timestamp and trace, no level.
Shutdown
Shutdown(timeout time.Duration) errorAttempts to gracefully shut down the logger. Sets atomic flags to prevent new logs, closes the internal log channel to signal the background processor, waits for a brief fixed duration (currently using theflush_interval_msconfiguration value,timeoutargument is used as a default if the interval is <= 0), and then closes the current log file. Returnsnilon success or an error if file operations fail. Note: This provides a best-effort flush; logs might be lost if disk I/O is slow or the application exits too quickly after calling Shutdown.
Constants
LevelDebug,LevelInfo,LevelWarn,LevelError(int64): Log level constants.
Implementation Details & Behavior
- Asynchronous Processing: Log calls (
log.Info, etc.) are non-blocking. They format alogRecordand attempt a non-blocking send to an internal buffered channel (ActiveLogChannel). A single background goroutine (processLogs) reads from this channel, serializes the record (to TXT or JSON using a reusable buffer), and writes it to the current log file. - Configuration Source: Relies on an initialized
github.com/LixenWraith/config.Configinstance passed tolog.Initor uses internal defaults withInitWithDefaults. It registers expected keys with "log." prefix and retrieves values using the config package's type-specific accessors (Int64, String, Bool, Float64). - State Management: Uses
sync.Mutex(initMu) only to protect initialization and reconfiguration logic. Usessync/atomicvariables extensively for runtime state (IsInitialized,CurrentFile,CurrentSize,DroppedLogs), allowing lock-free reads in logging functions and the processor loop. - Timers: Uses
time.Tickerinternally for:- Periodic buffer flushing (
flush_interval_ms). - Periodic log retention checks (
retention_check_mins). - Periodic and potentially adaptive disk space checks (
disk_check_interval_ms, etc.).
- Periodic buffer flushing (
- File Rotation: Triggered synchronously within
processLogswhen writing a record would exceedmax_size_mb. The old file is closed, a new one is created with a timestamped name, and the atomicCurrentFilepointer andCurrentSizeare updated. - Disk/Retention Checks:
performDiskCheckis called periodically by a timer and reactively if writes fail or a byte threshold is crossed. It checks total size and free space limits. If limits are exceeded andforceCleanupis true (for periodic checks), it callscleanOldLogs. If checks fail,DiskStatusOKis set to false, causing subsequent logs to be dropped until the condition resolves.cleanOldLogsdeletes the oldest files (by modification time, skipping the current file) until enough space is freed or no more files can be deleted.cleanExpiredLogsis called periodically by a timer based onretention_check_mins. It deletes files whose modification time is older thanretention_period_hrs.
- Shutdown Process:
Shutdownsets atomic flags (ShutdownCalled,LoggerDisabled) to prevent new logs.- It closes the current
ActiveLogChannel(obtained via atomic load). - It performs a fixed short sleep based on the configured
flush_interval_msas a best-effort attempt to allow the processor goroutine time to process remaining items in the channel buffer before the file is closed. - The
processLogsgoroutine detects the closed channel, performs a final file sync, and exits. Shutdownperforms finalSyncandCloseon the log file handle after the sleep.
Limitations, Caveats & Failure Modes
- Dependency: Requires
github.com/LixenWraith/configfor configuration vialog.Init. - Log Loss Scenarios:
- Buffer Full: If the application generates logs faster than they can be written to disk,
ActiveLogChannelfills up. Subsequent log calls will drop messages until space becomes available. A"Logs were dropped"message will be logged later. Increasebuffer_sizeor reduce logging volume. - Shutdown: The
Shutdownfunction uses a brief, fixed wait, not a guarantee that all logs are flushed. Logs remaining in the buffer or OS buffers afterShutdownreturns might be lost, especially under heavy load or slow disk I/O. Ensure critical logs are flushed before shutdown if necessary (though this logger doesn't provide an explicit flush mechanism). - Application Exit: If the application exits abruptly before or during
log.Shutdown, buffered logs will likely be lost. - Disk Full (Unrecoverable): If
performDiskCheckdetects low space andcleanOldLogscannot free enough space (e.g., no old files to delete, permissions issues),DiskStatusOKis set to false. Subsequent logs are dropped until the condition resolves. An error message is logged to stderr once when this state is entered.
- Buffer Full: If the application generates logs faster than they can be written to disk,
- Configuration Errors:
log.InitorInitWithDefaultswill return an error and fail if configuration values are invalid (e.g., negativemax_size_mb, invalidformat, bad override string) or if theconfig.Configinstance isnil(forInit). The application must handle these errors. - Cleanup Race Conditions: Under high load with frequent rotation/cleanup, benign
"failed to remove old log file ... no such file or directory"errors might appear in stderr if multiple cleanup attempts target the same file. - Retention Accuracy: Log retention is based on file modification time. External actions modifying old log files could interfere with accurate retention.
- Reconfiguration: Changing
buffer_sizerestarts the background processor, involving closing the old channel and creating a new one. Logs sent during this brief transition might be dropped. Other configuration changes are applied live where possible via atomic updates.
License
BSD-3-Clause