v0.1.9 kv flag added, unix and nofs build tags, heartbeat and rotation filename and test update
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -17,10 +18,16 @@ import (
|
||||
type Logger struct {
|
||||
currentConfig atomic.Value // stores *Config
|
||||
formatter atomic.Value // stores *formatter.Formatter
|
||||
ctxKeys atomic.Pointer[formatter.ContextKeys]
|
||||
spawner atomic.Pointer[func(func())]
|
||||
errHandler atomic.Pointer[func(string)]
|
||||
state State
|
||||
initMu sync.Mutex
|
||||
}
|
||||
|
||||
// levelOff closes the emit gate without touching configuration
|
||||
const levelOff int64 = math.MaxInt64
|
||||
|
||||
// NewLogger creates a new Logger instance with default settings
|
||||
func NewLogger() *Logger {
|
||||
l := &Logger{}
|
||||
@@ -28,14 +35,20 @@ func NewLogger() *Logger {
|
||||
// Set default configuration
|
||||
defaultCfg := DefaultConfig()
|
||||
l.currentConfig.Store(defaultCfg)
|
||||
l.rebuildFormatter(defaultCfg)
|
||||
|
||||
// Initialize default formatter to prevent nil access
|
||||
defaultFormatter := formatter.New(sanitizer.New()).
|
||||
Type(defaultCfg.Format).
|
||||
TimestampFormat(defaultCfg.TimestampFormat).
|
||||
ShowLevel(defaultCfg.ShowLevel).
|
||||
ShowTimestamp(defaultCfg.ShowTimestamp)
|
||||
l.formatter.Store(defaultFormatter)
|
||||
// Emission stays closed until ApplyConfig and Start succeed
|
||||
l.state.Level.Store(levelOff)
|
||||
l.state.Flags.Store(flagsFromConfig(defaultCfg))
|
||||
l.state.TraceDepth.Store(defaultCfg.TraceDepth)
|
||||
|
||||
// // Initialize default formatter to prevent nil access
|
||||
// defaultFormatter := formatter.New(sanitizer.New()).
|
||||
// Type(defaultCfg.Format).
|
||||
// TimestampFormat(defaultCfg.TimestampFormat).
|
||||
// ShowLevel(defaultCfg.ShowLevel).
|
||||
// ShowTimestamp(defaultCfg.ShowTimestamp)
|
||||
// l.formatter.Store(defaultFormatter)
|
||||
|
||||
// Initialize the state
|
||||
l.state.IsInitialized.Store(false)
|
||||
@@ -54,13 +67,17 @@ func NewLogger() *Logger {
|
||||
l.state.TotalRotations.Store(0)
|
||||
l.state.TotalDeletions.Store(0)
|
||||
|
||||
// Create a closed channel initially to prevent nil pointer issues
|
||||
initialChan := make(chan logRecord)
|
||||
close(initialChan)
|
||||
l.state.ActiveLogChannel.Store(initialChan)
|
||||
|
||||
// Typed nil: a non-blocking send on a nil channel always takes default
|
||||
l.state.ActiveLogChannel.Store((chan logRecord)(nil))
|
||||
l.state.flushRequestChan = make(chan chan struct{}, 1)
|
||||
|
||||
// // 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
|
||||
}
|
||||
|
||||
@@ -112,237 +129,6 @@ func (l *Logger) GetConfig() *Config {
|
||||
return l.getConfig().Clone()
|
||||
}
|
||||
|
||||
// Start begins log processing. Safe to call multiple times
|
||||
// Returns error if logger is not initialized
|
||||
func (l *Logger) Start() error {
|
||||
if !l.state.IsInitialized.Load() {
|
||||
return fmtErrorf("logger not initialized, call ApplyConfig first")
|
||||
}
|
||||
|
||||
// Check if processor didn't exit cleanly last time
|
||||
if l.state.Started.Load() && !l.state.ProcessorExited.Load() {
|
||||
// Force stop to clean up
|
||||
l.internalLog("warning - processor still running from previous start, forcing stop\n")
|
||||
if err := l.Stop(); err != nil {
|
||||
return fmtErrorf("failed to stop hung processor: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Only start if not already started
|
||||
if l.state.Started.CompareAndSwap(false, true) {
|
||||
cfg := l.getConfig()
|
||||
|
||||
// Create log channel
|
||||
logChannel := make(chan logRecord, cfg.BufferSize)
|
||||
l.state.ActiveLogChannel.Store(logChannel)
|
||||
|
||||
// Start processor
|
||||
l.state.ProcessorExited.Store(false)
|
||||
go l.processLogs(logChannel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop halts log processing. Can be restarted with Start()
|
||||
// Returns nil if already stopped
|
||||
func (l *Logger) Stop(timeout ...time.Duration) error {
|
||||
if !l.state.Started.CompareAndSwap(true, false) {
|
||||
return nil // Already stopped
|
||||
}
|
||||
|
||||
// Calculate effective timeout
|
||||
var effectiveTimeout time.Duration
|
||||
if len(timeout) > 0 {
|
||||
effectiveTimeout = timeout[0]
|
||||
} else {
|
||||
cfg := l.getConfig()
|
||||
effectiveTimeout = 2 * time.Duration(cfg.FlushIntervalMs) * time.Millisecond
|
||||
}
|
||||
|
||||
// Get current channel and close it
|
||||
ch := l.getCurrentLogChannel()
|
||||
if ch != nil {
|
||||
// Create closed channel for immediate replacement
|
||||
closedChan := make(chan logRecord)
|
||||
close(closedChan)
|
||||
l.state.ActiveLogChannel.Store(closedChan)
|
||||
|
||||
// Close the actual channel to signal processor
|
||||
close(ch)
|
||||
}
|
||||
|
||||
// Wait for processor to exit (with timeout)
|
||||
deadline := time.Now().Add(effectiveTimeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if l.state.ProcessorExited.Load() {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
if !l.state.ProcessorExited.Load() {
|
||||
return fmtErrorf("processor did not exit within timeout (%v)", effectiveTimeout)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown gracefully closes the logger, attempting to flush pending records
|
||||
// If no timeout is provided, uses a default of 2x flush interval
|
||||
func (l *Logger) Shutdown(timeout ...time.Duration) error {
|
||||
if !l.state.ShutdownCalled.CompareAndSwap(false, true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.state.LoggerDisabled.Store(true)
|
||||
|
||||
if !l.state.IsInitialized.Load() {
|
||||
l.state.ShutdownCalled.Store(false)
|
||||
l.state.LoggerDisabled.Store(false)
|
||||
l.state.ProcessorExited.Store(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
var stopErr error
|
||||
if l.state.Started.Load() {
|
||||
stopErr = l.Stop(timeout...)
|
||||
}
|
||||
|
||||
l.state.IsInitialized.Store(false)
|
||||
|
||||
var finalErr error
|
||||
cfPtr := l.state.CurrentFile.Load()
|
||||
if cfPtr != nil {
|
||||
if currentLogFile, ok := cfPtr.(*os.File); ok && currentLogFile != nil {
|
||||
if err := currentLogFile.Sync(); err != nil {
|
||||
syncErr := fmtErrorf("failed to sync log file '%s' during shutdown: %w", currentLogFile.Name(), err)
|
||||
finalErr = errors.Join(finalErr, syncErr)
|
||||
}
|
||||
if err := currentLogFile.Close(); err != nil {
|
||||
closeErr := fmtErrorf("failed to close log file '%s' during shutdown: %w", currentLogFile.Name(), err)
|
||||
finalErr = errors.Join(finalErr, closeErr)
|
||||
}
|
||||
l.state.CurrentFile.Store((*os.File)(nil))
|
||||
}
|
||||
}
|
||||
|
||||
if stopErr != nil {
|
||||
finalErr = errors.Join(finalErr, stopErr)
|
||||
}
|
||||
|
||||
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 {
|
||||
l.state.flushMutex.Lock()
|
||||
defer l.state.flushMutex.Unlock()
|
||||
|
||||
// State checks
|
||||
if !l.state.IsInitialized.Load() || l.state.ShutdownCalled.Load() {
|
||||
return fmtErrorf("logger not initialized or already shut down")
|
||||
}
|
||||
if !l.state.Started.Load() {
|
||||
return fmtErrorf("logger not started")
|
||||
}
|
||||
|
||||
// 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(minWaitTime): // Short timeout to prevent blocking if processor is stuck
|
||||
return fmtErrorf("failed to send flush request to processor (possible deadlock or high load)")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-confirmChan:
|
||||
return nil
|
||||
case <-time.After(timeout):
|
||||
return fmtErrorf("timeout waiting for flush confirmation (%v)", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logs a message at debug level
|
||||
func (l *Logger) Debug(args ...any) {
|
||||
flags := l.getFlags()
|
||||
cfg := l.getConfig()
|
||||
l.log(flags, LevelDebug, cfg.TraceDepth, args...)
|
||||
}
|
||||
|
||||
// Info logs a message at info level
|
||||
func (l *Logger) Info(args ...any) {
|
||||
flags := l.getFlags()
|
||||
cfg := l.getConfig()
|
||||
l.log(flags, LevelInfo, cfg.TraceDepth, args...)
|
||||
}
|
||||
|
||||
// Warn logs a message at warning level
|
||||
func (l *Logger) Warn(args ...any) {
|
||||
flags := l.getFlags()
|
||||
cfg := l.getConfig()
|
||||
l.log(flags, LevelWarn, cfg.TraceDepth, args...)
|
||||
}
|
||||
|
||||
// Error logs a message at error level
|
||||
func (l *Logger) Error(args ...any) {
|
||||
flags := l.getFlags()
|
||||
cfg := l.getConfig()
|
||||
l.log(flags, LevelError, cfg.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|FlagNoLevel, LevelInfo, 0, args...)
|
||||
}
|
||||
|
||||
// Message writes a plain record without timestamp or level info
|
||||
func (l *Logger) Message(args ...any) {
|
||||
l.log(FlagNoTimestamp|FlagNoLevel, 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|FlagNoLevel, LevelInfo, int64(depth), args...)
|
||||
}
|
||||
|
||||
// LogStructured logs a message with structured fields as proper JSON
|
||||
func (l *Logger) LogStructured(level int64, message string, fields map[string]any) {
|
||||
l.log(l.getFlags()|FlagStructuredJSON, level, 0, message, fields)
|
||||
}
|
||||
|
||||
// Write outputs raw, unformatted data ignoring configured format and sanitization without trailing new line
|
||||
func (l *Logger) Write(args ...any) {
|
||||
l.log(FlagRaw, LevelInfo, 0, args...)
|
||||
}
|
||||
|
||||
// getConfig returns the current configuration (thread-safe)
|
||||
func (l *Logger) getConfig() *Config {
|
||||
return l.currentConfig.Load().(*Config)
|
||||
@@ -353,20 +139,19 @@ func (l *Logger) applyConfig(cfg *Config) error {
|
||||
oldCfg := l.getConfig()
|
||||
l.currentConfig.Store(cfg)
|
||||
|
||||
// Create formatter with sanitizer
|
||||
s := sanitizer.New().Policy(cfg.Sanitization)
|
||||
newFormatter := formatter.New(s).
|
||||
Type(cfg.Format).
|
||||
TimestampFormat(cfg.TimestampFormat).
|
||||
ShowLevel(cfg.ShowLevel).
|
||||
ShowTimestamp(cfg.ShowTimestamp)
|
||||
l.formatter.Store(newFormatter)
|
||||
// Shared formatter and sanitizer constructor with SetContextKeys
|
||||
l.rebuildFormatter(cfg)
|
||||
|
||||
// Emit fast-path mirrors
|
||||
l.state.Flags.Store(flagsFromConfig(cfg))
|
||||
l.state.TraceDepth.Store(cfg.TraceDepth)
|
||||
|
||||
// Ensure log directory exists if file output is enabled
|
||||
if cfg.EnableFile {
|
||||
if err := os.MkdirAll(cfg.Directory, 0755); err != nil {
|
||||
l.state.LoggerDisabled.Store(true)
|
||||
l.currentConfig.Store(oldCfg) // Rollback
|
||||
l.refreshLevelGate()
|
||||
return fmtErrorf("failed to create log directory '%s': %w", cfg.Directory, err)
|
||||
}
|
||||
}
|
||||
@@ -453,6 +238,7 @@ func (l *Logger) applyConfig(cfg *Config) error {
|
||||
l.state.ShutdownCalled.Store(false)
|
||||
l.state.DiskFullLogged.Store(false)
|
||||
l.state.DiskStatusOK.Store(true)
|
||||
l.refreshLevelGate()
|
||||
|
||||
// Restart processor if it was running and needs restart
|
||||
if needsRestart {
|
||||
@@ -461,3 +247,354 @@ func (l *Logger) applyConfig(cfg *Config) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start begins log processing. Safe to call multiple times
|
||||
// Returns error if logger is not initialized
|
||||
func (l *Logger) Start() error {
|
||||
if !l.state.IsInitialized.Load() {
|
||||
return fmtErrorf("logger not initialized, call ApplyConfig first")
|
||||
}
|
||||
|
||||
// Check if processor didn't exit cleanly last time
|
||||
if l.state.Started.Load() && !l.state.ProcessorExited.Load() {
|
||||
// Force stop to clean up
|
||||
l.internalLog("warning - processor still running from previous start, forcing stop\n")
|
||||
if err := l.Stop(); err != nil {
|
||||
return fmtErrorf("failed to stop hung processor: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Only start if not already started
|
||||
if l.state.Started.CompareAndSwap(false, true) {
|
||||
cfg := l.getConfig()
|
||||
|
||||
// Create log channels
|
||||
ch := make(chan logRecord, cfg.BufferSize)
|
||||
stop := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
|
||||
l.state.ActiveLogChannel.Store(ch)
|
||||
l.state.ProcStop.Store(stop)
|
||||
l.state.ProcDone.Store(done)
|
||||
l.state.ProcessorExited.Store(false)
|
||||
|
||||
// Start processor
|
||||
l.spawn(func() { l.processLogs(ch, stop, done) })
|
||||
}
|
||||
|
||||
l.refreshLevelGate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop halts log processing. Can be restarted with Start()
|
||||
// The record channel is never closed: producers are detached to a nil channel
|
||||
// first, so a concurrent send falls through to the drop counter instead of
|
||||
// racing a close.
|
||||
func (l *Logger) Stop(timeout ...time.Duration) error {
|
||||
if !l.state.Started.CompareAndSwap(true, false) {
|
||||
return nil // Already stopped
|
||||
}
|
||||
l.refreshLevelGate()
|
||||
|
||||
// Calculate effective timeout
|
||||
var effectiveTimeout time.Duration
|
||||
if len(timeout) > 0 {
|
||||
effectiveTimeout = timeout[0]
|
||||
} else {
|
||||
effectiveTimeout = 2 * time.Duration(l.getConfig().FlushIntervalMs) * time.Millisecond
|
||||
}
|
||||
if effectiveTimeout < minWaitTime {
|
||||
effectiveTimeout = minWaitTime
|
||||
}
|
||||
|
||||
// 1. Detach producers
|
||||
l.state.ActiveLogChannel.Store((chan logRecord)(nil))
|
||||
|
||||
// 2. Signal the processor to drain and exit
|
||||
if s, ok := l.state.ProcStop.Load().(chan struct{}); ok && s != nil {
|
||||
close(s)
|
||||
}
|
||||
|
||||
// 3. Join
|
||||
d, _ := l.state.ProcDone.Load().(chan struct{})
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-d:
|
||||
return nil
|
||||
case <-time.After(effectiveTimeout):
|
||||
return fmtErrorf("processor did not exit within timeout (%v)", effectiveTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown gracefully closes the logger, attempting to flush pending records
|
||||
// If no timeout is provided, uses a default of 2x flush interval
|
||||
func (l *Logger) Shutdown(timeout ...time.Duration) error {
|
||||
if !l.state.ShutdownCalled.CompareAndSwap(false, true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.state.LoggerDisabled.Store(true)
|
||||
l.refreshLevelGate()
|
||||
|
||||
if !l.state.IsInitialized.Load() {
|
||||
l.state.ShutdownCalled.Store(false)
|
||||
l.state.LoggerDisabled.Store(false)
|
||||
l.state.ProcessorExited.Store(true)
|
||||
l.refreshLevelGate()
|
||||
return nil
|
||||
}
|
||||
|
||||
var stopErr error
|
||||
if l.state.Started.Load() {
|
||||
stopErr = l.Stop(timeout...)
|
||||
}
|
||||
|
||||
l.state.IsInitialized.Store(false)
|
||||
|
||||
var finalErr error
|
||||
cfPtr := l.state.CurrentFile.Load()
|
||||
if cfPtr != nil {
|
||||
if currentLogFile, ok := cfPtr.(*os.File); ok && currentLogFile != nil {
|
||||
if err := currentLogFile.Sync(); err != nil {
|
||||
syncErr := fmtErrorf("failed to sync log file '%s' during shutdown: %w", currentLogFile.Name(), err)
|
||||
finalErr = errors.Join(finalErr, syncErr)
|
||||
}
|
||||
if err := currentLogFile.Close(); err != nil {
|
||||
closeErr := fmtErrorf("failed to close log file '%s' during shutdown: %w", currentLogFile.Name(), err)
|
||||
finalErr = errors.Join(finalErr, closeErr)
|
||||
}
|
||||
l.state.CurrentFile.Store((*os.File)(nil))
|
||||
}
|
||||
}
|
||||
|
||||
if stopErr != nil {
|
||||
finalErr = errors.Join(finalErr, stopErr)
|
||||
}
|
||||
|
||||
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 {
|
||||
l.state.flushMutex.Lock()
|
||||
defer l.state.flushMutex.Unlock()
|
||||
|
||||
// State checks
|
||||
if !l.state.IsInitialized.Load() || l.state.ShutdownCalled.Load() {
|
||||
return fmtErrorf("logger not initialized or already shut down")
|
||||
}
|
||||
if !l.state.Started.Load() {
|
||||
return fmtErrorf("logger not started")
|
||||
}
|
||||
|
||||
// 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(minWaitTime): // Short timeout to prevent blocking if processor is stuck
|
||||
return fmtErrorf("failed to send flush request to processor (possible deadlock or high load)")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-confirmChan:
|
||||
return nil
|
||||
case <-time.After(timeout):
|
||||
return fmtErrorf("timeout waiting for flush confirmation (%v)", timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// SetSpawn installs the goroutine launcher used by Start. Hosts that own
|
||||
// panic recovery and terminal teardown pass their own launcher here.
|
||||
// Call before Start; a nil fn restores the default.
|
||||
func (l *Logger) SetSpawn(fn func(func())) {
|
||||
if fn == nil {
|
||||
l.spawner.Store(nil)
|
||||
return
|
||||
}
|
||||
l.spawner.Store(&fn)
|
||||
}
|
||||
|
||||
// SetErrorHandler routes internal diagnostics to fn instead of stderr.
|
||||
// Required for TUI hosts, where stderr writes corrupt the display.
|
||||
func (l *Logger) SetErrorHandler(fn func(string)) {
|
||||
if fn == nil {
|
||||
l.errHandler.Store(nil)
|
||||
return
|
||||
}
|
||||
l.errHandler.Store(&fn)
|
||||
}
|
||||
|
||||
// SetContextKeys names the record keys for Context values; empty names are
|
||||
// omitted. Safe to call at any time; rebuilds the formatter.
|
||||
func (l *Logger) SetContextKeys(tag string, vals ...string) {
|
||||
l.initMu.Lock()
|
||||
defer l.initMu.Unlock()
|
||||
|
||||
k := formatter.ContextKeys{Tag: tag}
|
||||
for i := 0; i < len(vals) && i < formatter.ContextSlots; i++ {
|
||||
k.Vals[i] = vals[i]
|
||||
}
|
||||
l.ctxKeys.Store(&k)
|
||||
l.rebuildFormatter(l.getConfig())
|
||||
}
|
||||
|
||||
// SetLevel changes the emit threshold in place, leaving the formatter and the
|
||||
// processor untouched
|
||||
func (l *Logger) SetLevel(level int64) {
|
||||
l.initMu.Lock()
|
||||
cfg := l.getConfig().Clone()
|
||||
cfg.Level = level
|
||||
l.currentConfig.Store(cfg)
|
||||
l.initMu.Unlock()
|
||||
l.refreshLevelGate()
|
||||
}
|
||||
|
||||
// Enabled reports whether a record at level would be emitted. Single atomic
|
||||
// load: the intended guard for hot call sites, where argument slices are
|
||||
// built before the call and would otherwise escape to the heap.
|
||||
func (l *Logger) Enabled(level int64) bool {
|
||||
return level >= l.state.Level.Load()
|
||||
}
|
||||
|
||||
// Flags returns the default record flags derived from display config
|
||||
func (l *Logger) Flags() int64 {
|
||||
return l.state.Flags.Load()
|
||||
}
|
||||
|
||||
// LogContext emits a record with caller-supplied context and explicit flags
|
||||
func (l *Logger) LogContext(ctx Context, flags, level, depth int64, args ...any) {
|
||||
if level < l.state.Level.Load() {
|
||||
return
|
||||
}
|
||||
l.emit(ctx, flags, level, depth, args)
|
||||
}
|
||||
|
||||
// spawn runs fn via the configured launcher; the default is a bare goroutine
|
||||
func (l *Logger) spawn(fn func()) {
|
||||
if p := l.spawner.Load(); p != nil {
|
||||
(*p)(fn)
|
||||
return
|
||||
}
|
||||
go fn()
|
||||
}
|
||||
|
||||
// rebuildFormatter installs a formatter matching cfg and the current context keys
|
||||
func (l *Logger) rebuildFormatter(cfg *Config) {
|
||||
f := formatter.New(sanitizer.New().Policy(cfg.Sanitization)).
|
||||
Type(cfg.Format).
|
||||
TimestampFormat(cfg.TimestampFormat).
|
||||
ShowLevel(cfg.ShowLevel).
|
||||
ShowTimestamp(cfg.ShowTimestamp)
|
||||
if k := l.ctxKeys.Load(); k != nil {
|
||||
f.ContextKeys(k.Tag, k.Vals[:]...)
|
||||
}
|
||||
l.formatter.Store(f)
|
||||
}
|
||||
|
||||
// flagsFromConfig derives the default record flags from display settings
|
||||
func flagsFromConfig(cfg *Config) int64 {
|
||||
var flags int64
|
||||
if cfg.ShowLevel {
|
||||
flags |= FlagShowLevel
|
||||
}
|
||||
if cfg.ShowTimestamp {
|
||||
flags |= FlagShowTimestamp
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
// refreshLevelGate recomputes the emit gate from lifecycle state.
|
||||
// MUST be called after every transition of IsInitialized, Started,
|
||||
// LoggerDisabled, or ShutdownCalled.
|
||||
func (l *Logger) refreshLevelGate() {
|
||||
if !l.state.IsInitialized.Load() || !l.state.Started.Load() ||
|
||||
l.state.LoggerDisabled.Load() || l.state.ShutdownCalled.Load() {
|
||||
l.state.Level.Store(levelOff)
|
||||
return
|
||||
}
|
||||
l.state.Level.Store(l.getConfig().Level)
|
||||
}
|
||||
|
||||
// === Logging methods ===
|
||||
|
||||
// Debug logs a message at debug level
|
||||
func (l *Logger) Debug(args ...any) {
|
||||
if LevelDebug < l.state.Level.Load() {
|
||||
return
|
||||
}
|
||||
l.emit(Context{}, l.getFlags(), LevelDebug, l.state.TraceDepth.Load(), args)
|
||||
}
|
||||
|
||||
// Info logs a message at info level
|
||||
func (l *Logger) Info(args ...any) {
|
||||
if LevelInfo < l.state.Level.Load() {
|
||||
return
|
||||
}
|
||||
l.emit(Context{}, l.getFlags(), LevelInfo, l.state.TraceDepth.Load(), args)
|
||||
}
|
||||
|
||||
// Warn logs a message at warning level
|
||||
func (l *Logger) Warn(args ...any) {
|
||||
if LevelWarn < l.state.Level.Load() {
|
||||
return
|
||||
}
|
||||
l.emit(Context{}, l.getFlags(), LevelWarn, l.state.TraceDepth.Load(), args)
|
||||
}
|
||||
|
||||
// Error logs a message at error level
|
||||
func (l *Logger) Error(args ...any) {
|
||||
if LevelError < l.state.Level.Load() {
|
||||
return
|
||||
}
|
||||
l.emit(Context{}, l.getFlags(), LevelError, l.state.TraceDepth.Load(), args)
|
||||
}
|
||||
|
||||
// DebugTrace logs a debug message with function call trace
|
||||
func (l *Logger) DebugTrace(depth int, args ...any) {
|
||||
l.LogContext(Context{}, l.getFlags(), LevelDebug, int64(depth), args...)
|
||||
}
|
||||
|
||||
// InfoTrace logs an info message with function call trace
|
||||
func (l *Logger) InfoTrace(depth int, args ...any) {
|
||||
l.LogContext(Context{}, l.getFlags(), LevelInfo, int64(depth), args...)
|
||||
}
|
||||
|
||||
// WarnTrace logs a warning message with function call trace
|
||||
func (l *Logger) WarnTrace(depth int, args ...any) {
|
||||
l.LogContext(Context{}, l.getFlags(), LevelWarn, int64(depth), args...)
|
||||
}
|
||||
|
||||
// ErrorTrace logs an error message with function call trace
|
||||
func (l *Logger) ErrorTrace(depth int, args ...any) {
|
||||
l.LogContext(Context{}, l.getFlags(), LevelError, int64(depth), args...)
|
||||
}
|
||||
|
||||
// Log writes a timestamp-only record without level information
|
||||
func (l *Logger) Log(args ...any) {
|
||||
l.LogContext(Context{}, FlagShowTimestamp|FlagNoLevel, LevelInfo, 0, args...)
|
||||
}
|
||||
|
||||
// Message writes a plain record without timestamp or level info
|
||||
func (l *Logger) Message(args ...any) {
|
||||
l.LogContext(Context{}, FlagNoTimestamp|FlagNoLevel, LevelInfo, 0, args...)
|
||||
}
|
||||
|
||||
// LogTrace writes a timestamp record with call trace but no level info
|
||||
func (l *Logger) LogTrace(depth int, args ...any) {
|
||||
l.LogContext(Context{}, FlagShowTimestamp|FlagNoLevel, LevelInfo, int64(depth), args...)
|
||||
}
|
||||
|
||||
// LogStructured logs a message with structured fields as proper JSON
|
||||
func (l *Logger) LogStructured(level int64, message string, fields map[string]any) {
|
||||
l.LogContext(Context{}, l.getFlags()|FlagStructuredJSON, level, 0, message, fields)
|
||||
}
|
||||
|
||||
// Write outputs raw, unformatted data ignoring configured format and sanitization
|
||||
func (l *Logger) Write(args ...any) {
|
||||
l.LogContext(Context{}, FlagRaw, LevelInfo, 0, args...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user