e1.0.1 Minor feature add, file restructure.
This commit is contained in:
213
state.go
213
state.go
@ -1,7 +1,15 @@
|
||||
// --- File: state.go ---
|
||||
package log
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/LixenWraith/config"
|
||||
)
|
||||
|
||||
// State encapsulates the runtime state of the logger
|
||||
@ -13,6 +21,9 @@ type State struct {
|
||||
DiskStatusOK atomic.Bool
|
||||
ProcessorExited atomic.Bool // Tracks if the processor goroutine is running or has exited
|
||||
|
||||
flushRequestChan chan chan struct{} // Channel to request a flush
|
||||
flushMutex sync.Mutex // Protect concurrent Flush calls
|
||||
|
||||
CurrentFile atomic.Value // stores *os.File
|
||||
CurrentSize atomic.Int64 // Size of the current log file
|
||||
EarliestFileTime atomic.Value // stores time.Time for retention
|
||||
@ -20,4 +31,206 @@ type State struct {
|
||||
LoggedDrops atomic.Uint64 // Counter for dropped logs message already logged
|
||||
|
||||
ActiveLogChannel atomic.Value // stores 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()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Shutdown gracefully closes the logger, attempting to flush pending records
|
||||
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 {
|
||||
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 = 2 * 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 {
|
||||
syncErr := fmtErrorf("failed to sync log file '%s' during shutdown: %w", currentLogFile.Name(), err)
|
||||
finalErr = combineErrors(finalErr, syncErr)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Prevent concurrent flushes overwhelming the processor or channel
|
||||
l.state.flushMutex.Lock()
|
||||
defer l.state.flushMutex.Unlock()
|
||||
|
||||
if !l.state.IsInitialized.Load() || l.state.ShutdownCalled.Load() {
|
||||
return fmtErrorf("logger not initialized or already shut down")
|
||||
}
|
||||
|
||||
// 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(10 * time.Millisecond): // Short timeout to prevent blocking if processor is stuck
|
||||
return fmtErrorf("failed to send flush request to processor (possible deadlock or high load)")
|
||||
}
|
||||
|
||||
// Wait for the processor to signal completion or timeout
|
||||
select {
|
||||
case <-confirmChan:
|
||||
return nil // Flush completed successfully
|
||||
case <-time.After(timeout):
|
||||
return fmtErrorf("timeout waiting for flush confirmation (%v)", timeout)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user