Files
log/processor.go
T
lixen 2417e51da1
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
v0.1.11 harden logging lifecycle, sanitization, and formatter edge cases
2026-09-26 14:25:14 -04:00

296 lines
8.0 KiB
Go

package log
import (
"io"
"os"
"time"
"github.com/lixenwraith/log/formatter"
)
// processLogs is the main log processing loop running in a separate goroutine.
// Exits on stop, draining buffered records first. No panic recovery: a fault
// here is fatal by design and is surfaced by the host's spawner.
func (l *Logger) processLogs(ch <-chan logRecord, stop <-chan struct{}, done chan<- struct{}, flush <-chan flushRequest) {
defer func() {
l.state.ProcessorExited.Store(true)
close(done)
}()
// Set up timers and state variables
timers := l.setupProcessingTimers()
defer l.stopProcessingTimers(timers)
c := l.getConfig()
// Perform an initial disk check on startup (skip if file output is disabled)
if c.EnableFile {
l.performDiskCheck(true)
}
// Send initial heartbeats immediately instead of waiting for first tick
heartbeatLevel := c.HeartbeatLevel
if heartbeatLevel > 0 {
if heartbeatLevel >= 1 {
l.logProcHeartbeat()
}
if heartbeatLevel >= 2 {
l.logDiskHeartbeat()
}
if heartbeatLevel >= 3 {
l.logSysHeartbeat()
}
}
// State variables for adaptive disk checks
var bytesSinceLastCheck int64 = 0
var lastCheckTime = time.Now()
var logsSinceLastCheck int64 = 0
// --- Main Loop ---
for {
select {
case <-stop:
l.drain(ch)
l.processorErr = l.performSync()
return
case record := <-ch:
// Process the received log record
bytesWritten := l.processLogRecord(record)
if bytesWritten > 0 {
// Update adaptive check counters
bytesSinceLastCheck += bytesWritten
logsSinceLastCheck++
// Reactive Check Trigger
if bytesSinceLastCheck > reactiveCheckThresholdBytes {
if l.performDiskCheck(false) {
bytesSinceLastCheck = 0
logsSinceLastCheck = 0
lastCheckTime = time.Now()
}
}
}
case <-timers.flushTicker.C:
l.handleFlushTick()
case <-timers.diskCheckTicker.C:
// Periodic disk check
if l.performDiskCheck(true) {
l.adjustDiskCheckInterval(timers, lastCheckTime, logsSinceLastCheck)
bytesSinceLastCheck = 0
logsSinceLastCheck = 0
lastCheckTime = time.Now()
}
case request := <-flush:
// Barrier: drain queued records before sync
l.handleFlushRequest(ch, request)
case <-timers.retentionChan:
l.handleRetentionCheck()
case <-timers.heartbeatChan:
l.handleHeartbeat()
}
}
}
// drain processes every buffered record without blocking
func (l *Logger) drain(ch <-chan logRecord) {
// A snapshot bounds the flush work even while producers keep the queue full.
// Stop has already joined producers, so this also drains its entire queue.
for range len(ch) {
l.processLogRecord(<-ch)
}
}
func (l *Logger) handleFlushRequest(ch <-chan logRecord, request flushRequest) {
l.drain(ch)
request.done <- l.performSync()
}
// processLogRecord handles individual log records and returns bytes written
func (l *Logger) processLogRecord(record logRecord) int64 {
c := l.getConfig()
enableFile := c.EnableFile
if enableFile && !l.state.DiskStatusOK.Load() {
// Simple increment of both counters
l.handleFailedSend()
return 0
}
// Atomically load formatter instance
formatterPtr := l.formatter.Load()
if formatterPtr == nil {
// Defensive: Should never happen after initialization
return 0
}
f := formatterPtr.(*formatter.Formatter)
// Format the log entry using atomically-loaded formatter
formattedData := f.FormatCtx(
record.Ctx,
record.Flags,
record.TimeStamp,
record.Level,
record.Trace,
record.Args,
)
formattedDataLen := int64(len(formattedData))
var consoleErr error
if c.EnableConsole {
if target, _ := l.state.StdoutWriter.Load().(*sink); target != nil {
writer := target.w
if c.ConsoleTarget == "split" && record.Level >= LevelWarn {
writer = os.Stderr
}
n, err := writer.Write(formattedData)
if err == nil && n != len(formattedData) {
err = io.ErrShortWrite
}
consoleErr = err
if err != nil {
l.internalLog("failed to write to console: %v\n", err)
}
}
}
// Skip file operations if file output is disabled
if !enableFile {
if consoleErr != nil {
l.handleFailedSend()
return 0
}
if !record.heartbeat {
l.state.TotalLogsProcessed.Add(1)
}
return formattedDataLen // Return data length for adaptive interval calculations
}
// File rotation check
currentFileSize := l.state.CurrentSize.Load()
estimatedSize := currentFileSize + formattedDataLen
maxSizeKB := c.MaxSizeKB
if maxSizeKB > 0 && estimatedSize > maxSizeKB*sizeMultiplier {
if err := l.rotateLogFile(); err != nil {
l.internalLog("failed to rotate log file: %v\n", err)
// Account for the dropped log that triggered the failed rotation
l.handleFailedSend()
return 0
}
}
// Write to file
cfPtr := l.state.CurrentFile.Load()
if currentLogFile, isFile := cfPtr.(*os.File); isFile && currentLogFile != nil {
n, err := currentLogFile.Write(formattedData)
if err != nil {
l.internalLog("failed to write to log file: %v\n", err)
l.handleFailedSend()
l.state.CurrentSize.Add(int64(n))
l.performDiskCheck(true)
return 0
} else {
l.state.CurrentSize.Add(int64(n))
if consoleErr != nil {
l.handleFailedSend()
} else if !record.heartbeat {
l.state.TotalLogsProcessed.Add(1)
}
return int64(n)
}
} else {
l.handleFailedSend()
return 0
}
}
// handleFlushTick handles the periodic flush timer tick
func (l *Logger) handleFlushTick() {
c := l.getConfig()
enableSync := c.EnablePeriodicSync
if enableSync {
if err := l.performSync(); err != nil {
l.internalLog("failed to sync log file: %v\n", err)
}
}
}
// handleRetentionCheck performs file retention check and cleanup
func (l *Logger) handleRetentionCheck() {
c := l.getConfig()
retentionPeriodHrs := c.RetentionPeriodHrs
retentionDur := time.Duration(retentionPeriodHrs * float64(time.Hour))
if c.EnableFile && retentionDur > 0 {
etPtr := l.state.EarliestFileTime.Load()
if earliest, ok := etPtr.(time.Time); ok && !earliest.IsZero() {
if time.Since(earliest) > retentionDur {
if err := l.cleanExpiredLogs(earliest); err == nil {
l.updateEarliestFileTime()
} else {
l.internalLog("failed to clean expired logs: %v\n", err)
}
}
} else if !ok || earliest.IsZero() {
l.updateEarliestFileTime()
}
}
}
// adjustDiskCheckInterval modifies the disk check interval based on logging activity
func (l *Logger) adjustDiskCheckInterval(timers *TimerSet, lastCheckTime time.Time, logsSinceLastCheck int64) {
c := l.getConfig()
enableAdaptive := c.EnableAdaptiveInterval
if !enableAdaptive {
return
}
elapsed := max(time.Since(lastCheckTime), minWaitTime) // Min arbitrary reasonable value
logsPerSecond := float64(logsSinceLastCheck) / elapsed.Seconds()
targetLogsPerSecond := float64(100) // Baseline
currentDiskCheckInterval := timers.diskInterval
if currentDiskCheckInterval == 0 {
currentDiskCheckInterval = time.Duration(c.DiskCheckIntervalMs) * time.Millisecond
}
// Calculate the new interval
var newInterval time.Duration
if logsPerSecond < targetLogsPerSecond/2 { // Load low -> increase interval
scaled := float64(currentDiskCheckInterval) * adaptiveIntervalFactor
if scaled >= float64(time.Duration(c.MaxCheckIntervalMs)*time.Millisecond) {
newInterval = time.Duration(c.MaxCheckIntervalMs) * time.Millisecond
} else {
newInterval = time.Duration(scaled)
}
} else if logsPerSecond > targetLogsPerSecond*2 { // Load high -> decrease interval
newInterval = time.Duration(float64(currentDiskCheckInterval) * adaptiveSpeedUpFactor)
} else {
// No change needed if within normal range
return
}
// Clamp interval using current config
minCheckIntervalMs := c.MinCheckIntervalMs
maxCheckIntervalMs := c.MaxCheckIntervalMs
minCheckInterval := time.Duration(minCheckIntervalMs) * time.Millisecond
maxCheckInterval := time.Duration(maxCheckIntervalMs) * time.Millisecond
if newInterval < minCheckInterval {
newInterval = minCheckInterval
}
if newInterval > maxCheckInterval {
newInterval = maxCheckInterval
}
timers.diskInterval = newInterval
timers.diskCheckTicker.Reset(newInterval)
}