From b7fa3f7606e3f29e998ede9f163cfbf6f13c798a Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Mon, 27 Apr 2026 17:04:41 +0300 Subject: [PATCH] (chore): document and test new LogLevel APIs - add godoc for SameLevel and Equal - update package docs and READMEs with new helpers - refresh unreleased release notes - add tests for Printf, HTTP method levels, and level constructors --- README.md | 18 +++++++++ README_ru.md | 19 +++++++++ RELEASE_NOTES.md | 7 +++- doc.go | 8 ++++ levels.go | 47 ++++++++++++++++++++++ logger_test.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 197 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 912db29..cbb35a8 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,23 @@ httpCache.SetForeground256Color(214) Because `LogLevel` setters mutate the level in place, call them on a variable, not on a temporary value returned by `NewLogLevel(...)`. Short forms such as `SetFgColor` and `SetBgColor` remain available for backward compatibility. +Common severities also have dedicated helpers: + +```go +access := sneklog.NewInfoLogLevelWithColors("access", sneklog.FgCyan, sneklog.BgNone) +audit := sneklog.NewWarnLogLevel("audit") +``` + +HTTP method-specific predefined levels are available out of the box: + +```go +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. + ## Message replacement `AddReplacer(old, new)` replaces matching text in every message before the @@ -157,6 +174,7 @@ An empty `old` value is ignored. - `Info`, `Warn`, `Error`, `Debug`, and `Fatal` accept a list of values. - `Infof`, `Warnf`, `Errorf`, `Debugf`, and `Fatalf` use `fmt.Sprintf`. +- `Printf(level, format, args...)` formats a message for an explicit `LogLevel`. - The `*ln` methods preserve newline semantics, which is useful for `stdout`, Docker, and line-based collectors. - `Fatal`, `Fatalf`, and `Fatalln` call `os.Exit(1)` after writing the message. - `AddReplacer` masks or rewrites message text before records are sent to writers. diff --git a/README_ru.md b/README_ru.md index e7cec6e..a60a6f2 100644 --- a/README_ru.md +++ b/README_ru.md @@ -135,6 +135,24 @@ httpCache.SetForeground256Color(214) Так как setter'ы `LogLevel` изменяют уровень на месте, их нужно вызывать на переменной, а не на временном результате `NewLogLevel(...)`. Короткие формы вроде `SetFgColor` и `SetBgColor` сохранены для обратной совместимости. +Для стандартных severity также есть отдельные helper'ы: + +```go +access := sneklog.NewInfoLogLevelWithColors("access", sneklog.FgCyan, sneklog.BgNone) +audit := sneklog.NewWarnLogLevel("audit") +``` + +Для HTTP-методов доступны готовые уровни: + +```go +level := sneklog.LogLevelForMethod(http.MethodPost) +logger.Print(level, "POST /users") +``` + +Если нужно сравнить только severity, используйте `SameLevel`. Если нужно +полное совпадение конфигурации уровня, включая цвета и атрибуты, используйте +`Equal`. + ## Замена сообщений `AddReplacer(old, new)` заменяет найденный текст в каждом сообщении до того, @@ -156,6 +174,7 @@ logger.Infoln("login token:", "SOME_SECRET") - `Info`, `Warn`, `Error`, `Debug`, `Fatal` принимают список значений. - `Infof`, `Warnf`, `Errorf`, `Debugf`, `Fatalf` используют `fmt.Sprintf`. +- `Printf(level, format, args...)` форматирует сообщение для явного `LogLevel`. - Методы `*ln` добавляют семантику перевода строки, что удобно для `stdout`, Docker и line-based collectors. - `Fatal`, `Fatalf` и `Fatalln` вызывают `os.Exit(1)` после записи сообщения. - `AddReplacer` маскирует или переписывает текст сообщений перед отправкой в writer'ы. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 62ba7c3..e45ff8c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,6 @@ -# v2.2.0 +# Unreleased (planned v2.2.0) -This release adds new public APIs for working with log levels, introduces HTTP method-specific log levels, and improves package documentation. +This release expands the public LogLevel API, adds HTTP method helpers, and improves package documentation and test coverage. ### Added @@ -28,15 +28,18 @@ This release adds new public APIs for working with log levels, introduces HTTP m - `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. ### 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. ### Documentation - Improved GoDoc coverage for exported types, constructors, formatters, colors, HTTP helpers, and time layouts. +- Documented level comparison helpers and explicit-level formatted logging. - Expanded inline documentation for formatter tokens and default formatter behavior. ### Compatibility diff --git a/doc.go b/doc.go index 7657c17..31fae98 100644 --- a/doc.go +++ b/doc.go @@ -17,6 +17,11 @@ // passed to writers. Call SetFormatter on a writer to customize timestamps, // traceback fields, colors, and message layout. // +// The package also provides helper constructors for common severities such as +// NewInfoLogLevel and NewErrorLogLevel, plus predefined HTTP method-specific +// levels such as HTTPGetLevel and HTTPPostLevel. Use LogLevelForMethod to map +// an HTTP method to one of those predefined levels. +// // Basic usage: // // logger := sneklog.CreateLogger(). @@ -41,4 +46,7 @@ // the logger and are closed by Logger.Close. Writers created from existing // 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 +// with Equal when the full level configuration must match. package sneklog diff --git a/levels.go b/levels.go index e80a430..52d5fb7 100644 --- a/levels.go +++ b/levels.go @@ -196,3 +196,50 @@ func (l *LogLevel) GetAttributes() []Attribute { copy(attrs, l.attrs) return attrs } + +// SameLevel reports whether two log levels have the same severity index. +func (l *LogLevel) SameLevel(other LogLevel) bool { + return l.n == other.n +} + +// 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 { + return false + } + if l.fg != other.fg || l.bg != other.bg { + return false + } + + if l.fg256 != other.fg256 || l.bg256 != other.bg256 { + return false + } + + if len(l.attrs) != len(other.attrs) { + return false + } + for i := range l.attrs { + if l.attrs[i] != other.attrs[i] { + return false + } + } + + if len(l.fgRgb) != len(other.fgRgb) { + return false + } + for i := range l.fgRgb { + if l.fgRgb[i] != other.fgRgb[i] { + return false + } + } + + if len(l.bgRgb) != len(other.bgRgb) { + return false + } + for i := range l.bgRgb { + if l.bgRgb[i] != other.bgRgb[i] { + return false + } + } + return true +} diff --git a/logger_test.go b/logger_test.go index 41d7833..a5de01e 100644 --- a/logger_test.go +++ b/logger_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "net/http" "os" "strings" "testing" @@ -100,6 +101,105 @@ func TestLoggerPreservesMessageTypesWithoutReplacers(t *testing.T) { } } +func TestLoggerPrintfFormatsMessage(t *testing.T) { + writer := &stubLoggerWriter{} + logger := CreateLogger(). + SetLevel(DEBUG). + AddWriter(writer) + + logger.Printf(INFO, "status=%d %s", 200, "ok") + + if writer.printCalls != 1 { + t.Fatalf("Printf() should write once, got %d calls", writer.printCalls) + } + if len(writer.messages) != 1 { + t.Fatalf("Printf() should produce one formatted message, got %d parts", len(writer.messages)) + } + if got, ok := writer.messages[0].(string); !ok || got != "status=200 ok" { + t.Fatalf("Printf() should format message, got %#v", writer.messages) + } +} + +func TestLogLevelForMethodReturnsExpectedLevels(t *testing.T) { + tests := []struct { + name string + method string + want LogLevel + }{ + {name: "get", method: http.MethodGet, want: HTTPGetLevel}, + {name: "head", method: http.MethodHead, want: HTTPHeadLevel}, + {name: "post", method: http.MethodPost, want: HTTPPostLevel}, + {name: "put", method: http.MethodPut, want: HTTPPutLevel}, + {name: "patch", method: http.MethodPatch, want: HTTPPatchLevel}, + {name: "delete", method: http.MethodDelete, want: HTTPDeleteLevel}, + {name: "options", method: http.MethodOptions, want: HTTPOptionsLevel}, + {name: "connect", method: http.MethodConnect, want: HTTPConnectLevel}, + {name: "trace", method: http.MethodTrace, want: HTTPTraceLevel}, + {name: "unknown", method: "PROPFIND", want: HTTPUnknownLevel}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := LogLevelForMethod(tt.method) + if got.n != tt.want.n || got.t != tt.want.t || got.fg != tt.want.fg || got.bg != tt.want.bg { + t.Fatalf("LogLevelForMethod(%q) = %#v, want %#v", tt.method, got, tt.want) + } + }) + } +} + +func TestNewLevelConstructorsSetExpectedSeverity(t *testing.T) { + tests := []struct { + name string + got LogLevel + want LogLevel + }{ + {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}, + } + + for _, tt := range tests { + t.Run(tt.name, func(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.GetName() != "custom" { + t.Fatalf("%s constructor should preserve name, got %q", tt.name, tt.got.GetName()) + } + }) + } +} + +func TestNewLevelConstructorsWithColorsSetExpectedFields(t *testing.T) { + info := NewInfoLogLevelWithColors("info-custom", FgBlue, BgYellow) + if info.n != INFO.n || info.GetName() != "info-custom" || info.fg != FgBlue || info.bg != BgYellow { + t.Fatalf("NewInfoLogLevelWithColors() = %#v", info) + } + + warn := NewWarnLogLevelWithColors("warn-custom", FgMagenta, BgCyan) + if warn.n != WARN.n || warn.GetName() != "warn-custom" || warn.fg != FgMagenta || warn.bg != BgCyan { + t.Fatalf("NewWarnLogLevelWithColors() = %#v", warn) + } + + errLevel := NewErrorLogLevelWithColors("error-custom", FgRed, BgWhite) + if errLevel.n != ERROR.n || errLevel.GetName() != "error-custom" || errLevel.fg != FgRed || errLevel.bg != BgWhite { + t.Fatalf("NewErrorLogLevelWithColors() = %#v", errLevel) + } + + fatal := NewFatalLogLevelWithColors("fatal-custom", FgHiRed, BgBlack) + if fatal.n != FATAL.n || fatal.GetName() != "fatal-custom" || fatal.fg != FgHiRed || fatal.bg != BgBlack { + t.Fatalf("NewFatalLogLevelWithColors() = %#v", fatal) + } + + debug := NewDebugLogLevelWithColors("debug-custom", FgGreen, BgDefault) + if debug.n != DEBUG.n || debug.GetName() != "debug-custom" || debug.fg != FgGreen || debug.bg != BgDefault { + t.Fatalf("NewDebugLogLevelWithColors() = %#v", debug) + } +} + func TestCreateTextWriterCloseOnNonCloserIsNoOp(t *testing.T) { writer := CreateTextWriter(&bytes.Buffer{}) if err := writer.Close(); err != nil {