e3.2.0 File and console output clarity and uniform configuration, minor cleanup.

This commit is contained in:
2025-09-29 10:53:47 -04:00
parent d58b61067f
commit 162541e53f
13 changed files with 73 additions and 101 deletions

View File

@ -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

View File

@ -21,6 +21,7 @@ func TestBuilder_Build(t *testing.T) {
Format("json").
BufferSize(2048).
EnableConsole(true).
EnableFile(true).
MaxSizeMB(10).
HeartbeatLevel(2).
Build()

View File

@ -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
}

View File

@ -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
)

View File

@ -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

View File

@ -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.

View File

@ -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()

View File

@ -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(

View File

@ -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:

View File

@ -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" {

View File

@ -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)

View File

@ -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
}

View File

@ -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