e1.5.0 Log sink options added.
This commit is contained in:
50
README.md
50
README.md
@ -144,6 +144,9 @@ if err != nil {
|
|||||||
| `max_check_interval_ms` | `int64` | Maximum interval (ms) for adaptive disk checks | `60000` |
|
| `max_check_interval_ms` | `int64` | Maximum interval (ms) for adaptive disk checks | `60000` |
|
||||||
| `heartbeat_level` | `int64` | Heartbeat detail level (0=disabled, 1=proc, 2=proc+disk, 3=proc+disk+sys) | `0` |
|
| `heartbeat_level` | `int64` | Heartbeat detail level (0=disabled, 1=proc, 2=proc+disk, 3=proc+disk+sys) | `0` |
|
||||||
| `heartbeat_interval_s` | `int64` | Interval (s) between heartbeat messages | `60` |
|
| `heartbeat_interval_s` | `int64` | Interval (s) between heartbeat messages | `60` |
|
||||||
|
| `enable_stdout` | `bool` | Mirror all log output to stdout/stderr | `false` |
|
||||||
|
| `stdout_target` | `string` | Target for console output (`"stdout"` or `"stderr"`) | `"stdout"` |
|
||||||
|
| `disable_file` | `bool` | Disable file output entirely (console-only mode) | `false` |
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
@ -210,6 +213,53 @@ Called on an initialized `*Logger` instance.
|
|||||||
the configured `level` filter.
|
the configured `level` filter.
|
||||||
- **`FlagShowTimestamp`, `FlagShowLevel`, `FlagDefault`**: Record flag constants controlling output format.
|
- **`FlagShowTimestamp`, `FlagShowLevel`, `FlagDefault`**: Record flag constants controlling output format.
|
||||||
|
|
||||||
|
### Console Output Configuration
|
||||||
|
|
||||||
|
The logger supports flexible output routing with options to mirror logs to stdout/stderr or disable file output entirely.
|
||||||
|
|
||||||
|
**Console-only logging** (no file output):
|
||||||
|
```go
|
||||||
|
logger := log.NewLogger()
|
||||||
|
err := logger.InitWithDefaults(
|
||||||
|
"enable_stdout=true",
|
||||||
|
"disable_file=true",
|
||||||
|
"level=-4", // Debug level
|
||||||
|
)
|
||||||
|
defer logger.Shutdown()
|
||||||
|
|
||||||
|
logger.Info("This goes only to console")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dual output** (both file and console):
|
||||||
|
```go
|
||||||
|
gologger := log.NewLogger()
|
||||||
|
err := logger.InitWithDefaults(
|
||||||
|
"directory=/var/log/app",
|
||||||
|
"enable_stdout=true",
|
||||||
|
"stdout_target=stderr", // Use stderr to keep stdout clean
|
||||||
|
)
|
||||||
|
defer logger.Shutdown()
|
||||||
|
|
||||||
|
logger.Info("This goes to both file and stderr")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dynamic configuration** (toggle at runtime):
|
||||||
|
```go
|
||||||
|
// Start with file-only logging
|
||||||
|
logger := log.NewLogger()
|
||||||
|
cfg := config.New()
|
||||||
|
cfg.Load("app.toml", os.Args[1:])
|
||||||
|
logger.Init(cfg, "logging")
|
||||||
|
|
||||||
|
// Later, enable console output dynamically
|
||||||
|
cfg.Set("logging.enable_stdout", true)
|
||||||
|
logger.Init(cfg, "logging") // Reconfigure
|
||||||
|
|
||||||
|
// Or disable file output
|
||||||
|
cfg.Set("logging.disable_file", true)
|
||||||
|
logger.Init(cfg, "logging") // Now console-only
|
||||||
|
```
|
||||||
|
|
||||||
## Implementation Details
|
## Implementation Details
|
||||||
|
|
||||||
- **Lock-Free Hot Path:** Logging methods (`l.Info`, `l.Debug`, etc.) operate without locks, using atomic operations to
|
- **Lock-Free Hot Path:** Logging methods (`l.Info`, `l.Debug`, etc.) operate without locks, using atomic operations to
|
||||||
|
|||||||
14
config.go
14
config.go
@ -40,6 +40,11 @@ type Config struct {
|
|||||||
// Heartbeat configuration
|
// Heartbeat configuration
|
||||||
HeartbeatLevel int64 `toml:"heartbeat_level"` // 0=disabled, 1=proc only, 2=proc+disk, 3=proc+disk+sys
|
HeartbeatLevel int64 `toml:"heartbeat_level"` // 0=disabled, 1=proc only, 2=proc+disk, 3=proc+disk+sys
|
||||||
HeartbeatIntervalS int64 `toml:"heartbeat_interval_s"` // Interval seconds for heartbeat
|
HeartbeatIntervalS int64 `toml:"heartbeat_interval_s"` // Interval seconds for heartbeat
|
||||||
|
|
||||||
|
// Stdout/console output settings
|
||||||
|
EnableStdout bool `toml:"enable_stdout"` // Mirror logs to stdout/stderr
|
||||||
|
StdoutTarget string `toml:"stdout_target"` // "stdout" or "stderr"
|
||||||
|
DisableFile bool `toml:"disable_file"` // Disable file output entirely
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultConfig is the single source for all configurable default values
|
// defaultConfig is the single source for all configurable default values
|
||||||
@ -77,6 +82,11 @@ var defaultConfig = Config{
|
|||||||
// Heartbeat settings
|
// Heartbeat settings
|
||||||
HeartbeatLevel: 0,
|
HeartbeatLevel: 0,
|
||||||
HeartbeatIntervalS: 60,
|
HeartbeatIntervalS: 60,
|
||||||
|
|
||||||
|
// Stdout settings
|
||||||
|
EnableStdout: false,
|
||||||
|
StdoutTarget: "stdout",
|
||||||
|
DisableFile: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig returns a copy of the default configuration
|
// DefaultConfig returns a copy of the default configuration
|
||||||
@ -139,5 +149,9 @@ func (c *Config) validate() error {
|
|||||||
if c.HeartbeatLevel > 0 && c.HeartbeatIntervalS <= 0 {
|
if c.HeartbeatLevel > 0 && c.HeartbeatIntervalS <= 0 {
|
||||||
return fmtErrorf("heartbeat_interval_s must be positive when heartbeat is enabled: %d", c.HeartbeatIntervalS)
|
return fmtErrorf("heartbeat_interval_s must be positive when heartbeat is enabled: %d", c.HeartbeatIntervalS)
|
||||||
}
|
}
|
||||||
|
if c.StdoutTarget != "stdout" && c.StdoutTarget != "stderr" {
|
||||||
|
return fmtErrorf("invalid stdout_target: '%s' (use stdout or stderr)", c.StdoutTarget)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
155
example/sink/main.go
Normal file
155
example/sink/main.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// FILE: main.go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lixenwraith/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logDirectory = "./temp_logs"
|
||||||
|
logInterval = 200 * time.Millisecond // Shorter interval for quicker tests
|
||||||
|
)
|
||||||
|
|
||||||
|
// main orchestrates the different test scenarios.
|
||||||
|
func main() {
|
||||||
|
// Ensure a clean state by removing the previous log directory.
|
||||||
|
if err := os.RemoveAll(logDirectory); err != nil {
|
||||||
|
fmt.Printf("Warning: could not remove old log directory: %v\n", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(logDirectory, 0755); err != nil {
|
||||||
|
fmt.Printf("Fatal: could not create log directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("--- Running Logger Test Suite ---")
|
||||||
|
fmt.Printf("! All file-based logs will be in the '%s' directory.\n\n", logDirectory)
|
||||||
|
|
||||||
|
// --- Scenario 1: Test different configurations on fresh logger instances ---
|
||||||
|
fmt.Println("--- SCENARIO 1: Testing configurations in isolation (new logger per test) ---")
|
||||||
|
testFileOnly()
|
||||||
|
testStdoutOnly()
|
||||||
|
testStderrOnly()
|
||||||
|
testNoOutput()
|
||||||
|
|
||||||
|
// --- Scenario 2: Test reconfiguration on a single logger instance ---
|
||||||
|
fmt.Println("\n--- SCENARIO 2: Testing reconfiguration on a single logger instance ---")
|
||||||
|
testReconfigurationTransitions()
|
||||||
|
|
||||||
|
fmt.Println("\n--- Logger Test Suite Complete ---")
|
||||||
|
fmt.Printf("Check the '%s' directory for log files.\n", logDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testFileOnly tests the default behavior: writing only to a file.
|
||||||
|
func testFileOnly() {
|
||||||
|
logger := log.NewLogger()
|
||||||
|
runTestPhase(logger, "1.1: File-Only",
|
||||||
|
"directory="+logDirectory,
|
||||||
|
"name=file_only_log", // Give it a unique name
|
||||||
|
"level=-4",
|
||||||
|
)
|
||||||
|
shutdownLogger(logger, "1.1: File-Only")
|
||||||
|
}
|
||||||
|
|
||||||
|
// testStdoutOnly tests writing only to the standard output.
|
||||||
|
func testStdoutOnly() {
|
||||||
|
logger := log.NewLogger()
|
||||||
|
runTestPhase(logger, "1.2: Stdout-Only",
|
||||||
|
"enable_stdout=true",
|
||||||
|
"disable_file=true", // Explicitly disable file
|
||||||
|
"level=-4",
|
||||||
|
)
|
||||||
|
shutdownLogger(logger, "1.2: Stdout-Only")
|
||||||
|
}
|
||||||
|
|
||||||
|
// testStderrOnly tests writing only to the standard error stream.
|
||||||
|
func testStderrOnly() {
|
||||||
|
fmt.Fprintln(os.Stderr, "\n---") // Separator for stderr output
|
||||||
|
logger := log.NewLogger()
|
||||||
|
runTestPhase(logger, "1.3: Stderr-Only",
|
||||||
|
"enable_stdout=true",
|
||||||
|
"stdout_target=stderr",
|
||||||
|
"disable_file=true",
|
||||||
|
"level=-4",
|
||||||
|
)
|
||||||
|
fmt.Fprintln(os.Stderr, "---") // Separator for stderr output
|
||||||
|
shutdownLogger(logger, "1.3: Stderr-Only")
|
||||||
|
}
|
||||||
|
|
||||||
|
// testNoOutput tests a configuration where all logging is disabled.
|
||||||
|
func testNoOutput() {
|
||||||
|
logger := log.NewLogger()
|
||||||
|
runTestPhase(logger, "1.4: No-Output (logs should be dropped)",
|
||||||
|
"enable_stdout=false", // Ensure stdout is off
|
||||||
|
"disable_file=true", // Ensure file is off
|
||||||
|
"level=-4",
|
||||||
|
)
|
||||||
|
shutdownLogger(logger, "1.4: No-Output")
|
||||||
|
}
|
||||||
|
|
||||||
|
// testReconfigurationTransitions tests the logger's ability to handle state changes.
|
||||||
|
func testReconfigurationTransitions() {
|
||||||
|
logger := log.NewLogger()
|
||||||
|
|
||||||
|
// Phase A: Start with dual output
|
||||||
|
runTestPhase(logger, "2.1: Reconfig - Initial (Dual File+Stdout)",
|
||||||
|
"directory="+logDirectory,
|
||||||
|
"name=reconfig_log",
|
||||||
|
"enable_stdout=true",
|
||||||
|
"disable_file=false",
|
||||||
|
"level=-4",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Phase B: Transition to file-disabled
|
||||||
|
runTestPhase(logger, "2.2: Reconfig - Transition to Stdout-Only",
|
||||||
|
"enable_stdout=true",
|
||||||
|
"disable_file=true", // The key change
|
||||||
|
"level=-4",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Phase C: Transition back to dual-output. This is the critical test.
|
||||||
|
runTestPhase(logger, "2.3: Reconfig - Transition back to Dual (File+Stdout)",
|
||||||
|
"directory="+logDirectory, // Re-specify directory
|
||||||
|
"name=reconfig_log",
|
||||||
|
"enable_stdout=true",
|
||||||
|
"disable_file=false", // Re-enable file
|
||||||
|
"level=-4",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Phase D: Test different levels on the final reconfigured state
|
||||||
|
fmt.Println("\n[Phase 2.4: Reconfig - Testing log levels on final state]")
|
||||||
|
logger.Debug("final-state", "This is a debug message.")
|
||||||
|
logger.Info("final-state", "This is an info message.")
|
||||||
|
logger.Warn("final-state", "This is a warning message.")
|
||||||
|
logger.Error("final-state", "This is an error message.")
|
||||||
|
time.Sleep(logInterval)
|
||||||
|
|
||||||
|
shutdownLogger(logger, "2: Reconfiguration")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runTestPhase is a helper to initialize and run a standard logging test.
|
||||||
|
func runTestPhase(logger *log.Logger, phaseName string, overrides ...string) {
|
||||||
|
fmt.Printf("\n[Phase %s]\n", phaseName)
|
||||||
|
fmt.Println(" Config:", overrides)
|
||||||
|
|
||||||
|
err := logger.InitWithDefaults(overrides...)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" ERROR: Failed to initialize/reconfigure logger: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("event", "start_phase", "name", phaseName)
|
||||||
|
time.Sleep(logInterval)
|
||||||
|
logger.Info("event", "end_phase", "name", phaseName)
|
||||||
|
time.Sleep(logInterval) // Give time for flush
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdownLogger is a helper to gracefully shut down the logger instance.
|
||||||
|
func shutdownLogger(l *log.Logger, phaseName string) {
|
||||||
|
if err := l.Shutdown(500 * time.Millisecond); err != nil {
|
||||||
|
fmt.Printf(" WARNING: Shutdown error in phase '%s': %v\n", phaseName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
4
go.mod
4
go.mod
@ -17,7 +17,7 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.34.0 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
8
go.sum
8
go.sum
@ -30,10 +30,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
|||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
109
logger.go
109
logger.go
@ -4,6 +4,7 @@ package log
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -173,52 +174,48 @@ func (l *Logger) applyAndReconfigureLocked() error {
|
|||||||
return fmtErrorf("failed to create log directory '%s': %w", dir, err)
|
return fmtErrorf("failed to create log directory '%s': %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to restart the processor
|
// Get current state
|
||||||
wasInitialized := l.state.IsInitialized.Load()
|
wasInitialized := l.state.IsInitialized.Load()
|
||||||
processorNeedsRestart := !wasInitialized
|
disableFile, _ := l.config.Bool("log.disable_file")
|
||||||
|
|
||||||
// Always restart the processor if initialized, to handle any config changes
|
// Get current file handle
|
||||||
// This is the simplest approach that works reliably for all config changes
|
currentFilePtr := l.state.CurrentFile.Load()
|
||||||
if wasInitialized {
|
var currentFile *os.File
|
||||||
processorNeedsRestart = true
|
if currentFilePtr != nil {
|
||||||
|
currentFile, _ = currentFilePtr.(*os.File)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart processor if needed
|
// Determine if we need a new file
|
||||||
if processorNeedsRestart {
|
needsNewFile := !wasInitialized || currentFile == nil
|
||||||
// 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
|
// Handle file state transitions
|
||||||
close(oldCh)
|
if disableFile {
|
||||||
|
// When disabling file output, properly close the current file
|
||||||
|
if currentFile != nil {
|
||||||
|
// Sync and close the file
|
||||||
|
_ = currentFile.Sync()
|
||||||
|
if err := currentFile.Close(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "log: warning - failed to close log file during disable: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
l.state.CurrentFile.Store((*os.File)(nil))
|
||||||
// Create the new channel
|
l.state.CurrentSize.Store(0)
|
||||||
bufferSize, _ := l.config.Int64("log.buffer_size")
|
} else if needsNewFile {
|
||||||
newLogChannel := make(chan logRecord, bufferSize)
|
// When enabling file output or initializing, create new file
|
||||||
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()
|
logFile, err := l.createNewLogFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.state.LoggerDisabled.Store(true)
|
l.state.LoggerDisabled.Store(true)
|
||||||
return fmtErrorf("failed to create initial/new log file: %w", err)
|
return fmtErrorf("failed to create log file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close old file if transitioning from one file to another
|
||||||
|
if currentFile != nil && currentFile != logFile {
|
||||||
|
_ = currentFile.Sync()
|
||||||
|
if err := currentFile.Close(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "log: warning - failed to close old log file: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
l.state.CurrentFile.Store(logFile)
|
l.state.CurrentFile.Store(logFile)
|
||||||
l.state.CurrentSize.Store(0)
|
l.state.CurrentSize.Store(0)
|
||||||
if fi, errStat := logFile.Stat(); errStat == nil {
|
if fi, errStat := logFile.Stat(); errStat == nil {
|
||||||
@ -226,6 +223,45 @@ func (l *Logger) applyAndReconfigureLocked() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close the old channel if reconfiguring
|
||||||
|
if wasInitialized {
|
||||||
|
oldCh := l.getCurrentLogChannel()
|
||||||
|
if oldCh != nil {
|
||||||
|
// Create new channel then close old channel
|
||||||
|
bufferSize, _ := l.config.Int64("log.buffer_size")
|
||||||
|
newLogChannel := make(chan logRecord, bufferSize)
|
||||||
|
l.state.ActiveLogChannel.Store(newLogChannel)
|
||||||
|
close(oldCh)
|
||||||
|
|
||||||
|
// Start new processor with new channel
|
||||||
|
l.state.ProcessorExited.Store(false)
|
||||||
|
go l.processLogs(newLogChannel)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Initial startup
|
||||||
|
bufferSize, _ := l.config.Int64("log.buffer_size")
|
||||||
|
newLogChannel := make(chan logRecord, bufferSize)
|
||||||
|
l.state.ActiveLogChannel.Store(newLogChannel)
|
||||||
|
l.state.ProcessorExited.Store(false)
|
||||||
|
go l.processLogs(newLogChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup stdout writer based on config
|
||||||
|
enableStdout, _ := l.config.Bool("log.enable_stdout")
|
||||||
|
if enableStdout {
|
||||||
|
target, _ := l.config.String("log.stdout_target")
|
||||||
|
if target == "stderr" {
|
||||||
|
var writer io.Writer = os.Stderr
|
||||||
|
l.state.StdoutWriter.Store(&sink{w: writer})
|
||||||
|
} else if target == "stdout" {
|
||||||
|
var writer io.Writer = os.Stdout
|
||||||
|
l.state.StdoutWriter.Store(&sink{w: writer})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var writer io.Writer = io.Discard
|
||||||
|
l.state.StdoutWriter.Store(&sink{w: writer})
|
||||||
|
}
|
||||||
|
|
||||||
// Mark as initialized
|
// Mark as initialized
|
||||||
l.state.IsInitialized.Store(true)
|
l.state.IsInitialized.Store(true)
|
||||||
l.state.ShutdownCalled.Store(false)
|
l.state.ShutdownCalled.Store(false)
|
||||||
@ -260,6 +296,9 @@ func (l *Logger) loadCurrentConfig() *Config {
|
|||||||
cfg.EnablePeriodicSync, _ = l.config.Bool("log.enable_periodic_sync")
|
cfg.EnablePeriodicSync, _ = l.config.Bool("log.enable_periodic_sync")
|
||||||
cfg.HeartbeatLevel, _ = l.config.Int64("log.heartbeat_level")
|
cfg.HeartbeatLevel, _ = l.config.Int64("log.heartbeat_level")
|
||||||
cfg.HeartbeatIntervalS, _ = l.config.Int64("log.heartbeat_interval_s")
|
cfg.HeartbeatIntervalS, _ = l.config.Int64("log.heartbeat_interval_s")
|
||||||
|
cfg.EnableStdout, _ = l.config.Bool("log.enable_stdout")
|
||||||
|
cfg.StdoutTarget, _ = l.config.String("log.stdout_target")
|
||||||
|
cfg.DisableFile, _ = l.config.Bool("log.disable_file")
|
||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
76
processor.go
76
processor.go
@ -12,21 +12,24 @@ const (
|
|||||||
// Threshold for triggering reactive disk check
|
// Threshold for triggering reactive disk check
|
||||||
reactiveCheckThresholdBytes int64 = 10 * 1024 * 1024
|
reactiveCheckThresholdBytes int64 = 10 * 1024 * 1024
|
||||||
// Factors to adjust check interval
|
// Factors to adjust check interval
|
||||||
adaptiveIntervalFactor float64 = 1.5 // Slow down factor
|
adaptiveIntervalFactor float64 = 1.5 // Slow down
|
||||||
adaptiveSpeedUpFactor float64 = 0.8 // Speed up factor
|
adaptiveSpeedUpFactor float64 = 0.8 // Speed up
|
||||||
)
|
)
|
||||||
|
|
||||||
// processLogs is the main log processing loop running in a separate goroutine
|
// processLogs is the main log processing loop running in a separate goroutine
|
||||||
func (l *Logger) processLogs(ch <-chan logRecord) {
|
func (l *Logger) processLogs(ch <-chan logRecord) {
|
||||||
l.state.ProcessorExited.Store(false) // Mark processor as running
|
l.state.ProcessorExited.Store(false)
|
||||||
defer l.state.ProcessorExited.Store(true) // Ensure flag is set on exit
|
defer l.state.ProcessorExited.Store(true)
|
||||||
|
|
||||||
// Set up timers and state variables
|
// Set up timers and state variables
|
||||||
timers := l.setupProcessingTimers()
|
timers := l.setupProcessingTimers()
|
||||||
defer l.closeProcessingTimers(timers)
|
defer l.closeProcessingTimers(timers)
|
||||||
|
|
||||||
// Perform an initial disk check on startup
|
// Perform an initial disk check on startup (skip if file output is disabled)
|
||||||
l.performDiskCheck(true) // Force check and update status
|
disableFile, _ := l.config.Bool("log.disable_file")
|
||||||
|
if !disableFile {
|
||||||
|
l.performDiskCheck(true)
|
||||||
|
}
|
||||||
|
|
||||||
// Send initial heartbeats immediately instead of waiting for first tick
|
// Send initial heartbeats immediately instead of waiting for first tick
|
||||||
heartbeatLevel, _ := l.config.Int64("log.heartbeat_level")
|
heartbeatLevel, _ := l.config.Int64("log.heartbeat_level")
|
||||||
@ -65,8 +68,8 @@ func (l *Logger) processLogs(ch <-chan logRecord) {
|
|||||||
|
|
||||||
// Reactive Check Trigger
|
// Reactive Check Trigger
|
||||||
if bytesSinceLastCheck > reactiveCheckThresholdBytes {
|
if bytesSinceLastCheck > reactiveCheckThresholdBytes {
|
||||||
if l.performDiskCheck(false) { // Check without forcing cleanup yet
|
if l.performDiskCheck(false) {
|
||||||
bytesSinceLastCheck = 0 // Reset if check OK
|
bytesSinceLastCheck = 0
|
||||||
logsSinceLastCheck = 0
|
logsSinceLastCheck = 0
|
||||||
lastCheckTime = time.Now()
|
lastCheckTime = time.Now()
|
||||||
}
|
}
|
||||||
@ -78,9 +81,8 @@ func (l *Logger) processLogs(ch <-chan logRecord) {
|
|||||||
|
|
||||||
case <-timers.diskCheckTicker.C:
|
case <-timers.diskCheckTicker.C:
|
||||||
// Periodic disk check
|
// Periodic disk check
|
||||||
if l.performDiskCheck(true) { // Periodic check, force cleanup if needed
|
if l.performDiskCheck(true) {
|
||||||
l.adjustDiskCheckInterval(timers, lastCheckTime, logsSinceLastCheck)
|
l.adjustDiskCheckInterval(timers, lastCheckTime, logsSinceLastCheck)
|
||||||
// Reset counters after successful periodic check
|
|
||||||
bytesSinceLastCheck = 0
|
bytesSinceLastCheck = 0
|
||||||
logsSinceLastCheck = 0
|
logsSinceLastCheck = 0
|
||||||
lastCheckTime = time.Now()
|
lastCheckTime = time.Now()
|
||||||
@ -193,8 +195,6 @@ func (l *Logger) setupHeartbeatTimer(timers *TimerSet) <-chan time.Time {
|
|||||||
if intervalS <= 0 {
|
if intervalS <= 0 {
|
||||||
intervalS = 60 // Default to 60 seconds
|
intervalS = 60 // Default to 60 seconds
|
||||||
}
|
}
|
||||||
// Create a new ticker that's offset slightly to avoid skipping the first tick
|
|
||||||
// by creating it and then waiting until exactly the next interval time
|
|
||||||
timers.heartbeatTicker = time.NewTicker(time.Duration(intervalS) * time.Second)
|
timers.heartbeatTicker = time.NewTicker(time.Duration(intervalS) * time.Second)
|
||||||
return timers.heartbeatTicker.C
|
return timers.heartbeatTicker.C
|
||||||
}
|
}
|
||||||
@ -203,11 +203,14 @@ func (l *Logger) setupHeartbeatTimer(timers *TimerSet) <-chan time.Time {
|
|||||||
|
|
||||||
// processLogRecord handles individual log records, returning bytes written
|
// processLogRecord handles individual log records, returning bytes written
|
||||||
func (l *Logger) processLogRecord(record logRecord) int64 {
|
func (l *Logger) processLogRecord(record logRecord) int64 {
|
||||||
if !l.state.DiskStatusOK.Load() {
|
// Check if the record should process this record
|
||||||
|
disableFile, _ := l.config.Bool("log.disable_file")
|
||||||
|
if !disableFile && !l.state.DiskStatusOK.Load() {
|
||||||
l.state.DroppedLogs.Add(1)
|
l.state.DroppedLogs.Add(1)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serialize the log entry once
|
||||||
format, _ := l.config.String("log.format")
|
format, _ := l.config.String("log.format")
|
||||||
data := l.serializer.serialize(
|
data := l.serializer.serialize(
|
||||||
format,
|
format,
|
||||||
@ -219,6 +222,25 @@ func (l *Logger) processLogRecord(record logRecord) int64 {
|
|||||||
)
|
)
|
||||||
dataLen := int64(len(data))
|
dataLen := int64(len(data))
|
||||||
|
|
||||||
|
// Mirror to stdout if enabled
|
||||||
|
enableStdout, _ := l.config.Bool("log.enable_stdout")
|
||||||
|
if enableStdout {
|
||||||
|
if s := l.state.StdoutWriter.Load(); s != nil {
|
||||||
|
// Assert to concrete type: *sink
|
||||||
|
if sinkWrapper, ok := s.(*sink); ok && sinkWrapper != nil {
|
||||||
|
// Use the wrapped writer (sinkWrapper.w)
|
||||||
|
_, _ = sinkWrapper.w.Write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip file operations if file output is disabled
|
||||||
|
if disableFile {
|
||||||
|
l.state.TotalLogsProcessed.Add(1)
|
||||||
|
return dataLen // Return data length for adaptive interval calculations
|
||||||
|
}
|
||||||
|
|
||||||
|
// File rotation check
|
||||||
currentFileSize := l.state.CurrentSize.Load()
|
currentFileSize := l.state.CurrentSize.Load()
|
||||||
estimatedSize := currentFileSize + dataLen
|
estimatedSize := currentFileSize + dataLen
|
||||||
|
|
||||||
@ -229,6 +251,7 @@ func (l *Logger) processLogRecord(record logRecord) int64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
cfPtr := l.state.CurrentFile.Load()
|
cfPtr := l.state.CurrentFile.Load()
|
||||||
if currentLogFile, isFile := cfPtr.(*os.File); isFile && currentLogFile != nil {
|
if currentLogFile, isFile := cfPtr.(*os.File); isFile && currentLogFile != nil {
|
||||||
n, err := currentLogFile.Write(data)
|
n, err := currentLogFile.Write(data)
|
||||||
@ -349,7 +372,7 @@ func (l *Logger) handleHeartbeat() {
|
|||||||
func (l *Logger) logProcHeartbeat() {
|
func (l *Logger) logProcHeartbeat() {
|
||||||
processed := l.state.TotalLogsProcessed.Load()
|
processed := l.state.TotalLogsProcessed.Load()
|
||||||
dropped := l.state.DroppedLogs.Load()
|
dropped := l.state.DroppedLogs.Load()
|
||||||
sequence := l.state.HeartbeatSequence.Add(1) // Increment and get sequence number
|
sequence := l.state.HeartbeatSequence.Add(1)
|
||||||
|
|
||||||
startTimeVal := l.state.LoggerStartTime.Load()
|
startTimeVal := l.state.LoggerStartTime.Load()
|
||||||
var uptimeHours float64 = 0
|
var uptimeHours float64 = 0
|
||||||
@ -436,19 +459,34 @@ func (l *Logger) logSysHeartbeat() {
|
|||||||
l.writeHeartbeatRecord(LevelSys, sysArgs)
|
l.writeHeartbeatRecord(LevelSys, sysArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeHeartbeatRecord handles the common logic for writing a heartbeat record
|
// writeHeartbeatRecord handles common logic for writing a heartbeat record
|
||||||
func (l *Logger) writeHeartbeatRecord(level int64, args []any) {
|
func (l *Logger) writeHeartbeatRecord(level int64, args []any) {
|
||||||
if l.state.LoggerDisabled.Load() || l.state.ShutdownCalled.Load() {
|
if l.state.LoggerDisabled.Load() || l.state.ShutdownCalled.Load() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !l.state.DiskStatusOK.Load() {
|
// Serialize heartbeat data
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
format, _ := l.config.String("log.format")
|
format, _ := l.config.String("log.format")
|
||||||
hbData := l.serializer.serialize(format, FlagDefault|FlagShowLevel, time.Now(), level, "", args)
|
hbData := l.serializer.serialize(format, FlagDefault|FlagShowLevel, time.Now(), level, "", args)
|
||||||
|
|
||||||
|
// Mirror to stdout if enabled
|
||||||
|
enableStdout, _ := l.config.Bool("log.enable_stdout")
|
||||||
|
if enableStdout {
|
||||||
|
if s := l.state.StdoutWriter.Load(); s != nil {
|
||||||
|
// Assert to concrete type: *sink
|
||||||
|
if sinkWrapper, ok := s.(*sink); ok && sinkWrapper != nil {
|
||||||
|
// Use the wrapped writer (sinkWrapper.w)
|
||||||
|
_, _ = sinkWrapper.w.Write(hbData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disableFile, _ := l.config.Bool("log.disable_file")
|
||||||
|
if disableFile || !l.state.DiskStatusOK.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
cfPtr := l.state.CurrentFile.Load()
|
cfPtr := l.state.CurrentFile.Load()
|
||||||
if cfPtr == nil {
|
if cfPtr == nil {
|
||||||
fmtFprintf(os.Stderr, "log: error - current file handle is nil during heartbeat\n")
|
fmtFprintf(os.Stderr, "log: error - current file handle is nil during heartbeat\n")
|
||||||
|
|||||||
7
state.go
7
state.go
@ -2,6 +2,7 @@
|
|||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -31,6 +32,7 @@ type State struct {
|
|||||||
LoggedDrops atomic.Uint64 // Counter for dropped logs message already logged
|
LoggedDrops atomic.Uint64 // Counter for dropped logs message already logged
|
||||||
|
|
||||||
ActiveLogChannel atomic.Value // stores chan logRecord
|
ActiveLogChannel atomic.Value // stores chan logRecord
|
||||||
|
StdoutWriter atomic.Value // stores io.Writer (os.Stdout, os.Stderr, or io.Discard)
|
||||||
|
|
||||||
// Heartbeat statistics
|
// Heartbeat statistics
|
||||||
HeartbeatSequence atomic.Uint64 // Counter for heartbeat sequence numbers
|
HeartbeatSequence atomic.Uint64 // Counter for heartbeat sequence numbers
|
||||||
@ -40,6 +42,11 @@ type State struct {
|
|||||||
TotalDeletions atomic.Uint64 // Counter for successful log deletions (cleanup/retention)
|
TotalDeletions atomic.Uint64 // Counter for successful log deletions (cleanup/retention)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sink is a wrapper around an io.Writer, atomic value type change workaround
|
||||||
|
type sink struct {
|
||||||
|
w io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
// Init initializes or reconfigures the logger using the provided config.Config instance
|
// Init initializes or reconfigures the logger using the provided config.Config instance
|
||||||
func (l *Logger) Init(cfg *config.Config, basePath string) error {
|
func (l *Logger) Init(cfg *config.Config, basePath string) error {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
|
|||||||
17
storage.go
17
storage.go
@ -13,6 +13,12 @@ import (
|
|||||||
|
|
||||||
// performSync syncs the current log file
|
// performSync syncs the current log file
|
||||||
func (l *Logger) performSync() {
|
func (l *Logger) performSync() {
|
||||||
|
// Skip sync if file output is disabled
|
||||||
|
disableFile, _ := l.config.Bool("log.disable_file")
|
||||||
|
if disableFile {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cfPtr := l.state.CurrentFile.Load()
|
cfPtr := l.state.CurrentFile.Load()
|
||||||
if cfPtr != nil {
|
if cfPtr != nil {
|
||||||
if currentLogFile, isFile := cfPtr.(*os.File); isFile && currentLogFile != nil {
|
if currentLogFile, isFile := cfPtr.(*os.File); isFile && currentLogFile != nil {
|
||||||
@ -33,6 +39,17 @@ func (l *Logger) performSync() {
|
|||||||
// performDiskCheck checks disk space, triggers cleanup if needed, and updates status
|
// performDiskCheck checks disk space, triggers cleanup if needed, and updates status
|
||||||
// Returns true if disk is OK, false otherwise
|
// Returns true if disk is OK, false otherwise
|
||||||
func (l *Logger) performDiskCheck(forceCleanup bool) bool {
|
func (l *Logger) performDiskCheck(forceCleanup bool) bool {
|
||||||
|
// Skip all disk checks if file output is disabled
|
||||||
|
disableFile, _ := l.config.Bool("log.disable_file")
|
||||||
|
if disableFile {
|
||||||
|
// Always return OK status when file output is disabled
|
||||||
|
if !l.state.DiskStatusOK.Load() {
|
||||||
|
l.state.DiskStatusOK.Store(true)
|
||||||
|
l.state.DiskFullLogged.Store(false)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
dir, _ := l.config.String("log.directory")
|
dir, _ := l.config.String("log.directory")
|
||||||
ext, _ := l.config.String("log.extension")
|
ext, _ := l.config.String("log.extension")
|
||||||
maxTotalMB, _ := l.config.Int64("log.max_total_size_mb")
|
maxTotalMB, _ := l.config.Int64("log.max_total_size_mb")
|
||||||
|
|||||||
Reference in New Issue
Block a user