e3.2.0 File and console output clarity and uniform configuration, minor cleanup.
This commit is contained in:
@ -94,9 +94,9 @@ func (b *Builder) MaxSizeMB(size int64) *Builder {
|
||||
return b
|
||||
}
|
||||
|
||||
// DisableFile disables file output entirely.
|
||||
func (b *Builder) DisableFile(disable bool) *Builder {
|
||||
b.cfg.DisableFile = disable
|
||||
// EnableFile enables file output.
|
||||
func (b *Builder) EnableFile(enable bool) *Builder {
|
||||
b.cfg.EnableFile = enable
|
||||
return b
|
||||
}
|
||||
|
||||
@ -220,7 +220,7 @@ func (b *Builder) InternalErrorsToStderr(enable bool) *Builder {
|
||||
return b
|
||||
}
|
||||
|
||||
// EnableConsole enables mirroring logs to console.
|
||||
// EnableConsole enables console output.
|
||||
func (b *Builder) EnableConsole(enable bool) *Builder {
|
||||
b.cfg.EnableConsole = enable
|
||||
return b
|
||||
|
||||
@ -21,6 +21,7 @@ func TestBuilder_Build(t *testing.T) {
|
||||
Format("json").
|
||||
BufferSize(2048).
|
||||
EnableConsole(true).
|
||||
EnableFile(true).
|
||||
MaxSizeMB(10).
|
||||
HeartbeatLevel(2).
|
||||
Build()
|
||||
|
||||
30
config.go
30
config.go
@ -10,6 +10,11 @@ import (
|
||||
|
||||
// Config holds all logger configuration values
|
||||
type Config struct {
|
||||
// File and Console output settings
|
||||
EnableConsole bool `toml:"enable_console"` // Enable console output (stdout/stderr)
|
||||
ConsoleTarget string `toml:"console_target"` // "stdout", "stderr", or "split"
|
||||
EnableFile bool `toml:"enable_file"` // Enable file output
|
||||
|
||||
// Basic settings
|
||||
Level int64 `toml:"level"`
|
||||
Name string `toml:"name"` // Base name for log files
|
||||
@ -45,18 +50,18 @@ type Config struct {
|
||||
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
|
||||
|
||||
// Stdout/console output settings
|
||||
EnableConsole bool `toml:"enable_console"` // Mirror logs to stdout/stderr
|
||||
ConsoleTarget string `toml:"console_target"` // "stdout" or "stderr"
|
||||
DisableFile bool `toml:"disable_file"` // Disable file output entirely
|
||||
|
||||
// Internal error handling
|
||||
InternalErrorsToStderr bool `toml:"internal_errors_to_stderr"` // Write internal errors to stderr
|
||||
}
|
||||
|
||||
// defaultConfig is the single source for all configurable default values
|
||||
var defaultConfig = Config{
|
||||
// Basic settings
|
||||
// Output settings
|
||||
EnableConsole: true,
|
||||
ConsoleTarget: "stdout",
|
||||
EnableFile: true,
|
||||
|
||||
// File settings
|
||||
Level: LevelInfo,
|
||||
Name: "log",
|
||||
Directory: "./log",
|
||||
@ -91,11 +96,6 @@ var defaultConfig = Config{
|
||||
HeartbeatLevel: 0,
|
||||
HeartbeatIntervalS: 60,
|
||||
|
||||
// Stdout settings
|
||||
EnableConsole: false,
|
||||
ConsoleTarget: "stdout",
|
||||
DisableFile: false,
|
||||
|
||||
// Internal error handling
|
||||
InternalErrorsToStderr: false,
|
||||
}
|
||||
@ -324,12 +324,12 @@ func applyConfigField(cfg *Config, key, value string) error {
|
||||
cfg.EnableConsole = boolVal
|
||||
case "console_target":
|
||||
cfg.ConsoleTarget = value
|
||||
case "disable_file":
|
||||
case "enable_file":
|
||||
boolVal, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return fmtErrorf("invalid boolean value for disable_file '%s': %w", value, err)
|
||||
return fmtErrorf("invalid boolean value for enable_file '%s': %w", value, err)
|
||||
}
|
||||
cfg.DisableFile = boolVal
|
||||
cfg.EnableFile = boolVal
|
||||
|
||||
// Internal error handling
|
||||
case "internal_errors_to_stderr":
|
||||
@ -354,7 +354,7 @@ func configRequiresRestart(oldCfg, newCfg *Config) bool {
|
||||
}
|
||||
|
||||
// File output changes require restart
|
||||
if oldCfg.DisableFile != newCfg.DisableFile {
|
||||
if oldCfg.EnableFile != newCfg.EnableFile {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -286,7 +286,7 @@ builder := compat.NewBuilder().
|
||||
"format=txt", // Human-readable
|
||||
"level=-4", // Debug level
|
||||
"trace_depth=3", // Include traces
|
||||
"enable_console=true", // Console output
|
||||
"enable_console=true", // Console output
|
||||
"flush_interval_ms=50", // Quick feedback
|
||||
)
|
||||
```
|
||||
@ -296,8 +296,8 @@ builder := compat.NewBuilder().
|
||||
```go
|
||||
builder := compat.NewBuilder().
|
||||
WithOptions(
|
||||
"disable_file=true", // No files
|
||||
"enable_console=true", // Console only
|
||||
"enable_file=false", // No files
|
||||
"enable_console=true", // Console only
|
||||
"format=json", // For aggregators
|
||||
"level=0", // Info and above
|
||||
)
|
||||
|
||||
@ -20,17 +20,17 @@ All builder methods return `*ConfigBuilder` for chaining. Errors are accumulated
|
||||
|
||||
### Common Methods
|
||||
|
||||
| Method | Parameters | Description |
|
||||
|--------|------------|-------------|
|
||||
| `Level(level int64)` | `level`: Numeric log level | Sets log level (-4 to 8) |
|
||||
| `LevelString(level string)` | `level`: Named level | Sets level by name ("debug", "info", etc.) |
|
||||
| `Directory(dir string)` | `dir`: Path | Sets log directory |
|
||||
| `Format(format string)` | `format`: Output format | Sets format ("txt", "json", "raw") |
|
||||
| `BufferSize(size int64)` | `size`: Buffer size | Sets channel buffer size |
|
||||
| `MaxSizeKB(size int64)` | `size`: Size in MB | Sets max file size |
|
||||
| `EnableConsole(enable bool)` | `enable`: Boolean | Enables console output |
|
||||
| `DisableFile(disable bool)` | `disable`: Boolean | Disables file output |
|
||||
| `HeartbeatLevel(level int64)` | `level`: 0-3 | Sets monitoring level |
|
||||
| Method | Parameters | Description |
|
||||
|-------------------------------|----------------------------|--------------------------------------------|
|
||||
| `Level(level int64)` | `level`: Numeric log level | Sets log level (-4 to 8) |
|
||||
| `LevelString(level string)` | `level`: Named level | Sets level by name ("debug", "info", etc.) |
|
||||
| `Directory(dir string)` | `dir`: Path | Sets log directory |
|
||||
| `Format(format string)` | `format`: Output format | Sets format ("txt", "json", "raw") |
|
||||
| `BufferSize(size int64)` | `size`: Buffer size | Sets channel buffer size |
|
||||
| `MaxSizeKB(size int64)` | `size`: Size in MB | Sets max file size |
|
||||
| `EnableConsole(enable bool)` | `enable`: Boolean | Enables console output |
|
||||
| `EnableFile(enable bool)` | `enable`: Boolean | Enable file output |
|
||||
| `HeartbeatLevel(level int64)` | `level`: 0-3 | Sets monitoring level |
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
@ -56,13 +56,13 @@ logger.Info("info txt log record written to /var/log/myapp.txt")
|
||||
|
||||
### Output Control
|
||||
|
||||
| Parameter | Type | Description | Default |
|
||||
|-----------|------|-------------|---------|
|
||||
| `show_timestamp` | `bool` | Include timestamps in log entries | `true` |
|
||||
| `show_level` | `bool` | Include log level in entries | `true` |
|
||||
| `enable_console` | `bool` | Mirror logs to stdout/stderr | `false` |
|
||||
| Parameter | Type | Description | Default |
|
||||
|------------------|------|------------------------------------------------------|------------|
|
||||
| `show_timestamp` | `bool` | Include timestamps in log entries | `true` |
|
||||
| `show_level` | `bool` | Include log level in entries | `true` |
|
||||
| `enable_console` | `bool` | Enable console output (stdout/stderr) | `true` |
|
||||
| `console_target` | `string` | Console target: `"stdout"`, `"stderr"`, or `"split"` | `"stdout"` |
|
||||
| `disable_file` | `bool` | Disable file output (console-only) | `false` |
|
||||
| `enable_file` | `bool` | Enable file output (console-only) | `true` |
|
||||
|
||||
**Note:** When `console_target="split"`, INFO/DEBUG logs go to stdout while WARN/ERROR logs go to stderr.
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ import (
|
||||
|
||||
func main() {
|
||||
// Create a new logger instance with default configuration
|
||||
// Writes to file ./log/log.log
|
||||
// Writes to both console (stdout) and file ./log/log.log
|
||||
logger := log.NewLogger()
|
||||
defer logger.Shutdown()
|
||||
|
||||
|
||||
@ -135,6 +135,8 @@ Default format for development and debugging:
|
||||
2024-01-15T10:30:45.234567890Z WARN Rate limit approaching user_id=42 requests=95 limit=100
|
||||
```
|
||||
|
||||
Note: The text format does not add quotes around string values containing spaces. This ensures predictability for simple, space-delimited parsing tools. For logs where maintaining the integrity of such values is critical, `json` format is recommended.
|
||||
|
||||
Configuration:
|
||||
```go
|
||||
logger.ApplyConfigString(
|
||||
|
||||
13
format.go
13
format.go
@ -219,19 +219,6 @@ func (s *serializer) writeTextValue(v any) {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
s.buf = append(s.buf, val...)
|
||||
|
||||
// // TODO: Make configurable or remove after analyzing use cases
|
||||
// // json handles string quotes
|
||||
// // txt format behavior may be unexpected with surrounding quotes,
|
||||
// // causing issues with automatic log parsers and complicates regex processing
|
||||
// if len(val) == 0 || strings.ContainsRune(val, ' ') {
|
||||
// s.buf = append(s.buf, '"')
|
||||
// s.writeString(val)
|
||||
// s.buf = append(s.buf, '"')
|
||||
// } else {
|
||||
// s.buf = append(s.buf, val...)
|
||||
// }
|
||||
|
||||
case int:
|
||||
s.buf = strconv.AppendInt(s.buf, int64(val), 10)
|
||||
case int64:
|
||||
|
||||
32
logger.go
32
logger.go
@ -130,16 +130,14 @@ func (l *Logger) Start() error {
|
||||
l.state.ProcessorExited.Store(false)
|
||||
go l.processLogs(logChannel)
|
||||
|
||||
// Log startup if file output enabled
|
||||
if !cfg.DisableFile {
|
||||
startRecord := logRecord{
|
||||
Flags: FlagDefault,
|
||||
TimeStamp: time.Now(),
|
||||
Level: LevelInfo,
|
||||
Args: []any{"Logger started"},
|
||||
}
|
||||
l.sendLogRecord(startRecord)
|
||||
// Log startup
|
||||
startRecord := logRecord{
|
||||
Flags: FlagDefault,
|
||||
TimeStamp: time.Now(),
|
||||
Level: LevelInfo,
|
||||
Args: []any{"Logger started"},
|
||||
}
|
||||
l.sendLogRecord(startRecord)
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -358,11 +356,13 @@ func (l *Logger) applyConfig(cfg *Config) error {
|
||||
|
||||
l.serializer.setTimestampFormat(cfg.TimestampFormat)
|
||||
|
||||
// Ensure log directory exists
|
||||
if err := os.MkdirAll(cfg.Directory, 0755); err != nil {
|
||||
l.state.LoggerDisabled.Store(true)
|
||||
l.currentConfig.Store(oldCfg) // Rollback
|
||||
return fmtErrorf("failed to create log directory '%s': %w", cfg.Directory, err)
|
||||
// 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
|
||||
return fmtErrorf("failed to create log directory '%s': %w", cfg.Directory, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get current state
|
||||
@ -394,7 +394,7 @@ func (l *Logger) applyConfig(cfg *Config) error {
|
||||
oldCfg.Extension != cfg.Extension
|
||||
|
||||
// Handle file state transitions
|
||||
if cfg.DisableFile {
|
||||
if !cfg.EnableFile {
|
||||
// When disabling file output, close the current file
|
||||
if currentFile != nil {
|
||||
// Sync and close the file
|
||||
@ -429,7 +429,7 @@ func (l *Logger) applyConfig(cfg *Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Setup stdout writer based on config
|
||||
// Setup console writer based on config
|
||||
if cfg.EnableConsole {
|
||||
var writer io.Writer
|
||||
if cfg.ConsoleTarget == "stderr" {
|
||||
|
||||
@ -19,6 +19,8 @@ func createTestLogger(t *testing.T) (*Logger, string) {
|
||||
logger := NewLogger()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.EnableConsole = false
|
||||
cfg.EnableFile = true
|
||||
cfg.Directory = tmpDir
|
||||
cfg.BufferSize = 100
|
||||
cfg.FlushIntervalMs = 10
|
||||
@ -90,12 +92,12 @@ func TestApplyConfigString(t *testing.T) {
|
||||
name: "boolean values",
|
||||
configString: []string{
|
||||
"enable_console=true",
|
||||
"disable_file=false",
|
||||
"enable_file=true",
|
||||
"show_timestamp=false",
|
||||
},
|
||||
verify: func(t *testing.T, cfg *Config) {
|
||||
assert.True(t, cfg.EnableConsole)
|
||||
assert.False(t, cfg.DisableFile)
|
||||
assert.True(t, cfg.EnableFile)
|
||||
assert.False(t, cfg.ShowTimestamp)
|
||||
},
|
||||
},
|
||||
@ -266,7 +268,7 @@ func TestLoggerStdoutMirroring(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Directory = t.TempDir()
|
||||
cfg.EnableConsole = true
|
||||
cfg.DisableFile = true
|
||||
cfg.EnableFile = false
|
||||
|
||||
err := logger.ApplyConfig(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
10
processor.go
10
processor.go
@ -18,7 +18,7 @@ func (l *Logger) processLogs(ch <-chan logRecord) {
|
||||
c := l.getConfig()
|
||||
|
||||
// Perform an initial disk check on startup (skip if file output is disabled)
|
||||
if !c.DisableFile {
|
||||
if c.EnableFile {
|
||||
l.performDiskCheck(true)
|
||||
}
|
||||
|
||||
@ -94,8 +94,8 @@ func (l *Logger) processLogs(ch <-chan logRecord) {
|
||||
// processLogRecord handles individual log records, returning bytes written
|
||||
func (l *Logger) processLogRecord(record logRecord) int64 {
|
||||
c := l.getConfig()
|
||||
disableFile := c.DisableFile
|
||||
if !disableFile && !l.state.DiskStatusOK.Load() {
|
||||
enableFile := c.EnableFile
|
||||
if enableFile && !l.state.DiskStatusOK.Load() {
|
||||
// Simple increment of both counters
|
||||
l.state.DroppedLogs.Add(1)
|
||||
l.state.TotalDroppedLogs.Add(1)
|
||||
@ -114,7 +114,7 @@ func (l *Logger) processLogRecord(record logRecord) int64 {
|
||||
)
|
||||
dataLen := int64(len(data))
|
||||
|
||||
// Mirror to stdout if enabled
|
||||
// Write to console if enabled
|
||||
enableConsole := c.EnableConsole
|
||||
if enableConsole {
|
||||
if s := l.state.StdoutWriter.Load(); s != nil {
|
||||
@ -137,7 +137,7 @@ func (l *Logger) processLogRecord(record logRecord) int64 {
|
||||
}
|
||||
|
||||
// Skip file operations if file output is disabled
|
||||
if disableFile {
|
||||
if !enableFile {
|
||||
l.state.TotalLogsProcessed.Add(1)
|
||||
return dataLen // Return data length for adaptive interval calculations
|
||||
}
|
||||
|
||||
28
storage.go
28
storage.go
@ -15,8 +15,8 @@ import (
|
||||
func (l *Logger) performSync() {
|
||||
c := l.getConfig()
|
||||
// Skip sync if file output is disabled
|
||||
disableFile := c.DisableFile
|
||||
if disableFile {
|
||||
enableFile := c.EnableFile
|
||||
if !enableFile {
|
||||
return
|
||||
}
|
||||
|
||||
@ -42,8 +42,8 @@ func (l *Logger) performSync() {
|
||||
func (l *Logger) performDiskCheck(forceCleanup bool) bool {
|
||||
c := l.getConfig()
|
||||
// Skip all disk checks if file output is disabled
|
||||
disableFile := c.DisableFile
|
||||
if disableFile {
|
||||
enableFile := c.EnableFile
|
||||
if !enableFile {
|
||||
// Always return OK status when file output is disabled
|
||||
if !l.state.DiskStatusOK.Load() {
|
||||
l.state.DiskStatusOK.Store(true)
|
||||
@ -130,26 +130,6 @@ func (l *Logger) performDiskCheck(forceCleanup bool) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO: add logic to drain channel if disk gets full
|
||||
// needs logic for wasOK and doc update
|
||||
// if !l.state.DiskStatusOK.Load() && wasOK {
|
||||
// // Drain pending logs to prevent writes
|
||||
// ch := l.getCurrentLogChannel()
|
||||
// drained := 0
|
||||
// drainLoop:
|
||||
// for {
|
||||
// select {
|
||||
// case <-ch:
|
||||
// drained++
|
||||
// default:
|
||||
// break drainLoop
|
||||
// }
|
||||
// }
|
||||
// if drained > 0 {
|
||||
// l.state.DroppedLogs.Add(uint64(drained))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// getDiskFreeSpace retrieves available disk space for the given path
|
||||
|
||||
Reference in New Issue
Block a user