From 162541e53f3a666316b69285857a1e3a6aef0e75c6488dfdad64e0d38a5dbabe Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Mon, 29 Sep 2025 10:53:47 -0400 Subject: [PATCH] e3.2.0 File and console output clarity and uniform configuration, minor cleanup. --- builder.go | 8 ++++---- builder_test.go | 1 + config.go | 30 +++++++++++++++--------------- doc/compatibility-adapters.md | 6 +++--- doc/config-builder.md | 22 +++++++++++----------- doc/configuration.md | 12 ++++++------ doc/getting-started.md | 2 +- doc/logging-guide.md | 2 ++ format.go | 13 ------------- logger.go | 32 ++++++++++++++++---------------- logger_test.go | 8 +++++--- processor.go | 10 +++++----- storage.go | 28 ++++------------------------ 13 files changed, 73 insertions(+), 101 deletions(-) diff --git a/builder.go b/builder.go index f30cc0a..d5df7da 100644 --- a/builder.go +++ b/builder.go @@ -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 diff --git a/builder_test.go b/builder_test.go index fd3a537..fcff443 100644 --- a/builder_test.go +++ b/builder_test.go @@ -21,6 +21,7 @@ func TestBuilder_Build(t *testing.T) { Format("json"). BufferSize(2048). EnableConsole(true). + EnableFile(true). MaxSizeMB(10). HeartbeatLevel(2). Build() diff --git a/config.go b/config.go index 010f487..306c613 100644 --- a/config.go +++ b/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 } diff --git a/doc/compatibility-adapters.md b/doc/compatibility-adapters.md index 3e24f5e..691eb7a 100644 --- a/doc/compatibility-adapters.md +++ b/doc/compatibility-adapters.md @@ -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 ) diff --git a/doc/config-builder.md b/doc/config-builder.md index 519c63e..33ec0cd 100644 --- a/doc/config-builder.md +++ b/doc/config-builder.md @@ -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 diff --git a/doc/configuration.md b/doc/configuration.md index fd18930..f6e41b1 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -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. diff --git a/doc/getting-started.md b/doc/getting-started.md index 9d991e4..80ac4de 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -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() diff --git a/doc/logging-guide.md b/doc/logging-guide.md index 73960de..1eb4598 100644 --- a/doc/logging-guide.md +++ b/doc/logging-guide.md @@ -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( diff --git a/format.go b/format.go index 8677626..1771b28 100644 --- a/format.go +++ b/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: diff --git a/logger.go b/logger.go index e655b71..8318830 100644 --- a/logger.go +++ b/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" { diff --git a/logger_test.go b/logger_test.go index 0724923..c9c4906 100644 --- a/logger_test.go +++ b/logger_test.go @@ -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) diff --git a/processor.go b/processor.go index 28a06ba..8667898 100644 --- a/processor.go +++ b/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 } diff --git a/storage.go b/storage.go index a7df17c..e4cfbe9 100644 --- a/storage.go +++ b/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