// --- File: logger.go --- package log import ( "fmt" "os" "strings" "sync" "time" "github.com/LixenWraith/config" ) // Logger is the core struct that encapsulates all logger functionality type Logger struct { config *config.Config // Config management state State initMu sync.Mutex // Only mutex we need to keep serializer *serializer // Encapsulated serializer instance } // NewLogger creates a new Logger instance with default settings func NewLogger() *Logger { l := &Logger{ config: config.New(), serializer: newSerializer(), } // Register all configuration parameters with their defaults l.registerConfigValues() // Initialize the state l.state.IsInitialized.Store(false) l.state.LoggerDisabled.Store(false) l.state.ShutdownCalled.Store(false) l.state.DiskFullLogged.Store(false) l.state.DiskStatusOK.Store(true) l.state.ProcessorExited.Store(true) l.state.CurrentSize.Store(0) l.state.EarliestFileTime.Store(time.Time{}) // Create a closed channel initially to prevent nil pointer issues initialChan := make(chan logRecord) 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 the entire config struct at once err := l.config.RegisterStruct("log.", defaultConfig) if err != nil { fmt.Fprintf(os.Stderr, "log: warning - failed to register config values: %v\n", err) } } // updateConfigFromExternal updates the logger config from an external config.Config instance func (l *Logger) updateConfigFromExternal(extCfg *config.Config, basePath string) error { // Get our registered config paths (already registered during initialization) registeredPaths := l.config.GetRegisteredPaths("log.") if len(registeredPaths) == 0 { // Register defaults first if not already done l.registerConfigValues() registeredPaths = l.config.GetRegisteredPaths("log.") } // For each registered path for path := range registeredPaths { // Extract local name and build external path localName := strings.TrimPrefix(path, "log.") fullPath := basePath + "." + localName if basePath == "" { fullPath = localName } // Get current value to use as default in external config currentVal, found := l.config.Get(path) if !found { continue // Skip if not found (shouldn't happen) } // Register in external config with current value as default err := extCfg.Register(fullPath, currentVal) if err != nil { return fmtErrorf("failed to register config key '%s': %w", fullPath, err) } // Get value from external config val, found := extCfg.Get(fullPath) if !found { continue // Use existing value if not found in external config } // Validate and update if err := validateConfigValue(localName, val); err != nil { return fmtErrorf("invalid value for '%s': %w", localName, err) } if err := l.config.Set(path, val); err != nil { return fmtErrorf("failed to update config value for '%s': %w", path, err) } } return nil } // applyAndReconfigureLocked applies the configuration and reconfigures logger components // Assumes initMu is held func (l *Logger) applyAndReconfigureLocked() error { // Check parameter relationship issues minInterval, _ := l.config.Int64("log.min_check_interval_ms") maxInterval, _ := l.config.Int64("log.max_check_interval_ms") if minInterval > maxInterval { fmt.Fprintf(os.Stderr, "log: warning - min_check_interval_ms (%d) > max_check_interval_ms (%d), max will be used\n", minInterval, maxInterval) // Update min_check_interval_ms to equal max_check_interval_ms err := l.config.Set("log.min_check_interval_ms", maxInterval) if err != nil { fmt.Fprintf(os.Stderr, "log: warning - failed to update min_check_interval_ms: %v\n", err) } } // 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 { l.state.LoggerDisabled.Store(true) return fmtErrorf("failed to create log directory '%s': %w", dir, err) } // Check if we need to restart the processor wasInitialized := l.state.IsInitialized.Load() processorNeedsRestart := !wasInitialized // Always restart the processor if initialized, to handle any config changes // This is the simplest approach that works reliably for all config changes if wasInitialized { processorNeedsRestart = true } // Restart processor if needed if processorNeedsRestart { // Close the old channel if reconfiguring if wasInitialized { oldCh := l.getCurrentLogChannel() if oldCh != nil { // Swap in a temporary closed channel tempClosedChan := make(chan logRecord) close(tempClosedChan) l.state.ActiveLogChannel.Store(tempClosedChan) // Close the actual old channel close(oldCh) } } // Create the new channel bufferSize, _ := l.config.Int64("log.buffer_size") newLogChannel := make(chan logRecord, bufferSize) l.state.ActiveLogChannel.Store(newLogChannel) // Start the new processor l.state.ProcessorExited.Store(false) go l.processLogs(newLogChannel) } // Initialize new log file if needed currentFileHandle := l.state.CurrentFile.Load() needsNewFile := !wasInitialized || currentFileHandle == nil if needsNewFile { logFile, err := l.createNewLogFile() if err != nil { l.state.LoggerDisabled.Store(true) return fmtErrorf("failed to create initial/new log file: %w", err) } l.state.CurrentFile.Store(logFile) l.state.CurrentSize.Store(0) if fi, errStat := logFile.Stat(); errStat == nil { l.state.CurrentSize.Store(fi.Size()) } } // Mark as initialized l.state.IsInitialized.Store(true) l.state.ShutdownCalled.Store(false) l.state.DiskFullLogged.Store(false) l.state.DiskStatusOK.Store(true) return nil } // 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 } // getCurrentLogChannel safely retrieves the current log channel func (l *Logger) getCurrentLogChannel() chan logRecord { chVal := l.state.ActiveLogChannel.Load() return chVal.(chan logRecord) } // Helper method to get flags from config func (l *Logger) getFlags() int64 { var flags int64 = 0 showLevel, _ := l.config.Bool("log.show_level") showTimestamp, _ := l.config.Bool("log.show_timestamp") if showLevel { flags |= FlagShowLevel } if showTimestamp { flags |= FlagShowTimestamp } return flags } // log handles the core logging logic func (l *Logger) log(flags int64, level int64, depth int64, args ...any) { // Quick checks first if l.state.LoggerDisabled.Load() || !l.state.IsInitialized.Load() { return } // Check if this log level should be processed configLevel, _ := l.config.Int64("log.level") if level < configLevel { return } // Report dropped logs if necessary currentDrops := l.state.DroppedLogs.Load() logged := l.state.LoggedDrops.Load() if currentDrops > logged { if l.state.LoggedDrops.CompareAndSwap(logged, currentDrops) { dropRecord := logRecord{ Flags: FlagDefault, // Use default flags for drop message TimeStamp: time.Now(), Level: LevelError, Args: []any{"Logs were dropped", "dropped_count", currentDrops - logged, "total_dropped", currentDrops}, } l.sendLogRecord(dropRecord) // Best effort send } } // Get trace if needed var trace string if depth > 0 { const skipTrace = 3 // log.Info -> log -> getTrace (Adjust if call stack changes) trace = getTrace(depth, skipTrace) } // Create record and send record := logRecord{ Flags: flags, TimeStamp: time.Now(), Level: level, Trace: trace, Args: args, } l.sendLogRecord(record) } // sendLogRecord handles safe sending to the active channel func (l *Logger) sendLogRecord(record logRecord) { defer func() { if recover() != nil { // Catch panic on send to closed channel l.state.DroppedLogs.Add(1) } }() if l.state.ShutdownCalled.Load() || l.state.LoggerDisabled.Load() { l.state.DroppedLogs.Add(1) return } // Load current channel reference atomically ch := l.getCurrentLogChannel() // Non-blocking send select { case ch <- record: // Success default: // Channel buffer is full or channel is closed l.state.DroppedLogs.Add(1) } }