diff --git a/README.md b/README.md index cbb35a8..a338985 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ func main() { Important: with the current level ordering, `Level(sneklog.FATAL)` allows `INFO`, `WARN`, `ERROR`, and `FATAL`, but not `DEBUG`. Use `Level(sneklog.DEBUG)` to enable every level. +If you want classic threshold filtering instead, call `SetThresholdMode(true)` and +configure levels with `NewThresholdLogLevel(...)` or +`NewThresholdLogLevelWithColors(...)`. + ## Writers and ownership `Logger.Close()` only closes writers created by the logger itself: @@ -142,6 +146,29 @@ access := sneklog.NewInfoLogLevelWithColors("access", sneklog.FgCyan, sneklog.Bg audit := sneklog.NewWarnLogLevel("audit") ``` +These helpers keep the legacy severity index for backward compatibility and also +assign default threshold values: + +- `INFO`: `th=10` +- `WARN`: `th=20` +- `ERROR`: `th=30` +- `FATAL`: `th=40` +- `DEBUG`: `th=0` + +To define custom threshold-based levels explicitly: + +```go +trace := sneklog.NewThresholdLogLevelWithColors(4, 5, "trace", sneklog.FgHiBlack, sneklog.BgNone) +audit := sneklog.NewThresholdLogLevel(1, 25, "audit") + +logger := sneklog.NewLogger(). + SetLevel(audit). + SetThresholdMode(true) + +logger.Print(trace, "verbose trace") // filtered out +logger.Print(audit, "audit event") // allowed +``` + HTTP method-specific predefined levels are available out of the box: ```go @@ -149,8 +176,11 @@ level := sneklog.LogLevelForMethod(http.MethodPost) logger.Print(level, "POST /users") ``` -When comparing levels, use `SameLevel` if only the severity matters and `Equal` -if the full configuration, including colors and attributes, must match. +When comparing levels: + +- use `SameLevel` if only the legacy severity index matters; +- use `SameThreshold` if only threshold filtering matters; +- use `Equal` if the full configuration, including threshold, colors, and attributes, must match. ## Message replacement diff --git a/README_ru.md b/README_ru.md index a60a6f2..9564816 100644 --- a/README_ru.md +++ b/README_ru.md @@ -71,6 +71,10 @@ func main() { Важно: в текущей модели уровней `Level(sneklog.FATAL)` пропускает `INFO`, `WARN`, `ERROR` и `FATAL`, но не `DEBUG`. Чтобы включить все сообщения, используйте `Level(sneklog.DEBUG)`. +Если нужна классическая threshold-фильтрация, включите `SetThresholdMode(true)` и +задавайте уровни через `NewThresholdLogLevel(...)` или +`NewThresholdLogLevelWithColors(...)`. + ## Writer'ы и владение `Logger.Close()` закрывает только writer'ы, которые логгер создал сам: @@ -142,6 +146,29 @@ access := sneklog.NewInfoLogLevelWithColors("access", sneklog.FgCyan, sneklog.Bg audit := sneklog.NewWarnLogLevel("audit") ``` +Эти helper'ы сохраняют legacy severity index для обратной совместимости и +одновременно выставляют значения threshold по умолчанию: + +- `INFO`: `th=10` +- `WARN`: `th=20` +- `ERROR`: `th=30` +- `FATAL`: `th=40` +- `DEBUG`: `th=0` + +Чтобы явно создавать уровни для threshold-режима: + +```go +trace := sneklog.NewThresholdLogLevelWithColors(4, 5, "trace", sneklog.FgHiBlack, sneklog.BgNone) +audit := sneklog.NewThresholdLogLevel(1, 25, "audit") + +logger := sneklog.NewLogger(). + SetLevel(audit). + SetThresholdMode(true) + +logger.Print(trace, "verbose trace") // будет отфильтровано +logger.Print(audit, "audit event") // будет записано +``` + Для HTTP-методов доступны готовые уровни: ```go @@ -149,9 +176,11 @@ level := sneklog.LogLevelForMethod(http.MethodPost) logger.Print(level, "POST /users") ``` -Если нужно сравнить только severity, используйте `SameLevel`. Если нужно -полное совпадение конфигурации уровня, включая цвета и атрибуты, используйте -`Equal`. +При сравнении уровней: + +- используйте `SameLevel`, если важен только legacy severity index; +- используйте `SameThreshold`, если важна только threshold-фильтрация; +- используйте `Equal`, если нужно полное совпадение конфигурации, включая threshold, цвета и атрибуты. ## Замена сообщений diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e45ff8c..dd063fd 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,8 @@ # Unreleased (planned v2.2.0) -This release expands the public LogLevel API, adds HTTP method helpers, and improves package documentation and test coverage. +This release expands the public LogLevel API, adds optional threshold-based +filtering alongside the legacy severity ordering, and improves documentation +and test coverage. ### Added @@ -15,6 +17,9 @@ This release expands the public LogLevel API, adds HTTP method helpers, and impr - `NewFatalLogLevelWithColors` - `NewDebugLogLevel` - `NewDebugLogLevelWithColors` +- Added threshold-aware log level constructors: + - `NewThresholdLogLevel` + - `NewThresholdLogLevelWithColors` - Added HTTP method-specific predefined log levels: - `HTTPGetLevel` - `HTTPHeadLevel` @@ -28,24 +33,30 @@ This release expands the public LogLevel API, adds HTTP method helpers, and impr - `HTTPUnknownLevel` - Added `LogLevelForMethod(method string) LogLevel` to map HTTP methods to predefined log levels. - Added `Logger.Printf(level, format, args...)` for formatted logging with an explicit log level. -- Added `LogLevel.SameLevel` and `LogLevel.Equal` for severity-only and full level comparisons. +- Added `Logger.SetThresholdMode(bool)` to switch filtering from legacy severity-index ordering to threshold-based ordering. +- Added `LogLevel.SameLevel`, `LogLevel.SameThreshold`, and `LogLevel.Equal` for severity-only, threshold-only, and full level comparisons. ### Changed - Refactored log level definitions into a dedicated `levels.go` file. - Unified `Logger.Print` and `Logger.Println` through a shared internal implementation without changing their behavior. -- Expanded test coverage for `Logger.Printf`, HTTP level mapping, and new level constructors. +- Common helper constructors such as `NewInfoLogLevel` now also assign threshold values while preserving legacy severity indexes. +- `LogLevel.Equal` now includes threshold configuration in full equality checks. +- Expanded test coverage for `Logger.Printf`, HTTP level mapping, constructor behavior, threshold filtering, and backward compatibility. ### Documentation - Improved GoDoc coverage for exported types, constructors, formatters, colors, HTTP helpers, and time layouts. -- Documented level comparison helpers and explicit-level formatted logging. +- Documented threshold mode, threshold-aware constructors, level comparison helpers, and explicit-level formatted logging. - Expanded inline documentation for formatter tokens and default formatter behavior. +- Updated English and Russian README files with threshold-mode examples and comparison guidance. ### Compatibility - No breaking API changes. -- Existing `Print`, `Println`, predefined levels, and color APIs remain compatible. +- Existing `Print`, `Println`, predefined levels, `Level/SetLevel`, and color APIs remain compatible. +- Legacy severity ordering remains the default; threshold filtering is enabled only when `SetThresholdMode(true)` is called. +- `SameLevel` remains severity-only for backward compatibility, even when levels carry threshold metadata. # v2.1.0 diff --git a/doc.go b/doc.go index 31fae98..500a302 100644 --- a/doc.go +++ b/doc.go @@ -47,6 +47,7 @@ // io.Writer values through CreateTextWriter or CreateJsonWriter remain owned by // the caller and are not closed by the logger. // -// LogLevel values can be compared with SameLevel when only severity matters or +// LogLevel values can be compared with SameLevel when only the legacy severity +// index matters, with SameThreshold when only threshold filtering matters, or // with Equal when the full level configuration must match. package sneklog diff --git a/io.go b/io.go index 28f1e87..9f3bdbb 100644 --- a/io.go +++ b/io.go @@ -84,7 +84,9 @@ func (l *Logger) Debugln(m ...any) { } func (l *Logger) print(level LogLevel, newline bool, m ...any) { - if l.level.n < level.n { + if l.threshold && l.level.th < level.th { + return + } else if l.level.n < level.n { return } if len(l.writers) == 0 { diff --git a/levels.go b/levels.go index 52d5fb7..76c7e4f 100644 --- a/levels.go +++ b/levels.go @@ -11,8 +11,9 @@ var ( // LogLevel describes a logging severity. type LogLevel struct { - n uint8 - t string + n uint8 + th uint8 + t string fg FgColor fg256 FgColor256 @@ -26,53 +27,68 @@ type LogLevel struct { } // NewLogLevel creates a log level without predefined colors. +// Deprecated: use NewThresholdLogLevel. This function will be removed in v3. func NewLogLevel(index uint8, name string) LogLevel { return NewLogLevelWithColors(index, name, 0, 0) } // NewLogLevelWithColors creates a log level with ANSI foreground and background colors. +// Deprecated: use NewThresholdLogLevelWithColors. This function will be removed in v3. func NewLogLevelWithColors(index uint8, name string, fg FgColor, bg BgColor) LogLevel { return LogLevel{n: index, t: name, fg: fg, bg: bg, attrs: []Attribute{}} } +// NewThresholdLogLevel creates a log level with both a legacy severity index and +// a threshold value used when Logger.SetThresholdMode(true) is enabled. +func NewThresholdLogLevel(index, th uint8, name string) LogLevel { + return NewThresholdLogLevelWithColors(index, th, name, FgNone, BgNone) +} + +// NewThresholdLogLevelWithColors creates a log level with both a legacy +// severity index and a threshold value, plus ANSI foreground and background +// colors. +func NewThresholdLogLevelWithColors(index, th uint8, name string, fg FgColor, bg BgColor) LogLevel { + return LogLevel{n: index, th: th, t: name, fg: fg, bg: bg, attrs: []Attribute{}} +} + // NewInfoLogLevel creates an info-level log level without predefined colors. -func NewInfoLogLevel(name string) LogLevel { return NewLogLevel(0, name) } +func NewInfoLogLevel(name string) LogLevel { return NewThresholdLogLevel(0, 10, name) } // NewInfoLogLevelWithColors creates an info-level log level with ANSI colors. func NewInfoLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { - return NewLogLevelWithColors(0, name, fg, bg) + return NewThresholdLogLevelWithColors(0, 10, name, fg, bg) } // NewWarnLogLevel creates a warn-level log level without predefined colors. -func NewWarnLogLevel(name string) LogLevel { return NewLogLevel(1, name) } +func NewWarnLogLevel(name string) LogLevel { return NewThresholdLogLevel(1, 20, name) } // NewWarnLogLevelWithColors creates a warn-level log level with ANSI colors. func NewWarnLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { - return NewLogLevelWithColors(1, name, fg, bg) + return NewThresholdLogLevelWithColors(1, 20, name, fg, bg) } // NewErrorLogLevel creates an error-level log level without predefined colors. -func NewErrorLogLevel(name string) LogLevel { return NewLogLevel(2, name) } +func NewErrorLogLevel(name string) LogLevel { return NewThresholdLogLevel(2, 30, name) } // NewErrorLogLevelWithColors creates an error-level log level with ANSI colors. func NewErrorLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { - return NewLogLevelWithColors(2, name, fg, bg) + return NewThresholdLogLevelWithColors(2, 30, name, fg, bg) } // NewFatalLogLevel creates a fatal-level log level without predefined colors. -func NewFatalLogLevel(name string) LogLevel { return NewLogLevel(3, name) } +func NewFatalLogLevel(name string) LogLevel { return NewThresholdLogLevel(3, 40, name) } // NewFatalLogLevelWithColors creates a fatal-level log level with ANSI colors. func NewFatalLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { - return NewLogLevelWithColors(3, name, fg, bg) + return NewThresholdLogLevelWithColors(3, 40, name, fg, bg) } // NewDebugLogLevel creates a debug-level log level without predefined colors. -func NewDebugLogLevel(name string) LogLevel { return NewLogLevel(4, name) } +func NewDebugLogLevel(name string) LogLevel { return NewThresholdLogLevel(4, 0, name) } // NewDebugLogLevelWithColors creates a debug-level log level with ANSI colors. func NewDebugLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { - return NewLogLevelWithColors(4, name, fg, bg) + return NewThresholdLogLevelWithColors(4, 0, name, fg, bg) } // GetName returns the lowercase textual representation of the level. @@ -198,13 +214,21 @@ func (l *LogLevel) GetAttributes() []Attribute { } // SameLevel reports whether two log levels have the same severity index. +// Deprecated: use SameThreshold for threshold-only comparisons or Equal for the +// full level configuration. SameLevel remains severity-only for backward +// compatibility and ignores threshold values. func (l *LogLevel) SameLevel(other LogLevel) bool { return l.n == other.n } +// SameThreshold reports whether two log levels have the same threshold value. +func (l *LogLevel) SameThreshold(other LogLevel) bool { + return l.th == other.th +} + // Equal reports whether two log levels have identical configuration. func (l *LogLevel) Equal(other LogLevel) bool { - if l.t != other.t || l.n != other.n { + if l.t != other.t || l.n != other.n || l.th != other.th { return false } if l.fg != other.fg || l.bg != other.bg { diff --git a/logger.go b/logger.go index 9d232c0..faecf76 100644 --- a/logger.go +++ b/logger.go @@ -18,12 +18,14 @@ type MethodTraceback struct { // Logger routes log records to one or more configured writers. type Logger struct { - prefix string - level LogLevel + prefix string + level LogLevel + writers []LoggerWriter replacers []replacer jsonPretty bool + threshold bool } // CreateLogger creates a logger with default settings. @@ -82,6 +84,13 @@ func (l *Logger) SetJSONPretty(b bool) *Logger { return l } +// SetThresholdMode switches Logger.Print filtering from legacy severity-index +// ordering to LogLevel threshold ordering. +func (l *Logger) SetThresholdMode(b bool) *Logger { + l.threshold = b + return l +} + // AddWriters appends multiple writers to the logger. func (l *Logger) AddWriters(writers ...LoggerWriter) *Logger { l.writers = append(l.writers, writers...) diff --git a/logger_test.go b/logger_test.go index a5de01e..d204efc 100644 --- a/logger_test.go +++ b/logger_test.go @@ -120,6 +120,77 @@ func TestLoggerPrintfFormatsMessage(t *testing.T) { } } +func TestLoggerUsesSeverityOrderingWhenThresholdModeIsDisabled(t *testing.T) { + writer := &stubLoggerWriter{} + logger := CreateLogger(). + SetLevel(NewThresholdLogLevel(1, 0, "warn")). + AddWriter(writer) + + logger.Print(NewThresholdLogLevel(0, 100, "info")) + logger.Print(NewThresholdLogLevel(2, 0, "error")) + + if writer.printCalls != 1 { + t.Fatalf("severity mode should allow only lower-or-equal severity levels, got %d writes", writer.printCalls) + } +} + +func TestLoggerUsesThresholdOrderingWhenThresholdModeIsEnabled(t *testing.T) { + writer := &stubLoggerWriter{} + logger := CreateLogger(). + SetLevel(NewThresholdLogLevel(4, 20, "custom")). + SetThresholdMode(true). + AddWriter(writer) + + logger.Print(NewThresholdLogLevel(4, 10, "debug")) + logger.Print(NewThresholdLogLevel(0, 30, "info")) + + if writer.printCalls != 1 { + t.Fatalf("threshold mode should allow only entries at or below the configured threshold, got %d writes", writer.printCalls) + } +} + +func TestDeprecatedLevelMethodKeepsLegacySeverityBehavior(t *testing.T) { + writer := &stubLoggerWriter{} + logger := CreateLogger(). + Level(FATAL). + AddWriter(writer) + + logger.Print(INFO, "info") + logger.Print(WARN, "warn") + logger.Print(ERROR, "error") + logger.Print(FATAL, "fatal") + logger.Print(DEBUG, "debug") + + if writer.printCalls != 4 { + t.Fatalf("Level(FATAL) should keep legacy behavior and allow INFO/WARN/ERROR/FATAL only, got %d writes", writer.printCalls) + } +} + +func TestSameLevelIgnoresThresholdDifferencesForCompatibility(t *testing.T) { + legacy := NewLogLevel(1, "warn") + threshold := NewThresholdLogLevel(1, 20, "warn") + + if !legacy.SameLevel(threshold) { + t.Fatalf("SameLevel() should continue comparing only severity index") + } + if legacy.Equal(threshold) { + t.Fatalf("Equal() should distinguish levels with different threshold configuration") + } +} + +func TestSameThresholdComparesOnlyThresholdValue(t *testing.T) { + first := NewThresholdLogLevel(1, 20, "warn") + second := NewThresholdLogLevel(9, 20, "custom") + third := NewThresholdLogLevel(1, 30, "warn") + + if !first.SameThreshold(second) { + t.Fatalf("SameThreshold() should ignore severity and compare threshold only") + } + if first.SameThreshold(third) { + t.Fatalf("SameThreshold() should detect different threshold values") + } +} + func TestLogLevelForMethodReturnsExpectedLevels(t *testing.T) { tests := []struct { name string @@ -150,15 +221,16 @@ func TestLogLevelForMethodReturnsExpectedLevels(t *testing.T) { func TestNewLevelConstructorsSetExpectedSeverity(t *testing.T) { tests := []struct { - name string - got LogLevel - want LogLevel + name string + got LogLevel + want LogLevel + wantTh uint8 }{ - {name: "info", got: NewInfoLogLevel("custom"), want: INFO}, - {name: "warn", got: NewWarnLogLevel("custom"), want: WARN}, - {name: "error", got: NewErrorLogLevel("custom"), want: ERROR}, - {name: "fatal", got: NewFatalLogLevel("custom"), want: FATAL}, - {name: "debug", got: NewDebugLogLevel("custom"), want: DEBUG}, + {name: "info", got: NewInfoLogLevel("custom"), want: INFO, wantTh: 10}, + {name: "warn", got: NewWarnLogLevel("custom"), want: WARN, wantTh: 20}, + {name: "error", got: NewErrorLogLevel("custom"), want: ERROR, wantTh: 30}, + {name: "fatal", got: NewFatalLogLevel("custom"), want: FATAL, wantTh: 40}, + {name: "debug", got: NewDebugLogLevel("custom"), want: DEBUG, wantTh: 0}, } for _, tt := range tests { @@ -166,6 +238,9 @@ func TestNewLevelConstructorsSetExpectedSeverity(t *testing.T) { if tt.got.n != tt.want.n { t.Fatalf("%s severity = %d, want %d", tt.name, tt.got.n, tt.want.n) } + if tt.got.th != tt.wantTh { + t.Fatalf("%s threshold = %d, want %d", tt.name, tt.got.th, tt.wantTh) + } if tt.got.GetName() != "custom" { t.Fatalf("%s constructor should preserve name, got %q", tt.name, tt.got.GetName()) }