(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.
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
+32 -3
View File
@@ -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, цвета и атрибуты.
## Замена сообщений
+16 -5
View File
@@ -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
+2 -1
View File
@@ -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
+3 -1
View File
@@ -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 {
+35 -11
View File
@@ -12,6 +12,7 @@ var (
// LogLevel describes a logging severity.
type LogLevel struct {
n uint8
th uint8
t string
fg FgColor
@@ -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 {
+9
View File
@@ -20,10 +20,12 @@ type MethodTraceback struct {
type Logger struct {
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...)
+80 -5
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) {
tests := []struct {
name string
@@ -153,12 +224,13 @@ func TestNewLevelConstructorsSetExpectedSeverity(t *testing.T) {
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())
}