(new): add threshold-based log level filtering and docs
Golang lint / lint (push) Successful in 1m36s

- add threshold-aware LogLevel constructors
- add Logger.SetThresholdMode and SameThreshold
- preserve legacy SameLevel behavior for compatibility
- extend tests for threshold mode and compatibility
- update README and release notes for v2.2.0
This commit is contained in:
2026-04-28 09:45:40 +03:00
parent b7fa3f7606
commit e6d15b530f
8 changed files with 216 additions and 35 deletions
+32 -2
View File
@@ -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. 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 ## Writers and ownership
`Logger.Close()` only closes writers created by the logger itself: `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") 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: HTTP method-specific predefined levels are available out of the box:
```go ```go
@@ -149,8 +176,11 @@ level := sneklog.LogLevelForMethod(http.MethodPost)
logger.Print(level, "POST /users") logger.Print(level, "POST /users")
``` ```
When comparing levels, use `SameLevel` if only the severity matters and `Equal` When comparing levels:
if the full configuration, including colors and attributes, must match.
- 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 ## Message replacement
+32 -3
View File
@@ -71,6 +71,10 @@ func main() {
Важно: в текущей модели уровней `Level(sneklog.FATAL)` пропускает `INFO`, `WARN`, `ERROR` и `FATAL`, но не `DEBUG`. Чтобы включить все сообщения, используйте `Level(sneklog.DEBUG)`. Важно: в текущей модели уровней `Level(sneklog.FATAL)` пропускает `INFO`, `WARN`, `ERROR` и `FATAL`, но не `DEBUG`. Чтобы включить все сообщения, используйте `Level(sneklog.DEBUG)`.
Если нужна классическая threshold-фильтрация, включите `SetThresholdMode(true)` и
задавайте уровни через `NewThresholdLogLevel(...)` или
`NewThresholdLogLevelWithColors(...)`.
## Writer'ы и владение ## Writer'ы и владение
`Logger.Close()` закрывает только writer'ы, которые логгер создал сам: `Logger.Close()` закрывает только writer'ы, которые логгер создал сам:
@@ -142,6 +146,29 @@ access := sneklog.NewInfoLogLevelWithColors("access", sneklog.FgCyan, sneklog.Bg
audit := sneklog.NewWarnLogLevel("audit") 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-методов доступны готовые уровни: Для HTTP-методов доступны готовые уровни:
```go ```go
@@ -149,9 +176,11 @@ level := sneklog.LogLevelForMethod(http.MethodPost)
logger.Print(level, "POST /users") logger.Print(level, "POST /users")
``` ```
Если нужно сравнить только severity, используйте `SameLevel`. Если нужно При сравнении уровней:
полное совпадение конфигурации уровня, включая цвета и атрибуты, используйте
`Equal`. - используйте `SameLevel`, если важен только legacy severity index;
- используйте `SameThreshold`, если важна только threshold-фильтрация;
- используйте `Equal`, если нужно полное совпадение конфигурации, включая threshold, цвета и атрибуты.
## Замена сообщений ## Замена сообщений
+16 -5
View File
@@ -1,6 +1,8 @@
# Unreleased (planned v2.2.0) # 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 ### Added
@@ -15,6 +17,9 @@ This release expands the public LogLevel API, adds HTTP method helpers, and impr
- `NewFatalLogLevelWithColors` - `NewFatalLogLevelWithColors`
- `NewDebugLogLevel` - `NewDebugLogLevel`
- `NewDebugLogLevelWithColors` - `NewDebugLogLevelWithColors`
- Added threshold-aware log level constructors:
- `NewThresholdLogLevel`
- `NewThresholdLogLevelWithColors`
- Added HTTP method-specific predefined log levels: - Added HTTP method-specific predefined log levels:
- `HTTPGetLevel` - `HTTPGetLevel`
- `HTTPHeadLevel` - `HTTPHeadLevel`
@@ -28,24 +33,30 @@ This release expands the public LogLevel API, adds HTTP method helpers, and impr
- `HTTPUnknownLevel` - `HTTPUnknownLevel`
- Added `LogLevelForMethod(method string) LogLevel` to map HTTP methods to predefined log levels. - 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 `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 ### Changed
- Refactored log level definitions into a dedicated `levels.go` file. - 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. - 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 ### Documentation
- Improved GoDoc coverage for exported types, constructors, formatters, colors, HTTP helpers, and time layouts. - 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. - Expanded inline documentation for formatter tokens and default formatter behavior.
- Updated English and Russian README files with threshold-mode examples and comparison guidance.
### Compatibility ### Compatibility
- No breaking API changes. - 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 # v2.1.0
+2 -1
View File
@@ -47,6 +47,7 @@
// io.Writer values through CreateTextWriter or CreateJsonWriter remain owned by // io.Writer values through CreateTextWriter or CreateJsonWriter remain owned by
// the caller and are not closed by the logger. // 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. // with Equal when the full level configuration must match.
package sneklog package sneklog
+3 -1
View File
@@ -84,7 +84,9 @@ func (l *Logger) Debugln(m ...any) {
} }
func (l *Logger) print(level LogLevel, newline bool, 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 return
} }
if len(l.writers) == 0 { if len(l.writers) == 0 {
+37 -13
View File
@@ -11,8 +11,9 @@ var (
// LogLevel describes a logging severity. // LogLevel describes a logging severity.
type LogLevel struct { type LogLevel struct {
n uint8 n uint8
t string th uint8
t string
fg FgColor fg FgColor
fg256 FgColor256 fg256 FgColor256
@@ -26,53 +27,68 @@ type LogLevel struct {
} }
// NewLogLevel creates a log level without predefined colors. // 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 { func NewLogLevel(index uint8, name string) LogLevel {
return NewLogLevelWithColors(index, name, 0, 0) return NewLogLevelWithColors(index, name, 0, 0)
} }
// NewLogLevelWithColors creates a log level with ANSI foreground and background colors. // 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 { func NewLogLevelWithColors(index uint8, name string, fg FgColor, bg BgColor) LogLevel {
return LogLevel{n: index, t: name, fg: fg, bg: bg, attrs: []Attribute{}} 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. // 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. // NewInfoLogLevelWithColors creates an info-level log level with ANSI colors.
func NewInfoLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { 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. // 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. // NewWarnLogLevelWithColors creates a warn-level log level with ANSI colors.
func NewWarnLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { 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. // 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. // NewErrorLogLevelWithColors creates an error-level log level with ANSI colors.
func NewErrorLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { 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. // 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. // NewFatalLogLevelWithColors creates a fatal-level log level with ANSI colors.
func NewFatalLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { 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. // 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. // NewDebugLogLevelWithColors creates a debug-level log level with ANSI colors.
func NewDebugLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel { 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. // 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. // 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 { func (l *LogLevel) SameLevel(other LogLevel) bool {
return l.n == other.n 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. // Equal reports whether two log levels have identical configuration.
func (l *LogLevel) Equal(other LogLevel) bool { 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 return false
} }
if l.fg != other.fg || l.bg != other.bg { if l.fg != other.fg || l.bg != other.bg {
+11 -2
View File
@@ -18,12 +18,14 @@ type MethodTraceback struct {
// Logger routes log records to one or more configured writers. // Logger routes log records to one or more configured writers.
type Logger struct { type Logger struct {
prefix string prefix string
level LogLevel level LogLevel
writers []LoggerWriter writers []LoggerWriter
replacers []replacer replacers []replacer
jsonPretty bool jsonPretty bool
threshold bool
} }
// CreateLogger creates a logger with default settings. // CreateLogger creates a logger with default settings.
@@ -82,6 +84,13 @@ func (l *Logger) SetJSONPretty(b bool) *Logger {
return l 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. // AddWriters appends multiple writers to the logger.
func (l *Logger) AddWriters(writers ...LoggerWriter) *Logger { func (l *Logger) AddWriters(writers ...LoggerWriter) *Logger {
l.writers = append(l.writers, writers...) l.writers = append(l.writers, writers...)
+83 -8
View File
@@ -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) { func TestLogLevelForMethodReturnsExpectedLevels(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -150,15 +221,16 @@ func TestLogLevelForMethodReturnsExpectedLevels(t *testing.T) {
func TestNewLevelConstructorsSetExpectedSeverity(t *testing.T) { func TestNewLevelConstructorsSetExpectedSeverity(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
got LogLevel got LogLevel
want LogLevel want LogLevel
wantTh uint8
}{ }{
{name: "info", got: NewInfoLogLevel("custom"), want: INFO}, {name: "info", got: NewInfoLogLevel("custom"), want: INFO, wantTh: 10},
{name: "warn", got: NewWarnLogLevel("custom"), want: WARN}, {name: "warn", got: NewWarnLogLevel("custom"), want: WARN, wantTh: 20},
{name: "error", got: NewErrorLogLevel("custom"), want: ERROR}, {name: "error", got: NewErrorLogLevel("custom"), want: ERROR, wantTh: 30},
{name: "fatal", got: NewFatalLogLevel("custom"), want: FATAL}, {name: "fatal", got: NewFatalLogLevel("custom"), want: FATAL, wantTh: 40},
{name: "debug", got: NewDebugLogLevel("custom"), want: DEBUG}, {name: "debug", got: NewDebugLogLevel("custom"), want: DEBUG, wantTh: 0},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -166,6 +238,9 @@ func TestNewLevelConstructorsSetExpectedSeverity(t *testing.T) {
if tt.got.n != tt.want.n { if tt.got.n != tt.want.n {
t.Fatalf("%s severity = %d, want %d", tt.name, 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" { if tt.got.GetName() != "custom" {
t.Fatalf("%s constructor should preserve name, got %q", tt.name, tt.got.GetName()) t.Fatalf("%s constructor should preserve name, got %q", tt.name, tt.got.GetName())
} }