From c809396455ff3e0af0cca5aad362a8b01b926243d136b11158109f57dcd6fe4d Mon Sep 17 00:00:00 2001 From: LixenWraith Date: Tue, 22 Apr 2025 18:48:29 -0400 Subject: [PATCH] e1.0.1 Minor feature add, file restructure. --- README.md | 43 +++---- config.go | 2 + default.go | 98 +++++++++++++++ format.go | 2 +- go.mod | 1 - go.sum | 6 - interface.go | 1 + logger.go | 343 +++++++++------------------------------------------ processor.go | 11 +- state.go | 213 ++++++++++++++++++++++++++++++++ utility.go | 7 +- 11 files changed, 411 insertions(+), 316 deletions(-) create mode 100644 default.go diff --git a/README.md b/README.md index b16407a..5d88c2f 100644 --- a/README.md +++ b/README.md @@ -130,27 +130,28 @@ func main() { 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` | -| `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` | +| 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`)** diff --git a/config.go b/config.go index 6ee7d92..1617fb3 100644 --- a/config.go +++ b/config.go @@ -33,6 +33,7 @@ type Config struct { // Disk check settings DiskCheckIntervalMs int64 `toml:"disk_check_interval_ms"` // Base interval for disk checks EnableAdaptiveInterval bool `toml:"enable_adaptive_interval"` // Adjust interval based on log rate + EnablePeriodicSync bool `toml:"enable_periodic_sync"` // Periodic sync with disk MinCheckIntervalMs int64 `toml:"min_check_interval_ms"` // Minimum adaptive interval MaxCheckIntervalMs int64 `toml:"max_check_interval_ms"` // Maximum adaptive interval } @@ -60,6 +61,7 @@ func DefaultConfig() *Config { RetentionCheckMins: 60.0, DiskCheckIntervalMs: 5000, EnableAdaptiveInterval: true, + EnablePeriodicSync: false, MinCheckIntervalMs: 100, MaxCheckIntervalMs: 60000, } diff --git a/default.go b/default.go new file mode 100644 index 0000000..9b5876c --- /dev/null +++ b/default.go @@ -0,0 +1,98 @@ +// --- File: default.go --- +package log + +import ( + "time" + + "github.com/LixenWraith/config" +) + +// Global instance for package-level functions +var defaultLogger = NewLogger() + +// Default package-level functions that delegate to the default logger + +// Init initializes or reconfigures the logger using the provided config.Config instance +func Init(cfg *config.Config, basePath string) error { + return defaultLogger.Init(cfg, basePath) +} + +// InitWithDefaults initializes the logger with built-in defaults and optional overrides +func InitWithDefaults(overrides ...string) error { + return defaultLogger.InitWithDefaults(overrides...) +} + +// Shutdown gracefully closes the logger, attempting to flush pending records +func Shutdown(timeout time.Duration) error { + return defaultLogger.Shutdown(timeout) +} + +// Debug logs a message at debug level +func Debug(args ...any) { + defaultLogger.Debug(args...) +} + +// Info logs a message at info level +func Info(args ...any) { + defaultLogger.Info(args...) +} + +// Warn logs a message at warning level +func Warn(args ...any) { + defaultLogger.Warn(args...) +} + +// Error logs a message at error level +func Error(args ...any) { + defaultLogger.Error(args...) +} + +// DebugTrace logs a debug message with function call trace +func DebugTrace(depth int, args ...any) { + defaultLogger.DebugTrace(depth, args...) +} + +// InfoTrace logs an info message with function call trace +func InfoTrace(depth int, args ...any) { + defaultLogger.InfoTrace(depth, args...) +} + +// WarnTrace logs a warning message with function call trace +func WarnTrace(depth int, args ...any) { + defaultLogger.WarnTrace(depth, args...) +} + +// ErrorTrace logs an error message with function call trace +func ErrorTrace(depth int, args ...any) { + defaultLogger.ErrorTrace(depth, args...) +} + +// Log writes a timestamp-only record without level information +func Log(args ...any) { + defaultLogger.Log(args...) +} + +// Message writes a plain record without timestamp or level info +func Message(args ...any) { + defaultLogger.Message(args...) +} + +// LogTrace writes a timestamp record with call trace but no level info +func LogTrace(depth int, args ...any) { + defaultLogger.LogTrace(depth, args...) +} + +// SaveConfig saves the current logger configuration to a file +func SaveConfig(path string) error { + return defaultLogger.SaveConfig(path) +} + +// LoadConfig loads logger configuration from a file with optional CLI overrides +func LoadConfig(path string, args []string) error { + return defaultLogger.LoadConfig(path, args) +} + +// Flush triggers a sync of the current log file buffer to disk and waits for completion or timeout +func Flush(timeout time.Duration) error { + return defaultLogger.Flush(timeout) +} \ No newline at end of file diff --git a/format.go b/format.go index af23b33..4208258 100644 --- a/format.go +++ b/format.go @@ -1,4 +1,4 @@ -// format.go +// --- File: format.go --- package log import ( diff --git a/go.mod b/go.mod index 8863a31..4cf94a7 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,5 @@ require github.com/LixenWraith/config v0.0.0-20250422065842-0c5b33a935d3 require ( github.com/LixenWraith/tinytoml v0.0.0-20250422065624-8aa28720f04a // indirect - github.com/google/uuid v1.6.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect ) diff --git a/go.sum b/go.sum index 4361879..2df3778 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,6 @@ -github.com/LixenWraith/config v0.0.0-20250421043933-12935fcc57a0 h1:HKd8Aj8EUHuLqVO9J+MeByPqUvJPAHZODSjVpyhnIrg= -github.com/LixenWraith/config v0.0.0-20250421043933-12935fcc57a0/go.mod h1:JF6kBabENV4uSgXd14tqt0DwvVS/9xxsxbU0xx+7yt8= github.com/LixenWraith/config v0.0.0-20250422065842-0c5b33a935d3 h1:FosLYzJhQRB5skEvG50gZb5gALUS1zn7jzA6bWLxjB4= github.com/LixenWraith/config v0.0.0-20250422065842-0c5b33a935d3/go.mod h1:LWz2FXeYAN1IxmPFAmbMZLhL/5LbHzJgnj4m7l5jGvc= -github.com/LixenWraith/tinytoml v0.0.0-20250305012228-6862ba843264 h1:p2hpE672qTRuhR9FAt7SIHp8aP0pJbBKushCiIRNRpo= -github.com/LixenWraith/tinytoml v0.0.0-20250305012228-6862ba843264/go.mod h1:pm+BQlZ/VQC30uaB5Vfeih2b77QkGIiMvu+QgG/XOTk= github.com/LixenWraith/tinytoml v0.0.0-20250422065624-8aa28720f04a h1:m+lhpIexwlJa5m1QuEveRmaGIE+wp87T97PyX1IWbMw= github.com/LixenWraith/tinytoml v0.0.0-20250422065624-8aa28720f04a/go.mod h1:Vax79K0I//Klsa8POjua/XHbsMUiIdjJHr59VFbc0/8= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= diff --git a/interface.go b/interface.go index b7267c0..906e5cf 100644 --- a/interface.go +++ b/interface.go @@ -1,3 +1,4 @@ +// --- File: interface.go --- package log import ( diff --git a/logger.go b/logger.go index 4a81554..e9b993b 100644 --- a/logger.go +++ b/logger.go @@ -1,9 +1,9 @@ +// --- File: logger.go --- package log import ( "fmt" "os" - "strconv" "strings" "sync" "time" @@ -42,9 +42,6 @@ var configDefaults = map[string]interface{}{ "log.max_check_interval_ms": int64(60000), } -// Global instance for package-level functions -var defaultLogger = NewLogger() - // NewLogger creates a new Logger instance with default settings func NewLogger() *Logger { l := &Logger{ @@ -70,9 +67,33 @@ func NewLogger() *Logger { close(initialChan) l.state.ActiveLogChannel.Store(initialChan) + l.state.flushRequestChan = make(chan chan struct{}, 1) + return l } +// LoadConfig loads logger configuration from a file with optional CLI overrides +func (l *Logger) LoadConfig(path string, args []string) error { + configExists, err := l.config.Load(path, args) + if err != nil { + return err + } + + // If no config file exists and no CLI args were provided, there's nothing to apply + if !configExists && len(args) == 0 { + return nil + } + + l.initMu.Lock() + defer l.initMu.Unlock() + return l.applyAndReconfigureLocked() +} + +// SaveConfig saves the current logger configuration to a file +func (l *Logger) SaveConfig(path string) error { + return l.config.Save(path) +} + // registerConfigValues registers all configuration parameters with the config instance func (l *Logger) registerConfigValues() { // Register each configuration value with its default @@ -85,35 +106,6 @@ func (l *Logger) registerConfigValues() { } } -// getCurrentLogChannel safely retrieves the current log channel -func (l *Logger) getCurrentLogChannel() chan logRecord { - chVal := l.state.ActiveLogChannel.Load() - return chVal.(chan logRecord) -} - -// Init initializes or reconfigures the logger using the provided config.Config instance -func (l *Logger) Init(cfg *config.Config, basePath string) error { - if cfg == nil { - l.state.LoggerDisabled.Store(true) - return fmtErrorf("config instance cannot be nil") - } - - l.initMu.Lock() - defer l.initMu.Unlock() - - if l.state.LoggerDisabled.Load() { - return fmtErrorf("logger previously failed to initialize and is disabled") - } - - // Update configuration from external config - if err := l.updateConfigFromExternal(cfg, basePath); err != nil { - return err - } - - // Apply configuration and reconfigure logger components - return l.applyAndReconfigureLocked() -} - // updateConfigFromExternal updates the logger config from an external config.Config instance func (l *Logger) updateConfigFromExternal(extCfg *config.Config, basePath string) error { // For each config key, get value from external config and update local config @@ -160,73 +152,6 @@ func (l *Logger) updateConfigFromExternal(extCfg *config.Config, basePath string return nil } -// InitWithDefaults initializes the logger with built-in defaults and optional overrides -func (l *Logger) InitWithDefaults(overrides ...string) error { - l.initMu.Lock() - defer l.initMu.Unlock() - - if l.state.LoggerDisabled.Load() { - return fmtErrorf("logger previously failed to initialize and is disabled") - } - - // Apply provided overrides - for _, override := range overrides { - key, valueStr, err := parseKeyValue(override) - if err != nil { - return err - } - - keyLower := strings.ToLower(key) - path := "log." + keyLower - - // Check if this is a valid config key - if _, exists := l.config.Get(path); !exists { - return fmtErrorf("unknown config key in override: %s", key) - } - - // Get current value to determine type for parsing - currentVal, found := l.config.Get(path) - if !found { - return fmtErrorf("failed to get current value for '%s'", key) - } - - // Parse according to type - var parsedValue interface{} - var parseErr error - - switch currentVal.(type) { - case int64: - parsedValue, parseErr = strconv.ParseInt(valueStr, 10, 64) - case string: - parsedValue = valueStr - case bool: - parsedValue, parseErr = strconv.ParseBool(valueStr) - case float64: - parsedValue, parseErr = strconv.ParseFloat(valueStr, 64) - default: - return fmtErrorf("unsupported type for key '%s'", key) - } - - if parseErr != nil { - return fmtErrorf("invalid value format for '%s': %w", key, parseErr) - } - - // Validate the parsed value - if err := validateConfigValue(keyLower, parsedValue); err != nil { - return fmtErrorf("invalid value for '%s': %w", key, err) - } - - // Update config with new value - err = l.config.Set(path, parsedValue) - if err != nil { - return fmtErrorf("failed to update config value for '%s': %w", key, err) - } - } - - // Apply configuration and reconfigure logger components - return l.applyAndReconfigureLocked() -} - // applyAndReconfigureLocked applies the configuration and reconfigures logger components // Assumes initMu is held func (l *Logger) applyAndReconfigureLocked() error { @@ -244,6 +169,13 @@ func (l *Logger) applyAndReconfigureLocked() error { } } + // Validate config (Basic) + currentCfg := l.loadCurrentConfig() // Helper to load struct from l.config + if err := currentCfg.validate(); err != nil { + l.state.LoggerDisabled.Store(true) // Disable logger on validation failure + return fmtErrorf("invalid configuration detected: %w", err) + } + // Ensure log directory exists dir, _ := l.config.String("log.directory") if err := os.MkdirAll(dir, 0755); err != nil { @@ -313,189 +245,36 @@ func (l *Logger) applyAndReconfigureLocked() error { return nil } -// Default package-level functions that delegate to the default logger - -// Init initializes or reconfigures the logger using the provided config.Config instance -func Init(cfg *config.Config, basePath string) error { - return defaultLogger.Init(cfg, basePath) +// loadCurrentConfig loads the current configuration for validation +func (l *Logger) loadCurrentConfig() *Config { + cfg := &Config{} + cfg.Level, _ = l.config.Int64("log.level") + cfg.Name, _ = l.config.String("log.name") + cfg.Directory, _ = l.config.String("log.directory") + cfg.Format, _ = l.config.String("log.format") + cfg.Extension, _ = l.config.String("log.extension") + cfg.ShowTimestamp, _ = l.config.Bool("log.show_timestamp") + cfg.ShowLevel, _ = l.config.Bool("log.show_level") + cfg.BufferSize, _ = l.config.Int64("log.buffer_size") + cfg.MaxSizeMB, _ = l.config.Int64("log.max_size_mb") + cfg.MaxTotalSizeMB, _ = l.config.Int64("log.max_total_size_mb") + cfg.MinDiskFreeMB, _ = l.config.Int64("log.min_disk_free_mb") + cfg.FlushIntervalMs, _ = l.config.Int64("log.flush_interval_ms") + cfg.TraceDepth, _ = l.config.Int64("log.trace_depth") + cfg.RetentionPeriodHrs, _ = l.config.Float64("log.retention_period_hrs") + cfg.RetentionCheckMins, _ = l.config.Float64("log.retention_check_mins") + cfg.DiskCheckIntervalMs, _ = l.config.Int64("log.disk_check_interval_ms") + cfg.EnableAdaptiveInterval, _ = l.config.Bool("log.enable_adaptive_interval") + cfg.MinCheckIntervalMs, _ = l.config.Int64("log.min_check_interval_ms") + cfg.MaxCheckIntervalMs, _ = l.config.Int64("log.max_check_interval_ms") + cfg.EnablePeriodicSync, _ = l.config.Bool("log.enable_periodic_sync") + return cfg } -// InitWithDefaults initializes the logger with built-in defaults and optional overrides -func InitWithDefaults(overrides ...string) error { - return defaultLogger.InitWithDefaults(overrides...) -} - -// Shutdown gracefully closes the logger, attempting to flush pending records -func Shutdown(timeout time.Duration) error { - return defaultLogger.Shutdown(timeout) -} - -// Debug logs a message at debug level -func Debug(args ...any) { - defaultLogger.Debug(args...) -} - -// Info logs a message at info level -func Info(args ...any) { - defaultLogger.Info(args...) -} - -// Warn logs a message at warning level -func Warn(args ...any) { - defaultLogger.Warn(args...) -} - -// Error logs a message at error level -func Error(args ...any) { - defaultLogger.Error(args...) -} - -// DebugTrace logs a debug message with function call trace -func DebugTrace(depth int, args ...any) { - defaultLogger.DebugTrace(depth, args...) -} - -// InfoTrace logs an info message with function call trace -func InfoTrace(depth int, args ...any) { - defaultLogger.InfoTrace(depth, args...) -} - -// WarnTrace logs a warning message with function call trace -func WarnTrace(depth int, args ...any) { - defaultLogger.WarnTrace(depth, args...) -} - -// ErrorTrace logs an error message with function call trace -func ErrorTrace(depth int, args ...any) { - defaultLogger.ErrorTrace(depth, args...) -} - -// Log writes a timestamp-only record without level information -func Log(args ...any) { - defaultLogger.Log(args...) -} - -// Message writes a plain record without timestamp or level info -func Message(args ...any) { - defaultLogger.Message(args...) -} - -// LogTrace writes a timestamp record with call trace but no level info -func LogTrace(depth int, args ...any) { - defaultLogger.LogTrace(depth, args...) -} - -// SaveConfig saves the current logger configuration to a file -func SaveConfig(path string) error { - return defaultLogger.SaveConfig(path) -} - -// LoadConfig loads logger configuration from a file with optional CLI overrides -func LoadConfig(path string, args []string) error { - return defaultLogger.LoadConfig(path, args) -} - -// SaveConfig saves the current logger configuration to a file -func (l *Logger) SaveConfig(path string) error { - return l.config.Save(path) -} - -// LoadConfig loads logger configuration from a file with optional CLI overrides -func (l *Logger) LoadConfig(path string, args []string) error { - configExists, err := l.config.Load(path, args) - if err != nil { - return err - } - - // If no config file exists and no CLI args were provided, there's nothing to apply - if !configExists && len(args) == 0 { - return nil - } - - l.initMu.Lock() - defer l.initMu.Unlock() - return l.applyAndReconfigureLocked() -} - -// Helper functions -func (l *Logger) Shutdown(timeout time.Duration) error { - // Ensure shutdown runs only once - if !l.state.ShutdownCalled.CompareAndSwap(false, true) { - return nil - } - - // Prevent new logs from being processed or sent - l.state.LoggerDisabled.Store(true) - - // If the logger was never initialized, there's nothing to shut down - if !l.state.IsInitialized.Load() { - l.state.ShutdownCalled.Store(false) // Allow potential future init/shutdown cycle - l.state.LoggerDisabled.Store(false) - l.state.ProcessorExited.Store(true) // Mark as not running - return nil - } - - // Signal the processor goroutine to stop by closing its channel - l.initMu.Lock() - ch := l.getCurrentLogChannel() - closedChan := make(chan logRecord) // Create a dummy closed channel - close(closedChan) - l.state.ActiveLogChannel.Store(closedChan) // Point producers to the dummy channel - // Close the actual channel the processor is reading from - if ch != closedChan { // Avoid closing the dummy channel itself - close(ch) - } - l.initMu.Unlock() - - // Determine the maximum time to wait for the processor to finish - effectiveTimeout := timeout - if effectiveTimeout <= 0 { - // Use the configured flush interval as the default timeout if none provided - flushMs, _ := l.config.Int64("log.flush_interval_ms") - effectiveTimeout = time.Duration(flushMs) * time.Millisecond - } - - // Wait for the processor goroutine to signal its exit, or until the timeout - deadline := time.Now().Add(effectiveTimeout) - pollInterval := 10 * time.Millisecond // Check status periodically - processorCleanlyExited := false - for time.Now().Before(deadline) { - if l.state.ProcessorExited.Load() { - processorCleanlyExited = true - break // Processor finished cleanly - } - time.Sleep(pollInterval) - } - - // Mark the logger as uninitialized - l.state.IsInitialized.Store(false) - - // Sync and close the current log file - var finalErr error - cfPtr := l.state.CurrentFile.Load() - if cfPtr != nil { - if currentLogFile, ok := cfPtr.(*os.File); ok && currentLogFile != nil { - // Attempt to sync data to disk - if err := currentLogFile.Sync(); err != nil { - finalErr = fmtErrorf("failed to sync log file '%s' during shutdown: %w", currentLogFile.Name(), err) - } - // Attempt to close the file descriptor - if err := currentLogFile.Close(); err != nil { - closeErr := fmtErrorf("failed to close log file '%s' during shutdown: %w", currentLogFile.Name(), err) - finalErr = combineErrors(finalErr, closeErr) // Combine sync/close errors - } - // Clear the atomic reference to the file - l.state.CurrentFile.Store((*os.File)(nil)) - } - } - - // Report timeout error if processor didn't exit cleanly - if !processorCleanlyExited { - timeoutErr := fmtErrorf("logger processor did not exit within timeout (%v)", effectiveTimeout) - finalErr = combineErrors(finalErr, timeoutErr) - } - - return finalErr +// getCurrentLogChannel safely retrieves the current log channel +func (l *Logger) getCurrentLogChannel() chan logRecord { + chVal := l.state.ActiveLogChannel.Load() + return chVal.(chan logRecord) } // Logger instance methods for logging at different levels @@ -613,7 +392,7 @@ func (l *Logger) log(flags int64, level int64, depth int64, args ...any) { // Get trace if needed var trace string if depth > 0 { - const skipTrace = 3 // log.Info -> logInternal -> getTrace (Adjust if call stack changes) + const skipTrace = 3 // log.Info -> log -> getTrace (Adjust if call stack changes) trace = getTrace(depth, skipTrace) } diff --git a/processor.go b/processor.go index d54bd5d..8f3f36a 100644 --- a/processor.go +++ b/processor.go @@ -1,4 +1,4 @@ -// processor.go +// --- File: processor.go --- package log import ( @@ -146,7 +146,10 @@ func (l *Logger) processLogs(ch <-chan logRecord) { } case <-flushTicker.C: - l.performSync() + enableSync, _ := l.config.Bool("log.enable_periodic_sync") + if enableSync { + l.performSync() + } case <-diskCheckTicker.C: // Periodic disk check @@ -188,6 +191,10 @@ func (l *Logger) processLogs(ch <-chan logRecord) { lastCheckTime = time.Now() } + case confirmChan := <-l.state.flushRequestChan: + l.performSync() + close(confirmChan) // Signal completion back to the Flush caller + case <-retentionChan: // Check file retention retentionPeriodHrs, _ := l.config.Float64("log.retention_period_hrs") diff --git a/state.go b/state.go index 099dea4..1795d79 100644 --- a/state.go +++ b/state.go @@ -1,7 +1,15 @@ +// --- File: state.go --- package log import ( + "os" + "strconv" + "strings" + "sync" "sync/atomic" + "time" + + "github.com/LixenWraith/config" ) // State encapsulates the runtime state of the logger @@ -13,6 +21,9 @@ type State struct { DiskStatusOK atomic.Bool ProcessorExited atomic.Bool // Tracks if the processor goroutine is running or has exited + flushRequestChan chan chan struct{} // Channel to request a flush + flushMutex sync.Mutex // Protect concurrent Flush calls + CurrentFile atomic.Value // stores *os.File CurrentSize atomic.Int64 // Size of the current log file EarliestFileTime atomic.Value // stores time.Time for retention @@ -20,4 +31,206 @@ type State struct { LoggedDrops atomic.Uint64 // Counter for dropped logs message already logged ActiveLogChannel atomic.Value // stores chan logRecord +} + +// Init initializes or reconfigures the logger using the provided config.Config instance +func (l *Logger) Init(cfg *config.Config, basePath string) error { + if cfg == nil { + l.state.LoggerDisabled.Store(true) + return fmtErrorf("config instance cannot be nil") + } + + l.initMu.Lock() + defer l.initMu.Unlock() + + if l.state.LoggerDisabled.Load() { + return fmtErrorf("logger previously failed to initialize and is disabled") + } + + // Update configuration from external config + if err := l.updateConfigFromExternal(cfg, basePath); err != nil { + return err + } + + // Apply configuration and reconfigure logger components + return l.applyAndReconfigureLocked() +} + +// InitWithDefaults initializes the logger with built-in defaults and optional overrides +func (l *Logger) InitWithDefaults(overrides ...string) error { + l.initMu.Lock() + defer l.initMu.Unlock() + + if l.state.LoggerDisabled.Load() { + return fmtErrorf("logger previously failed to initialize and is disabled") + } + + // Apply provided overrides + for _, override := range overrides { + key, valueStr, err := parseKeyValue(override) + if err != nil { + return err + } + + keyLower := strings.ToLower(key) + path := "log." + keyLower + + // Check if this is a valid config key + if _, exists := l.config.Get(path); !exists { + return fmtErrorf("unknown config key in override: %s", key) + } + + // Get current value to determine type for parsing + currentVal, found := l.config.Get(path) + if !found { + return fmtErrorf("failed to get current value for '%s'", key) + } + + // Parse according to type + var parsedValue interface{} + var parseErr error + + switch currentVal.(type) { + case int64: + parsedValue, parseErr = strconv.ParseInt(valueStr, 10, 64) + case string: + parsedValue = valueStr + case bool: + parsedValue, parseErr = strconv.ParseBool(valueStr) + case float64: + parsedValue, parseErr = strconv.ParseFloat(valueStr, 64) + default: + return fmtErrorf("unsupported type for key '%s'", key) + } + + if parseErr != nil { + return fmtErrorf("invalid value format for '%s': %w", key, parseErr) + } + + // Validate the parsed value + if err := validateConfigValue(keyLower, parsedValue); err != nil { + return fmtErrorf("invalid value for '%s': %w", key, err) + } + + // Update config with new value + err = l.config.Set(path, parsedValue) + if err != nil { + return fmtErrorf("failed to update config value for '%s': %w", key, err) + } + } + + // Apply configuration and reconfigure logger components + return l.applyAndReconfigureLocked() +} + +// Shutdown gracefully closes the logger, attempting to flush pending records +func (l *Logger) Shutdown(timeout time.Duration) error { + // Ensure shutdown runs only once + if !l.state.ShutdownCalled.CompareAndSwap(false, true) { + return nil + } + + // Prevent new logs from being processed or sent + l.state.LoggerDisabled.Store(true) + + // If the logger was never initialized, there's nothing to shut down + if !l.state.IsInitialized.Load() { + l.state.ShutdownCalled.Store(false) // Allow potential future init/shutdown cycle + l.state.LoggerDisabled.Store(false) + l.state.ProcessorExited.Store(true) // Mark as not running + return nil + } + + // Signal the processor goroutine to stop by closing its channel + l.initMu.Lock() + ch := l.getCurrentLogChannel() + closedChan := make(chan logRecord) // Create a dummy closed channel + close(closedChan) + l.state.ActiveLogChannel.Store(closedChan) // Point producers to the dummy channel + // Close the actual channel the processor is reading from + if ch != closedChan { + close(ch) + } + l.initMu.Unlock() + + // Determine the maximum time to wait for the processor to finish + effectiveTimeout := timeout + if effectiveTimeout <= 0 { + // Use the configured flush interval as the default timeout if none provided + flushMs, _ := l.config.Int64("log.flush_interval_ms") + effectiveTimeout = 2 * time.Duration(flushMs) * time.Millisecond + } + + // Wait for the processor goroutine to signal its exit, or until the timeout + deadline := time.Now().Add(effectiveTimeout) + pollInterval := 10 * time.Millisecond // Check status periodically + processorCleanlyExited := false + for time.Now().Before(deadline) { + if l.state.ProcessorExited.Load() { + processorCleanlyExited = true + break // Processor finished cleanly + } + time.Sleep(pollInterval) + } + + // Mark the logger as uninitialized + l.state.IsInitialized.Store(false) + + // Sync and close the current log file + var finalErr error + cfPtr := l.state.CurrentFile.Load() + if cfPtr != nil { + if currentLogFile, ok := cfPtr.(*os.File); ok && currentLogFile != nil { + // Attempt to sync data to disk + if err := currentLogFile.Sync(); err != nil { + syncErr := fmtErrorf("failed to sync log file '%s' during shutdown: %w", currentLogFile.Name(), err) + finalErr = combineErrors(finalErr, syncErr) + } + // Attempt to close the file descriptor + if err := currentLogFile.Close(); err != nil { + closeErr := fmtErrorf("failed to close log file '%s' during shutdown: %w", currentLogFile.Name(), err) + finalErr = combineErrors(finalErr, closeErr) + } + // Clear the atomic reference to the file + l.state.CurrentFile.Store((*os.File)(nil)) + } + } + + // Report timeout error if processor didn't exit cleanly + if !processorCleanlyExited { + timeoutErr := fmtErrorf("logger processor did not exit within timeout (%v)", effectiveTimeout) + finalErr = combineErrors(finalErr, timeoutErr) + } + + return finalErr +} + +// Flush explicitly triggers a sync of the current log file buffer to disk and waits for completion or timeout. +func (l *Logger) Flush(timeout time.Duration) error { + // Prevent concurrent flushes overwhelming the processor or channel + l.state.flushMutex.Lock() + defer l.state.flushMutex.Unlock() + + if !l.state.IsInitialized.Load() || l.state.ShutdownCalled.Load() { + return fmtErrorf("logger not initialized or already shut down") + } + + // Create a channel to wait for confirmation from the processor + confirmChan := make(chan struct{}) + + // Send the request with the confirmation channel + select { + case l.state.flushRequestChan <- confirmChan: + // Request sent + case <-time.After(10 * time.Millisecond): // Short timeout to prevent blocking if processor is stuck + return fmtErrorf("failed to send flush request to processor (possible deadlock or high load)") + } + + // Wait for the processor to signal completion or timeout + select { + case <-confirmChan: + return nil // Flush completed successfully + case <-time.After(timeout): + return fmtErrorf("timeout waiting for flush confirmation (%v)", timeout) + } } \ No newline at end of file diff --git a/utility.go b/utility.go index fb8ffbc..ec39fcc 100644 --- a/utility.go +++ b/utility.go @@ -1,3 +1,4 @@ +// --- File: utility.go --- package log import ( @@ -141,8 +142,8 @@ func validateConfigValue(key string, value interface{}) error { return nil } -// parseLevel converts level string to numeric constant. -func parseLevel(levelStr string) (int64, error) { +// Level converts level string to numeric constant. +func Level(levelStr string) (int64, error) { switch strings.ToLower(strings.TrimSpace(levelStr)) { case "debug": return LevelDebug, nil @@ -155,4 +156,4 @@ func parseLevel(levelStr string) (int64, error) { default: return 0, fmtErrorf("invalid level string: '%s' (use debug, info, warn, error)", levelStr) } -} +} \ No newline at end of file