e1.0.0 Initial commit, restructured and refactored logger package, used config package for configuration management.
This commit is contained in:
655
logger.go
Normal file
655
logger.go
Normal file
@ -0,0 +1,655 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"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
|
||||
}
|
||||
|
||||
// configDefaults holds the default values for logger configuration
|
||||
var configDefaults = map[string]interface{}{
|
||||
"log.level": LevelInfo,
|
||||
"log.name": "log",
|
||||
"log.directory": "./logs",
|
||||
"log.format": "txt",
|
||||
"log.extension": "log",
|
||||
"log.show_timestamp": true,
|
||||
"log.show_level": true,
|
||||
"log.buffer_size": int64(1024),
|
||||
"log.max_size_mb": int64(10),
|
||||
"log.max_total_size_mb": int64(50),
|
||||
"log.min_disk_free_mb": int64(100),
|
||||
"log.flush_interval_ms": int64(100),
|
||||
"log.trace_depth": int64(0),
|
||||
"log.retention_period_hrs": float64(0.0),
|
||||
"log.retention_check_mins": float64(60.0),
|
||||
"log.disk_check_interval_ms": int64(5000),
|
||||
"log.enable_adaptive_interval": true,
|
||||
"log.min_check_interval_ms": int64(100),
|
||||
"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{
|
||||
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)
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// registerConfigValues registers all configuration parameters with the config instance
|
||||
func (l *Logger) registerConfigValues() {
|
||||
// Register each configuration value with its default
|
||||
for path, defaultValue := range configDefaults {
|
||||
err := l.config.Register(path, defaultValue)
|
||||
if err != nil {
|
||||
// If registration fails, we'll handle it gracefully
|
||||
fmt.Fprintf(os.Stderr, "log: warning - failed to register config key '%s': %v\n", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
for path := range configDefaults {
|
||||
// Extract the local name without the "log." prefix
|
||||
localName := strings.TrimPrefix(path, "log.")
|
||||
|
||||
// Create the full path for the external config
|
||||
fullPath := localName
|
||||
if basePath != "" {
|
||||
fullPath = basePath + "." + localName
|
||||
}
|
||||
|
||||
// Get current value from our config to use as default in external config
|
||||
currentVal, found := l.config.Get(path)
|
||||
if !found {
|
||||
// Use the original default if not found in current config
|
||||
currentVal = configDefaults[path]
|
||||
}
|
||||
|
||||
// Register in external config with our current value as the 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 the value before updating
|
||||
if err := validateConfigValue(localName, val); err != nil {
|
||||
return fmtErrorf("invalid value for '%s': %w", localName, err)
|
||||
}
|
||||
|
||||
// Update our config with the new value
|
||||
err = l.config.Set(path, val)
|
||||
if err != nil {
|
||||
return fmtErrorf("failed to update config value for '%s': %w", path, err)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Logger instance methods for logging at different levels
|
||||
|
||||
// Debug logs a message at debug level.
|
||||
func (l *Logger) Debug(args ...any) {
|
||||
flags := l.getFlags()
|
||||
traceDepth, _ := l.config.Int64("log.trace_depth")
|
||||
l.log(flags, LevelDebug, traceDepth, args...)
|
||||
}
|
||||
|
||||
// Info logs a message at info level.
|
||||
func (l *Logger) Info(args ...any) {
|
||||
flags := l.getFlags()
|
||||
traceDepth, _ := l.config.Int64("log.trace_depth")
|
||||
l.log(flags, LevelInfo, traceDepth, args...)
|
||||
}
|
||||
|
||||
// Warn logs a message at warning level.
|
||||
func (l *Logger) Warn(args ...any) {
|
||||
flags := l.getFlags()
|
||||
traceDepth, _ := l.config.Int64("log.trace_depth")
|
||||
l.log(flags, LevelWarn, traceDepth, args...)
|
||||
}
|
||||
|
||||
// Error logs a message at error level.
|
||||
func (l *Logger) Error(args ...any) {
|
||||
flags := l.getFlags()
|
||||
traceDepth, _ := l.config.Int64("log.trace_depth")
|
||||
l.log(flags, LevelError, traceDepth, args...)
|
||||
}
|
||||
|
||||
// DebugTrace logs a debug message with function call trace.
|
||||
func (l *Logger) DebugTrace(depth int, args ...any) {
|
||||
flags := l.getFlags()
|
||||
l.log(flags, LevelDebug, int64(depth), args...)
|
||||
}
|
||||
|
||||
// InfoTrace logs an info message with function call trace.
|
||||
func (l *Logger) InfoTrace(depth int, args ...any) {
|
||||
flags := l.getFlags()
|
||||
l.log(flags, LevelInfo, int64(depth), args...)
|
||||
}
|
||||
|
||||
// WarnTrace logs a warning message with function call trace.
|
||||
func (l *Logger) WarnTrace(depth int, args ...any) {
|
||||
flags := l.getFlags()
|
||||
l.log(flags, LevelWarn, int64(depth), args...)
|
||||
}
|
||||
|
||||
// ErrorTrace logs an error message with function call trace.
|
||||
func (l *Logger) ErrorTrace(depth int, args ...any) {
|
||||
flags := l.getFlags()
|
||||
l.log(flags, LevelError, int64(depth), args...)
|
||||
}
|
||||
|
||||
// Log writes a timestamp-only record without level information.
|
||||
func (l *Logger) Log(args ...any) {
|
||||
l.log(FlagShowTimestamp, LevelInfo, 0, args...)
|
||||
}
|
||||
|
||||
// Message writes a plain record without timestamp or level info.
|
||||
func (l *Logger) Message(args ...any) {
|
||||
l.log(0, LevelInfo, 0, args...)
|
||||
}
|
||||
|
||||
// LogTrace writes a timestamp record with call trace but no level info.
|
||||
func (l *Logger) LogTrace(depth int, args ...any) {
|
||||
l.log(FlagShowTimestamp, LevelInfo, int64(depth), args...)
|
||||
}
|
||||
|
||||
// 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 -> logInternal -> 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user