Test / test (push) Canceled after 0s
Test / fuzz (./compat, FuzzParseFormat) (push) Canceled after 0s
Test / fuzz (./formatter, FuzzFormatJSON) (push) Canceled after 0s
Test / fuzz (./formatter, FuzzFormatTextPolicy) (push) Canceled after 0s
Test / fuzz (./sanitizer, FuzzSanitize) (push) Canceled after 0s
Test / fuzz (./sanitizer, FuzzSerializerJSON) (push) Canceled after 0s
A host that draws on the terminal (a TUI) cannot let the logger write to stdout or stderr, yet the records are worth showing inside the host. SetErrorHandler already covers internal diagnostics; SetConsoleWriter is its sibling for console output. - SetConsoleWriter(w) sends console output of every console_target to w, split's warn-and-above records included; nil restores stdout/stderr. It holds across ApplyConfig and is safe while logging. - One stored value: ApplyConfig and SetConsoleWriter publish the console writers through storeConsole; the processor's split branch now reads the stored warn writer instead of writing os.Stderr directly. - TestLoggerConsoleTargets becomes TestConsoleWriterTakesEveryTarget, which asserts where each target's records land; a second test checks that nil restores the configured writers. - README names both hooks for terminal hosts; the API doc's callback contract covers console writers. Verified: go build ./... ok; gofmt -l on changed files clean; go vet ./... ok; go test ./... ok (all four packages, under 1 s); go test -race on the new tests ok; mutation run 6 of 6 caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyaMAhyEzAp3ULLWJZG4jQ
580 lines
17 KiB
Go
580 lines
17 KiB
Go
package log
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"math"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/lixenwraith/log/formatter"
|
|
"github.com/lixenwraith/log/sanitizer"
|
|
)
|
|
|
|
// Logger is the core struct that encapsulates all logger functionality
|
|
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)]
|
|
consoleW io.Writer // SetConsoleWriter's writer, under initMu
|
|
state State
|
|
initMu sync.Mutex // serializes lifecycle and configuration changes
|
|
sendMu sync.RWMutex // joins enqueues before detaching a processor
|
|
processorErr error // written by processor, read only after ProcDone closes
|
|
}
|
|
|
|
// 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{}
|
|
|
|
// Set default configuration
|
|
defaultCfg := DefaultConfig()
|
|
l.currentConfig.Store(defaultCfg)
|
|
l.rebuildFormatter(defaultCfg)
|
|
|
|
// 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 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{})
|
|
|
|
// Initialize heartbeat counters
|
|
l.state.HeartbeatSequence.Store(0)
|
|
l.state.LoggerStartTime.Store(time.Now())
|
|
l.state.TotalLogsProcessed.Store(0)
|
|
l.state.TotalRotations.Store(0)
|
|
l.state.TotalDeletions.Store(0)
|
|
|
|
// Typed nil: a non-blocking send on a nil channel always takes default
|
|
l.state.ActiveLogChannel.Store((chan logRecord)(nil))
|
|
|
|
return l
|
|
}
|
|
|
|
// ApplyConfig applies a validated configuration to the logger
|
|
// This is the primary way applications should configure the logger
|
|
func (l *Logger) ApplyConfig(cfg *Config) error {
|
|
if cfg == nil {
|
|
return fmt.Errorf("log: configuration cannot be nil")
|
|
}
|
|
|
|
cfg = cfg.Clone()
|
|
if err := cfg.Validate(); err != nil {
|
|
return fmt.Errorf("log: invalid configuration: %w", err)
|
|
}
|
|
|
|
l.initMu.Lock()
|
|
defer l.initMu.Unlock()
|
|
|
|
return l.applyConfig(cfg)
|
|
}
|
|
|
|
// ApplyConfigString applies string key-value overrides to the logger's current configuration
|
|
// Each override should be in the format "key=value"
|
|
func (l *Logger) ApplyConfigString(overrides ...string) error {
|
|
l.initMu.Lock()
|
|
defer l.initMu.Unlock()
|
|
cfg := l.getConfig().Clone()
|
|
|
|
var errors []error
|
|
|
|
for _, override := range overrides {
|
|
key, value, err := parseKeyValue(override)
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
continue
|
|
}
|
|
|
|
if err := applyConfigField(cfg, key, value); err != nil {
|
|
errors = append(errors, err)
|
|
}
|
|
}
|
|
|
|
if len(errors) > 0 {
|
|
return combineConfigErrors(errors)
|
|
}
|
|
|
|
if err := cfg.Validate(); err != nil {
|
|
return fmtErrorf("invalid configuration: %w", err)
|
|
}
|
|
return l.applyConfig(cfg)
|
|
}
|
|
|
|
// GetConfig returns a copy of current configuration
|
|
func (l *Logger) GetConfig() *Config {
|
|
return l.getConfig().Clone()
|
|
}
|
|
|
|
// getConfig returns the current configuration (thread-safe)
|
|
func (l *Logger) getConfig() *Config {
|
|
return l.currentConfig.Load().(*Config)
|
|
}
|
|
|
|
// applyConfig is the internal implementation for applying configuration, assuming initMu is held
|
|
func (l *Logger) applyConfig(cfg *Config) error {
|
|
oldCfg := l.getConfig()
|
|
wasStarted := l.state.Started.Load()
|
|
needsRestart := wasStarted && configRequiresRestart(oldCfg, cfg)
|
|
|
|
// A timed-out processor still owns its file and formatter. Do not replace
|
|
// either resource until it has actually exited.
|
|
if !wasStarted && !l.state.ProcessorExited.Load() {
|
|
return fmtErrorf("previous processor is still stopping")
|
|
}
|
|
|
|
currentFile, _ := l.state.CurrentFile.Load().(*os.File)
|
|
needsNewFile := cfg.EnableFile && (!l.state.IsInitialized.Load() || !oldCfg.EnableFile ||
|
|
!wasStarted && currentFile == nil || l.state.LoggerDisabled.Load() ||
|
|
oldCfg.Directory != cfg.Directory || oldCfg.Name != cfg.Name || oldCfg.Extension != cfg.Extension)
|
|
var newFile *os.File
|
|
if cfg.EnableFile {
|
|
if err := os.MkdirAll(cfg.Directory, 0755); err != nil {
|
|
return fmtErrorf("failed to create log directory '%s': %w", cfg.Directory, err)
|
|
}
|
|
if needsNewFile {
|
|
var err error
|
|
newFile, err = openLogFile(cfg)
|
|
if err != nil {
|
|
return fmtErrorf("failed to create log file: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drain with the old configuration and file before publishing the new one.
|
|
if needsRestart || wasStarted && needsNewFile {
|
|
needsRestart = true
|
|
if err := l.stopLocked(); err != nil {
|
|
if newFile != nil {
|
|
_ = newFile.Close()
|
|
}
|
|
return fmtErrorf("failed to stop processor for restart: %w", err)
|
|
}
|
|
}
|
|
|
|
// Draining may rotate the old file; close the handle the processor left behind.
|
|
currentFile, _ = l.state.CurrentFile.Load().(*os.File)
|
|
if !cfg.EnableFile || newFile != nil {
|
|
if currentFile != nil {
|
|
if err := currentFile.Close(); err != nil {
|
|
l.internalLog("failed to close old log file: %v\n", err)
|
|
}
|
|
}
|
|
l.state.CurrentFile.Store(newFile)
|
|
l.state.CurrentSize.Store(0)
|
|
if newFile != nil {
|
|
if info, err := newFile.Stat(); err == nil {
|
|
l.state.CurrentSize.Store(info.Size())
|
|
}
|
|
}
|
|
}
|
|
|
|
l.storeConsole(cfg)
|
|
l.currentConfig.Store(cfg)
|
|
l.rebuildFormatter(cfg)
|
|
l.state.Flags.Store(flagsFromConfig(cfg))
|
|
l.state.TraceDepth.Store(cfg.TraceDepth)
|
|
l.state.IsInitialized.Store(true)
|
|
l.state.ShutdownCalled.Store(false)
|
|
// A running processor may have just reported an output failure. A hot
|
|
// configuration update must not revive its invalid file handle.
|
|
if !wasStarted || needsRestart {
|
|
l.state.LoggerDisabled.Store(false)
|
|
l.state.DiskFullLogged.Store(false)
|
|
}
|
|
l.state.DiskStatusOK.Store(true)
|
|
l.refreshLevelGate()
|
|
if needsRestart {
|
|
return l.startLocked()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Start begins log processing. Repeated calls while running are no-ops.
|
|
// ApplyConfig must succeed first. A processor still stopping cannot be restarted.
|
|
func (l *Logger) Start() error {
|
|
l.initMu.Lock()
|
|
defer l.initMu.Unlock()
|
|
return l.startLocked()
|
|
}
|
|
|
|
func (l *Logger) startLocked() error {
|
|
if !l.state.IsInitialized.Load() || l.state.ShutdownCalled.Load() {
|
|
return fmtErrorf("logger not initialized, call ApplyConfig first")
|
|
}
|
|
if l.state.Started.Load() {
|
|
if l.state.ProcessorExited.Load() {
|
|
return fmtErrorf("processor exited unexpectedly; call Stop before restarting")
|
|
}
|
|
return nil
|
|
}
|
|
if !l.state.ProcessorExited.Load() {
|
|
return fmtErrorf("previous processor is still stopping")
|
|
}
|
|
|
|
ch := make(chan logRecord, l.getConfig().BufferSize)
|
|
stop, done := make(chan struct{}), make(chan struct{})
|
|
flush := make(chan flushRequest, 1)
|
|
l.processorErr = nil
|
|
l.sendMu.Lock()
|
|
l.state.ActiveLogChannel.Store(ch)
|
|
l.state.ProcStop.Store(stop)
|
|
l.state.ProcDone.Store(done)
|
|
l.state.flushRequestChan = flush
|
|
l.state.ProcessorExited.Store(false)
|
|
l.state.Started.Store(true)
|
|
l.refreshLevelGate()
|
|
l.sendMu.Unlock()
|
|
l.spawn(func() { l.processLogs(ch, stop, done, flush) })
|
|
return nil
|
|
}
|
|
|
|
// Stop detaches producers, drains accepted records, and joins the processor.
|
|
// After a timeout, Stop can be retried; Start and resource replacement remain
|
|
// blocked until that processor exits. The default timeout is 2x flush interval.
|
|
func (l *Logger) Stop(timeout ...time.Duration) error {
|
|
l.initMu.Lock()
|
|
defer l.initMu.Unlock()
|
|
return l.stopLocked(timeout...)
|
|
}
|
|
|
|
func (l *Logger) stopLocked(timeout ...time.Duration) error {
|
|
l.sendMu.Lock()
|
|
if l.state.Started.Swap(false) {
|
|
l.refreshLevelGate()
|
|
l.state.ActiveLogChannel.Store((chan logRecord)(nil))
|
|
close(l.state.ProcStop.Load().(chan struct{}))
|
|
}
|
|
l.sendMu.Unlock()
|
|
|
|
done, _ := l.state.ProcDone.Load().(chan struct{})
|
|
if done == nil {
|
|
return nil
|
|
}
|
|
effectiveTimeout := 2 * time.Duration(l.getConfig().FlushIntervalMs) * time.Millisecond
|
|
if len(timeout) > 0 {
|
|
effectiveTimeout = timeout[0]
|
|
}
|
|
effectiveTimeout = max(effectiveTimeout, minWaitTime)
|
|
// Prefer an already completed processor even with a tiny timeout.
|
|
select {
|
|
case <-done:
|
|
return l.processorErr
|
|
default:
|
|
}
|
|
timer := time.NewTimer(effectiveTimeout)
|
|
defer timer.Stop()
|
|
select {
|
|
case <-done:
|
|
return l.processorErr
|
|
case <-timer.C:
|
|
return fmtErrorf("processor did not exit within timeout (%v)", effectiveTimeout)
|
|
}
|
|
}
|
|
|
|
// Shutdown stops the logger and closes its file after the processor exits.
|
|
// If joining times out, resources remain owned by the processor; retry Shutdown
|
|
// to finish cleanup. A completed shutdown can be reset by ApplyConfig.
|
|
func (l *Logger) Shutdown(timeout ...time.Duration) error {
|
|
l.initMu.Lock()
|
|
defer l.initMu.Unlock()
|
|
if !l.state.IsInitialized.Load() {
|
|
return nil
|
|
}
|
|
l.state.ShutdownCalled.Store(true)
|
|
l.state.LoggerDisabled.Store(true)
|
|
l.refreshLevelGate()
|
|
err := l.stopLocked(timeout...)
|
|
if !l.state.ProcessorExited.Load() {
|
|
return err
|
|
}
|
|
l.state.IsInitialized.Store(false)
|
|
if file, _ := l.state.CurrentFile.Load().(*os.File); file != nil {
|
|
err = errors.Join(err, file.Sync(), file.Close())
|
|
l.state.CurrentFile.Store((*os.File)(nil))
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Flush processes records queued before the request and syncs the current file.
|
|
// The timeout covers queueing and confirmation. Concurrent later records need
|
|
// not be drained, so sustained producers cannot starve the barrier.
|
|
func (l *Logger) Flush(timeout time.Duration) error {
|
|
l.sendMu.RLock()
|
|
if !l.state.IsInitialized.Load() || l.state.ShutdownCalled.Load() {
|
|
l.sendMu.RUnlock()
|
|
return fmtErrorf("logger not initialized or already shut down")
|
|
}
|
|
if !l.state.Started.Load() {
|
|
l.sendMu.RUnlock()
|
|
return fmtErrorf("logger not started")
|
|
}
|
|
requests := l.state.flushRequestChan
|
|
done := l.state.ProcDone.Load().(chan struct{})
|
|
l.sendMu.RUnlock()
|
|
if timeout <= 0 {
|
|
return fmtErrorf("timeout waiting for flush confirmation (%v)", timeout)
|
|
}
|
|
|
|
timer := time.NewTimer(timeout)
|
|
defer timer.Stop()
|
|
request := flushRequest{done: make(chan error, 1)}
|
|
select {
|
|
case requests <- request:
|
|
case <-done:
|
|
return fmtErrorf("processor stopped during flush")
|
|
case <-timer.C:
|
|
return fmtErrorf("timeout sending flush request (%v)", timeout)
|
|
}
|
|
select {
|
|
case err := <-request.done:
|
|
return err
|
|
case <-done:
|
|
return fmtErrorf("processor stopped during flush")
|
|
case <-timer.C:
|
|
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; fn must launch asynchronously and return promptly.
|
|
// 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. The callback
|
|
// must return promptly and must not call lifecycle/configuration methods or Flush.
|
|
func (l *Logger) SetErrorHandler(fn func(string)) {
|
|
if fn == nil {
|
|
l.errHandler.Store(nil)
|
|
return
|
|
}
|
|
l.errHandler.Store(&fn)
|
|
}
|
|
|
|
// SetConsoleWriter sends console output to w instead of stdout and stderr,
|
|
// for every console_target, split included; nil restores them. It holds across
|
|
// ApplyConfig and is safe while logging. Writes must return promptly and must
|
|
// not call lifecycle/configuration methods or Flush, as for SetErrorHandler.
|
|
func (l *Logger) SetConsoleWriter(w io.Writer) {
|
|
l.initMu.Lock()
|
|
defer l.initMu.Unlock()
|
|
l.consoleW = w
|
|
l.storeConsole(l.getConfig())
|
|
}
|
|
|
|
// storeConsole publishes the writers the processor uses for cfg's console output
|
|
func (l *Logger) storeConsole(cfg *Config) {
|
|
w, warn := l.consoleW, l.consoleW
|
|
switch {
|
|
case !cfg.EnableConsole:
|
|
w, warn = io.Discard, io.Discard
|
|
case w != nil:
|
|
case cfg.ConsoleTarget == "stderr":
|
|
w, warn = os.Stderr, os.Stderr
|
|
case cfg.ConsoleTarget == "split":
|
|
w, warn = os.Stdout, os.Stderr
|
|
default:
|
|
w, warn = os.Stdout, os.Stdout
|
|
}
|
|
l.state.StdoutWriter.Store(&sink{w: w, warn: warn})
|
|
}
|
|
|
|
// 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.refreshLevelGate()
|
|
l.initMu.Unlock()
|
|
}
|
|
|
|
// 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 {
|
|
threshold := l.state.Level.Load()
|
|
return threshold != levelOff && level >= threshold
|
|
}
|
|
|
|
// 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 !l.Enabled(level) {
|
|
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) {
|
|
if !l.Enabled(level) {
|
|
return
|
|
}
|
|
l.LogContext(Context{}, l.getFlags()|FlagStructuredJSON, level, 0, message, maps.Clone(fields))
|
|
}
|
|
|
|
// Write outputs raw, unformatted data ignoring configured format and sanitization
|
|
func (l *Logger) Write(args ...any) {
|
|
l.LogContext(Context{}, FlagRaw, LevelInfo, 0, args...)
|
|
}
|