REPOSITORY / ScuroNeko/SNekLog

Compare commits

DIFF REPOSITORY
5 Commits
Author SHA1 Message Date
ScuroNeko dd3c7286d2 (new): JSON HTML escaping control
Golang lint / lint (push) Successful in 45s
- add Formatter.JSONEscapeHTML and SetJSONEscapeHTML to let JSON writers preserve HTML-sensitive characters when needed while keeping escaping enabled by default.
- switch JSON output to json.Encoder, preserve existing Print/Println newline semantics, and cover the new escaping behavior in tests.
2026-04-28 14:36:03 +03:00
ScuroNeko a54a854a77 (fix): RELEASE_NOTES.md
Golang lint / lint (push) Successful in 1m44s
2026-04-28 09:46:18 +03:00
ScuroNeko e6d15b530f (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
2026-04-28 09:45:40 +03:00
ScuroNeko b7fa3f7606 (chore): document and test new LogLevel APIs
Golang lint / lint (push) Successful in 47s
- 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
2026-04-27 17:04:41 +03:00
ScuroNeko 812caed471 (new): add log level helpers and HTTP method levels
Golang lint / lint (push) Successful in 42s
- move LogLevel and predefined levels into levels.go
- add level-specific constructors for info/warn/error/fatal/debug
- add predefined HTTP method log levels and LogLevelForMethod
- unify Logger Print/Println internals and add Printf
- improve GoDoc coverage for exported APIs
2026-04-27 16:36:30 +03:00
14 changed files with 833 additions and 207 deletions
+48
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:
@@ -135,6 +139,49 @@ httpCache.SetForeground256Color(214)
Because `LogLevel` setters mutate the level in place, call them on a variable, not on a temporary value returned by `NewLogLevel(...)`. 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. 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")
```
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
level := sneklog.LogLevelForMethod(http.MethodPost)
logger.Print(level, "POST /users")
```
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 ## Message replacement
`AddReplacer(old, new)` replaces matching text in every message before the `AddReplacer(old, new)` replaces matching text in every message before the
@@ -157,6 +204,7 @@ An empty `old` value is ignored.
- `Info`, `Warn`, `Error`, `Debug`, and `Fatal` accept a list of values. - `Info`, `Warn`, `Error`, `Debug`, and `Fatal` accept a list of values.
- `Infof`, `Warnf`, `Errorf`, `Debugf`, and `Fatalf` use `fmt.Sprintf`. - `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. - 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. - `Fatal`, `Fatalf`, and `Fatalln` call `os.Exit(1)` after writing the message.
- `AddReplacer` masks or rewrites message text before records are sent to writers. - `AddReplacer` masks or rewrites message text before records are sent to writers.
+48
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'ы, которые логгер создал сам:
@@ -135,6 +139,49 @@ httpCache.SetForeground256Color(214)
Так как setter'ы `LogLevel` изменяют уровень на месте, их нужно вызывать на переменной, а не на временном результате `NewLogLevel(...)`. Так как setter'ы `LogLevel` изменяют уровень на месте, их нужно вызывать на переменной, а не на временном результате `NewLogLevel(...)`.
Короткие формы вроде `SetFgColor` и `SetBgColor` сохранены для обратной совместимости. Короткие формы вроде `SetFgColor` и `SetBgColor` сохранены для обратной совместимости.
Для стандартных severity также есть отдельные helper'ы:
```go
access := sneklog.NewInfoLogLevelWithColors("access", sneklog.FgCyan, sneklog.BgNone)
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
level := sneklog.LogLevelForMethod(http.MethodPost)
logger.Print(level, "POST /users")
```
При сравнении уровней:
- используйте `SameLevel`, если важен только legacy severity index;
- используйте `SameThreshold`, если важна только threshold-фильтрация;
- используйте `Equal`, если нужно полное совпадение конфигурации, включая threshold, цвета и атрибуты.
## Замена сообщений ## Замена сообщений
`AddReplacer(old, new)` заменяет найденный текст в каждом сообщении до того, `AddReplacer(old, new)` заменяет найденный текст в каждом сообщении до того,
@@ -156,6 +203,7 @@ logger.Infoln("login token:", "SOME_SECRET")
- `Info`, `Warn`, `Error`, `Debug`, `Fatal` принимают список значений. - `Info`, `Warn`, `Error`, `Debug`, `Fatal` принимают список значений.
- `Infof`, `Warnf`, `Errorf`, `Debugf`, `Fatalf` используют `fmt.Sprintf`. - `Infof`, `Warnf`, `Errorf`, `Debugf`, `Fatalf` используют `fmt.Sprintf`.
- `Printf(level, format, args...)` форматирует сообщение для явного `LogLevel`.
- Методы `*ln` добавляют семантику перевода строки, что удобно для `stdout`, Docker и line-based collectors. - Методы `*ln` добавляют семантику перевода строки, что удобно для `stdout`, Docker и line-based collectors.
- `Fatal`, `Fatalf` и `Fatalln` вызывают `os.Exit(1)` после записи сообщения. - `Fatal`, `Fatalf` и `Fatalln` вызывают `os.Exit(1)` после записи сообщения.
- `AddReplacer` маскирует или переписывает текст сообщений перед отправкой в writer'ы. - `AddReplacer` маскирует или переписывает текст сообщений перед отправкой в writer'ы.
+83
View File
@@ -1,3 +1,86 @@
# v2.3.0
This release adds JSON HTML-escaping control while preserving the existing
JSON writer defaults and newline semantics.
### Added
- Added `Formatter.SetJSONEscapeHTML(bool)` and `Formatter.JSONEscapeHTML` to control whether JSON writers escape HTML-sensitive characters such as `<`, `>`, and `&`.
### Changed
- JSON writers now use `json.Encoder` internally so formatter-level HTML escaping can be configured.
### Fixed
- Preserved `Print` and `Println` newline behavior for JSON output while switching to `json.Encoder`.
### Compatibility
- No breaking API changes.
- HTML escaping remains enabled by default, matching the previous `encoding/json` behavior.
- `Print` still emits JSON without an added newline, and `Println` emits exactly one trailing newline.
# v2.2.0
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 dedicated log level constructors:
- `NewInfoLogLevel`
- `NewInfoLogLevelWithColors`
- `NewWarnLogLevel`
- `NewWarnLogLevelWithColors`
- `NewErrorLogLevel`
- `NewErrorLogLevelWithColors`
- `NewFatalLogLevel`
- `NewFatalLogLevelWithColors`
- `NewDebugLogLevel`
- `NewDebugLogLevelWithColors`
- Added threshold-aware log level constructors:
- `NewThresholdLogLevel`
- `NewThresholdLogLevelWithColors`
- Added HTTP method-specific predefined log levels:
- `HTTPGetLevel`
- `HTTPHeadLevel`
- `HTTPPostLevel`
- `HTTPPutLevel`
- `HTTPPatchLevel`
- `HTTPDeleteLevel`
- `HTTPOptionsLevel`
- `HTTPConnectLevel`
- `HTTPTraceLevel`
- `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 `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.
- 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 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, `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
This release expands sneklog color customization while preserving compatibility with the v2.0.1 API. This release expands sneklog color customization while preserving compatibility with the v2.0.1 API.
+26 -1
View File
@@ -5,12 +5,25 @@ import (
"strings" "strings"
) )
// FgColor is a basic ANSI foreground color code.
type FgColor uint8 type FgColor uint8
// BgColor is a basic ANSI background color code.
type BgColor uint8 type BgColor uint8
// FgColor256 is an ANSI 256-color foreground value.
type FgColor256 uint8 type FgColor256 uint8
// BgColor256 is an ANSI 256-color background value.
type BgColor256 uint8 type BgColor256 uint8
// BgColorRGB is an ANSI truecolor background value encoded as RGB bytes.
type BgColorRGB []uint8 type BgColorRGB []uint8
// FgColorRGB is an ANSI truecolor foreground value encoded as RGB bytes.
type FgColorRGB []uint8 type FgColorRGB []uint8
// Attribute is an ANSI text attribute code.
type Attribute uint8 type Attribute uint8
// NewBgColorRGB builds an RGB background color value. // NewBgColorRGB builds an RGB background color value.
@@ -23,6 +36,7 @@ func NewFgColorRGB(r, g, b uint8) FgColorRGB {
return FgColorRGB{r, g, b} return FgColorRGB{r, g, b}
} }
// Text attribute codes.
const ( const (
Reset Attribute = iota Reset Attribute = iota
Bold Bold
@@ -35,6 +49,8 @@ const (
Conceal Conceal
CrossedOut CrossedOut
) )
// Additional text attribute codes.
const ( const (
DoubleUnderline Attribute = iota + 21 // Can be disable bold on some terminals DoubleUnderline Attribute = iota + 21 // Can be disable bold on some terminals
DisableBold DisableBold
@@ -45,6 +61,8 @@ const (
DisableConceal DisableConceal
DisableCrossedOut DisableCrossedOut
) )
// Basic foreground color codes.
const ( const (
FgNone = 0 FgNone = 0
FgBlack FgColor = iota + 30 FgBlack FgColor = iota + 30
@@ -57,6 +75,8 @@ const (
FgWhite FgWhite
FgDefault FgColor = 39 FgDefault FgColor = 39
) )
// Basic background color codes.
const ( const (
BgNone = 0 BgNone = 0
BgBlack BgColor = iota + 40 BgBlack BgColor = iota + 40
@@ -69,6 +89,8 @@ const (
BgWhite BgWhite
BgDefault BgColor = 49 BgDefault BgColor = 49
) )
// High-intensity foreground color codes.
const ( const (
FgHiBlack FgColor = iota + 90 FgHiBlack FgColor = iota + 90
FgHiRed FgHiRed
@@ -79,6 +101,8 @@ const (
FgHiCyan FgHiCyan
FgHiWhite FgHiWhite
) )
// High-intensity background color codes.
const ( const (
BgHiBlack BgColor = iota + 100 BgHiBlack BgColor = iota + 100
BgHiRed BgHiRed
@@ -90,7 +114,7 @@ const (
BgHiWhite BgHiWhite
) )
// This attributes rarely used and not supported by all terminals. // Rarely used text attribute codes that are not supported by all terminals.
const ( const (
Border Attribute = iota + 51 Border Attribute = iota + 51
Outline Outline
@@ -99,6 +123,7 @@ const (
DisableUpperline DisableUpperline
) )
// ColorStringBuilder incrementally builds ANSI-colored strings.
type ColorStringBuilder struct { type ColorStringBuilder struct {
str string str string
} }
+9
View File
@@ -17,6 +17,11 @@
// passed to writers. Call SetFormatter on a writer to customize timestamps, // passed to writers. Call SetFormatter on a writer to customize timestamps,
// traceback fields, colors, and message layout. // 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: // Basic usage:
// //
// logger := sneklog.CreateLogger(). // logger := sneklog.CreateLogger().
@@ -41,4 +46,8 @@
// the logger and are closed by Logger.Close. Writers created from existing // the logger and are closed by Logger.Close. Writers created from existing
// 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 the legacy severity
// index matters, with SameThreshold when only threshold filtering matters, or
// with Equal when the full level configuration must match.
package sneklog package sneklog
+2
View File
@@ -32,6 +32,7 @@ func main() {
jsonFormatter := sneklog.NewFormatter(). jsonFormatter := sneklog.NewFormatter().
SetFormat("[%L] [%N] %m"). SetFormat("[%L] [%N] %m").
SetColorOutput(false). SetColorOutput(false).
SetJSONEscapeHTML(false).
SetTimeStampFormat(sneklog.Kitchen) SetTimeStampFormat(sneklog.Kitchen)
textStdout.SetFormatter(formatter) textStdout.SetFormatter(formatter)
@@ -62,6 +63,7 @@ func main() {
externalJSON, externalJSON,
) )
logger.Infoln("Тест <>&")
logger.Infoln("service started") logger.Infoln("service started")
logger.Warnln("cache miss") logger.Warnln("cache miss")
logger.Errorln("request failed") logger.Errorln("request failed")
+40 -10
View File
@@ -6,23 +6,36 @@ import (
"time" "time"
) )
// Formatter // Formatter configures how log records are rendered.
// Format: %t - time, %N - name, %l - level, %L - level uppercase, %b - traceback, %B - full traceback, %m - message,
// %M - method, %f - filename, %n - line number, %s - method signature, %p - full path
// Example: "%t [%l] %N: %m (%f:%n %s)"
// //
// TimeStampFormat: %Y - year, %m - month, %d - day, %H - hour, %M - minute, %S - second, // Format tokens:
// %z - timezone(i.e. 0300), %Z - timezone(i.e. 03:00) // - %t: time
// Example: "%d.%m.%Y %H:%M:%S" // - %N: name
// - %l: level
// - %L: uppercase level
// - %b: first traceback frame
// - %B: full traceback
// - %m: message
// - %M: method
// - %f: filename
// - %n: line number
// - %s: method signature
// - %p: full path
// //
// TraceBackFormat: %M - method, %f - filename, %n - line number, %s - method signature, %p - full path // Example format: "%t [%l] %N: %m (%f:%n %s)"
//
// TimeStampFormat uses strftime-like directives such as %Y, %m, %d, %H, %M,
// %S, %z, and %Z.
//
// TraceBackFormat supports the traceback-related tokens %M, %f, %n, %s, and %p.
type Formatter struct { type Formatter struct {
Format string Format string
MessageSeparator string MessageSeparator string
// JSONTraceBackPath // JSONTraceBackPath enables full file paths in formatted traceback fields.
// If true, in traceback will be path to file, otherwise only filename.
JSONTraceBackPath bool JSONTraceBackPath bool
// JSONEscapeHTML controls whether JSON output escapes HTML-sensitive characters.
JSONEscapeHTML bool
TimeStampFormat string TimeStampFormat string
TraceBackFormat string TraceBackFormat string
TraceBackSeparator string TraceBackSeparator string
@@ -31,6 +44,7 @@ type Formatter struct {
ColorOnlyStdout bool ColorOnlyStdout bool
} }
// DefaultTextFormatter is the default formatter used by text writers.
var DefaultTextFormatter = &Formatter{ var DefaultTextFormatter = &Formatter{
Format: "%t %l %N: %m", Format: "%t %l %N: %m",
MessageSeparator: " ", MessageSeparator: " ",
@@ -40,8 +54,11 @@ var DefaultTextFormatter = &Formatter{
ColorOutput: true, ColorOutput: true,
ColorOnlyStdout: true, ColorOnlyStdout: true,
} }
// DefaultJsonFormatter is the default formatter used by JSON writers.
var DefaultJsonFormatter = &Formatter{ var DefaultJsonFormatter = &Formatter{
Format: "%m", Format: "%m",
JSONEscapeHTML: true,
MessageSeparator: " ", MessageSeparator: " ",
TimeStampFormat: RFC3339, TimeStampFormat: RFC3339,
ColorOutput: false, ColorOutput: false,
@@ -52,6 +69,7 @@ func NewFormatter() *Formatter {
return &Formatter{ return &Formatter{
Format: DefaultTextFormatter.Format, Format: DefaultTextFormatter.Format,
MessageSeparator: DefaultTextFormatter.MessageSeparator, MessageSeparator: DefaultTextFormatter.MessageSeparator,
JSONEscapeHTML: true,
TimeStampFormat: DefaultTextFormatter.TimeStampFormat, TimeStampFormat: DefaultTextFormatter.TimeStampFormat,
TraceBackFormat: DefaultTextFormatter.TraceBackFormat, TraceBackFormat: DefaultTextFormatter.TraceBackFormat,
TraceBackSeparator: DefaultTextFormatter.TraceBackSeparator, TraceBackSeparator: DefaultTextFormatter.TraceBackSeparator,
@@ -72,6 +90,18 @@ func (f *Formatter) SetMessageSeparator(separator string) *Formatter {
return f return f
} }
// SetJSONTraceBackPath enables or disables full file paths in formatted traceback fields.
func (f *Formatter) SetJSONTraceBackPath(set bool) *Formatter {
f.JSONTraceBackPath = set
return f
}
// SetJSONEscapeHTML enables or disables HTML escaping in JSON output.
func (f *Formatter) SetJSONEscapeHTML(escape bool) *Formatter {
f.JSONEscapeHTML = escape
return f
}
// SetTimeStampFormat sets the strftime-like format used for timestamps. // SetTimeStampFormat sets the strftime-like format used for timestamps.
func (f *Formatter) SetTimeStampFormat(format string) *Formatter { func (f *Formatter) SetTimeStampFormat(format string) *Formatter {
f.TimeStampFormat = format f.TimeStampFormat = format
+47
View File
@@ -0,0 +1,47 @@
package sneklog
import "net/http"
// HTTP method-specific log levels.
var (
HTTPGetLevel = NewInfoLogLevelWithColors("get", FgGreen, BgNone)
HTTPHeadLevel = NewInfoLogLevelWithColors("head", FgCyan, BgNone)
HTTPPostLevel = NewInfoLogLevelWithColors("post", FgBlue, BgNone)
HTTPPutLevel = NewInfoLogLevelWithColors("put", FgYellow, BgNone)
HTTPPatchLevel = NewInfoLogLevelWithColors("patch", FgMagenta, BgNone)
HTTPDeleteLevel = NewInfoLogLevelWithColors("delete", FgRed, BgNone)
)
// Additional HTTP method-specific log levels.
var (
HTTPOptionsLevel = NewDebugLogLevelWithColors("options", FgWhite, BgNone)
HTTPConnectLevel = NewDebugLogLevelWithColors("connect", FgCyan, BgNone)
HTTPTraceLevel = NewDebugLogLevelWithColors("trace", FgWhite, BgNone)
HTTPUnknownLevel = NewDebugLogLevelWithColors("http", FgWhite, BgNone)
)
// LogLevelForMethod returns the predefined log level associated with an HTTP method.
func LogLevelForMethod(method string) LogLevel {
switch method {
case http.MethodGet:
return HTTPGetLevel
case http.MethodHead:
return HTTPHeadLevel
case http.MethodPost:
return HTTPPostLevel
case http.MethodPut:
return HTTPPutLevel
case http.MethodPatch:
return HTTPPatchLevel
case http.MethodDelete:
return HTTPDeleteLevel
case http.MethodOptions:
return HTTPOptionsLevel
case http.MethodConnect:
return HTTPConnectLevel
case http.MethodTrace:
return HTTPTraceLevel
default:
return HTTPUnknownLevel
}
}
+18 -27
View File
@@ -83,10 +83,10 @@ func (l *Logger) Debugln(m ...any) {
l.Println(DEBUG, m...) l.Println(DEBUG, m...)
} }
// Print write message without trailing "\n" func (l *Logger) print(level LogLevel, newline bool, m ...any) {
// Good for database if l.threshold && l.level.th < level.th {
func (l *Logger) Print(level LogLevel, m ...any) { return
if l.level.n < level.n { } else if l.level.n < level.n {
return return
} }
if len(l.writers) == 0 { if len(l.writers) == 0 {
@@ -94,30 +94,10 @@ func (l *Logger) Print(level LogLevel, m ...any) {
} }
tb := getFullTraceback(1) tb := getFullTraceback(1)
for _, writer := range l.writers { messages := l.replaceAll(m...)
if writer == nil { if newline {
continue messages = append(messages, any("\n"))
} }
err := writer.Print(level, l.prefix, tb, l.replaceAll(m...)...)
if err != nil {
l.reportWriterError(err)
}
}
}
// Println
// Docker requires "\n" at end to write to log.
// print not work for docker, otherwise it will work and write into stdout
func (l *Logger) Println(level LogLevel, m ...any) {
if l.level.n < level.n {
return
}
if len(l.writers) == 0 {
return
}
tb := getFullTraceback(1)
messages := append(append(make([]any, 0, len(m)+1), l.replaceAll(m...)...), "\n")
for _, writer := range l.writers { for _, writer := range l.writers {
if writer == nil { if writer == nil {
continue continue
@@ -129,6 +109,17 @@ func (l *Logger) Println(level LogLevel, m ...any) {
} }
} }
// Print logs a message without appending a trailing newline semantic.
func (l *Logger) Print(level LogLevel, m ...any) { l.print(level, false, m...) }
// Println logs a message and appends a trailing newline semantic for writers that need it.
func (l *Logger) Println(level LogLevel, m ...any) { l.print(level, true, m...) }
// Printf formats according to a format specifier and logs the resulting message.
func (l *Logger) Printf(level LogLevel, format string, args ...any) {
l.print(level, false, fmt.Sprintf(format, args...))
}
// reportWriterError writes internal writer failures directly to stderr to avoid // reportWriterError writes internal writer failures directly to stderr to avoid
// re-entering the same logger path that just failed. // re-entering the same logger path that just failed.
func (l *Logger) reportWriterError(err error) { func (l *Logger) reportWriterError(err error) {
+269
View File
@@ -0,0 +1,269 @@
package sneklog
// Predefined log levels.
var (
INFO = NewInfoLogLevelWithColors("info", FgWhite, BgNone)
WARN = NewWarnLogLevelWithColors("warn", FgHiYellow, BgNone)
ERROR = NewErrorLogLevelWithColors("error", FgHiRed, BgNone)
FATAL = NewFatalLogLevelWithColors("fatal", FgRed, BgNone)
DEBUG = NewDebugLogLevelWithColors("debug", FgGreen, BgNone)
)
// LogLevel describes a logging severity.
type LogLevel struct {
n uint8
th uint8
t string
fg FgColor
fg256 FgColor256
fgRgb FgColorRGB
bg BgColor
bg256 BgColor256
bgRgb BgColorRGB
attrs []Attribute
}
// 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 NewThresholdLogLevel(0, 10, name) }
// NewInfoLogLevelWithColors creates an info-level log level with ANSI colors.
func NewInfoLogLevelWithColors(name string, fg FgColor, bg BgColor) LogLevel {
return NewThresholdLogLevelWithColors(0, 10, name, fg, bg)
}
// NewWarnLogLevel creates a warn-level log level without predefined colors.
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 NewThresholdLogLevelWithColors(1, 20, name, fg, bg)
}
// NewErrorLogLevel creates an error-level log level without predefined colors.
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 NewThresholdLogLevelWithColors(2, 30, name, fg, bg)
}
// NewFatalLogLevel creates a fatal-level log level without predefined colors.
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 NewThresholdLogLevelWithColors(3, 40, name, fg, bg)
}
// NewDebugLogLevel creates a debug-level log level without predefined colors.
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 NewThresholdLogLevelWithColors(4, 0, name, fg, bg)
}
// GetName returns the lowercase textual representation of the level.
func (l *LogLevel) GetName() string { return l.t }
// SetFgColor sets the ANSI foreground color and clears other foreground color modes.
// Deprecated: use SetForegroundColor. This method will be removed in v3.
func (l *LogLevel) SetFgColor(color FgColor) *LogLevel {
return l.SetForegroundColor(color)
}
// GetFgColor returns the ANSI foreground color for the level.
// Deprecated: use GetForegroundColor. This method will be removed in v3.
func (l *LogLevel) GetFgColor() FgColor { return l.GetForegroundColor() }
// SetForegroundColor sets the ANSI foreground color and clears other foreground color modes.
func (l *LogLevel) SetForegroundColor(color FgColor) *LogLevel {
l.fg = color
l.fg256 = 0
l.fgRgb = nil
return l
}
// GetForegroundColor returns the ANSI foreground color for the level.
func (l *LogLevel) GetForegroundColor() FgColor { return l.fg }
// SetForeground256Color sets the 256-color foreground and clears other foreground color modes.
func (l *LogLevel) SetForeground256Color(color FgColor256) *LogLevel {
l.fg = 0
l.fg256 = color
l.fgRgb = nil
return l
}
// GetForeground256Color returns the configured 256-color foreground value.
func (l *LogLevel) GetForeground256Color() FgColor256 { return l.fg256 }
// SetForegroundRGB sets the RGB foreground color and clears other foreground color modes.
func (l *LogLevel) SetForegroundRGB(r, g, b uint8) *LogLevel {
l.fg = 0
l.fg256 = 0
l.fgRgb = FgColorRGB{r, g, b}
return l
}
// GetForegroundRGB returns the configured RGB foreground value.
func (l *LogLevel) GetForegroundRGB() FgColorRGB { return l.fgRgb }
// SetBgColor sets the ANSI background color and clears other background color modes.
// Deprecated: use SetBackgroundColor. This method will be removed in v3.
func (l *LogLevel) SetBgColor(color BgColor) *LogLevel {
return l.SetBackgroundColor(color)
}
// GetBgColor returns the ANSI background color for the level.
// Deprecated: use GetBackgroundColor. This method will be removed in v3.
func (l *LogLevel) GetBgColor() BgColor { return l.GetBackgroundColor() }
// SetBackgroundColor sets the ANSI background color and clears other background color modes.
func (l *LogLevel) SetBackgroundColor(color BgColor) *LogLevel {
l.bg = color
l.bg256 = 0
l.bgRgb = nil
return l
}
// GetBackgroundColor returns the ANSI background color for the level.
func (l *LogLevel) GetBackgroundColor() BgColor { return l.bg }
// SetBackground256Color sets the 256-color background and clears other background color modes.
func (l *LogLevel) SetBackground256Color(color BgColor256) *LogLevel {
l.bg = 0
l.bg256 = color
l.bgRgb = nil
return l
}
// GetBackground256Color returns the configured 256-color background value.
func (l *LogLevel) GetBackground256Color() BgColor256 { return l.bg256 }
// SetBackgroundRGB sets the RGB background color and clears other background color modes.
func (l *LogLevel) SetBackgroundRGB(r, g, b uint8) *LogLevel {
l.bg = 0
l.bg256 = 0
l.bgRgb = BgColorRGB{r, g, b}
return l
}
// GetBackgroundRGB returns the configured RGB background value.
func (l *LogLevel) GetBackgroundRGB() BgColorRGB { return l.bgRgb }
// AddAttribute appends an ANSI text attribute to the level.
func (l *LogLevel) AddAttribute(a Attribute) *LogLevel {
l.attrs = append(l.attrs, a)
return l
}
// RemoveAttribute removes all matching ANSI text attributes from the level.
func (l *LogLevel) RemoveAttribute(a Attribute) *LogLevel {
attrs := make([]Attribute, 0)
for _, attr := range l.attrs {
if attr != a {
attrs = append(attrs, attr)
}
}
l.attrs = attrs
return l
}
// SetAttributes replaces the level attributes with a copy of the provided slice.
func (l *LogLevel) SetAttributes(a []Attribute) *LogLevel {
attrs := make([]Attribute, len(a))
copy(attrs, a)
l.attrs = attrs
return l
}
// GetAttributes returns a copy of the configured ANSI text attributes.
func (l *LogLevel) GetAttributes() []Attribute {
attrs := make([]Attribute, len(l.attrs))
copy(attrs, l.attrs)
return attrs
}
// 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 || l.th != other.th {
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
}
+9 -157
View File
@@ -7,154 +7,6 @@ import (
"strings" "strings"
) )
// LogLevel describes a logging severity.
type LogLevel struct {
n uint8
t string
fg FgColor
fg256 FgColor256
fgRgb FgColorRGB
bg BgColor
bg256 BgColor256
bgRgb BgColorRGB
attrs []Attribute
}
// NewLogLevel creates a log level without predefined colors.
func NewLogLevel(index uint8, name string) LogLevel {
return LogLevel{n: index, t: name, attrs: []Attribute{}}
}
// NewLogLevelWithColors creates a log level with ANSI foreground and background colors.
func NewLogLevelWithColors(index uint8, name string, fg FgColor, bg BgColor) LogLevel {
return LogLevel{n: index, t: name, fg: fg, bg: bg, attrs: []Attribute{}}
}
// GetName returns the lowercase textual representation of the level.
func (l *LogLevel) GetName() string { return l.t }
// SetFgColor sets the ANSI foreground color and clears other foreground color modes.
// Deprecated: use SetForegroundColor. This method will be removed in v3.
func (l *LogLevel) SetFgColor(color FgColor) *LogLevel {
return l.SetForegroundColor(color)
}
// GetFgColor returns the ANSI foreground color for the level.
// Deprecated: use GetForegroundColor. This method will be removed in v3.
func (l *LogLevel) GetFgColor() FgColor { return l.GetForegroundColor() }
// SetForegroundColor sets the ANSI foreground color and clears other foreground color modes.
func (l *LogLevel) SetForegroundColor(color FgColor) *LogLevel {
l.fg = color
l.fg256 = 0
l.fgRgb = nil
return l
}
// GetForegroundColor returns the ANSI foreground color for the level.
func (l *LogLevel) GetForegroundColor() FgColor { return l.fg }
// SetForeground256Color sets the 256-color foreground and clears other foreground color modes.
func (l *LogLevel) SetForeground256Color(color FgColor256) *LogLevel {
l.fg = 0
l.fg256 = color
l.fgRgb = nil
return l
}
// GetForeground256Color returns the configured 256-color foreground value.
func (l *LogLevel) GetForeground256Color() FgColor256 { return l.fg256 }
// SetForegroundRGB sets the RGB foreground color and clears other foreground color modes.
func (l *LogLevel) SetForegroundRGB(r, g, b uint8) *LogLevel {
l.fg = 0
l.fg256 = 0
l.fgRgb = FgColorRGB{r, g, b}
return l
}
// GetForegroundRGB returns the configured RGB foreground value.
func (l *LogLevel) GetForegroundRGB() FgColorRGB { return l.fgRgb }
// SetBgColor sets the ANSI background color and clears other background color modes.
// Deprecated: use SetBackgroundColor. This method will be removed in v3.
func (l *LogLevel) SetBgColor(color BgColor) *LogLevel {
return l.SetBackgroundColor(color)
}
// GetBgColor returns the ANSI background color for the level.
// Deprecated: use GetBackgroundColor. This method will be removed in v3.
func (l *LogLevel) GetBgColor() BgColor { return l.GetBackgroundColor() }
// SetBackgroundColor sets the ANSI background color and clears other background color modes.
func (l *LogLevel) SetBackgroundColor(color BgColor) *LogLevel {
l.bg = color
l.bg256 = 0
l.bgRgb = nil
return l
}
// GetBackgroundColor returns the ANSI background color for the level.
func (l *LogLevel) GetBackgroundColor() BgColor { return l.bg }
// SetBackground256Color sets the 256-color background and clears other background color modes.
func (l *LogLevel) SetBackground256Color(color BgColor256) *LogLevel {
l.bg = 0
l.bg256 = color
l.bgRgb = nil
return l
}
// GetBackground256Color returns the configured 256-color background value.
func (l *LogLevel) GetBackground256Color() BgColor256 { return l.bg256 }
// SetBackgroundRGB sets the RGB background color and clears other background color modes.
func (l *LogLevel) SetBackgroundRGB(r, g, b uint8) *LogLevel {
l.bg = 0
l.bg256 = 0
l.bgRgb = BgColorRGB{r, g, b}
return l
}
// GetBackgroundRGB returns the configured RGB background value.
func (l *LogLevel) GetBackgroundRGB() BgColorRGB { return l.bgRgb }
// AddAttribute appends an ANSI text attribute to the level.
func (l *LogLevel) AddAttribute(a Attribute) *LogLevel {
l.attrs = append(l.attrs, a)
return l
}
// RemoveAttribute removes all matching ANSI text attributes from the level.
func (l *LogLevel) RemoveAttribute(a Attribute) *LogLevel {
attrs := make([]Attribute, 0)
for _, attr := range l.attrs {
if attr != a {
attrs = append(attrs, attr)
}
}
l.attrs = attrs
return l
}
// SetAttributes replaces the level attributes with a copy of the provided slice.
func (l *LogLevel) SetAttributes(a []Attribute) *LogLevel {
attrs := make([]Attribute, len(a))
copy(attrs, a)
l.attrs = attrs
return l
}
// GetAttributes returns a copy of the configured ANSI text attributes.
func (l *LogLevel) GetAttributes() []Attribute {
attrs := make([]Attribute, len(l.attrs))
copy(attrs, l.attrs)
return attrs
}
// MethodTraceback describes a single stack frame attached to a log entry. // MethodTraceback describes a single stack frame attached to a log entry.
type MethodTraceback struct { type MethodTraceback struct {
Method string `json:"method"` Method string `json:"method"`
@@ -164,23 +16,16 @@ type MethodTraceback struct {
FullPath string `json:"fullPath"` FullPath string `json:"fullPath"`
} }
// Predefined log levels.
var (
INFO = NewLogLevelWithColors(0, "info", FgWhite, BgNone)
WARN = NewLogLevelWithColors(1, "warn", FgHiYellow, BgNone)
ERROR = NewLogLevelWithColors(2, "error", FgHiRed, BgNone)
FATAL = NewLogLevelWithColors(3, "fatal", FgRed, BgNone)
DEBUG = NewLogLevelWithColors(4, "debug", FgGreen, BgNone)
)
// 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.
@@ -239,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...)
+214
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"net/http"
"os" "os"
"strings" "strings"
"testing" "testing"
@@ -100,6 +101,180 @@ 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 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
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
wantTh uint8
}{
{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 {
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.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())
}
})
}
}
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) { func TestCreateTextWriterCloseOnNonCloserIsNoOp(t *testing.T) {
writer := CreateTextWriter(&bytes.Buffer{}) writer := CreateTextWriter(&bytes.Buffer{})
if err := writer.Close(); err != nil { if err := writer.Close(); err != nil {
@@ -167,6 +342,42 @@ func TestJsonWriterPrintAllowsEmptyMessages(t *testing.T) {
} }
} }
func TestJsonWriterPrintDoesNotAppendNewline(t *testing.T) {
var buf bytes.Buffer
writer := CreateJsonWriter(&buf, false)
if err := writer.Print(INFO, "TEST", nil, "hello"); err != nil {
t.Fatalf("Print() error = %v", err)
}
if bytes.HasSuffix(buf.Bytes(), []byte("\n")) {
t.Fatalf("Print() should not append newline, got %q", buf.String())
}
}
func TestJsonWriterSetJSONEscapeHTML(t *testing.T) {
var escaped bytes.Buffer
escapedWriter := CreateJsonWriter(&escaped, false)
if err := escapedWriter.Print(INFO, "TEST", nil, "<>&"); err != nil {
t.Fatalf("Print() with default formatter error = %v", err)
}
if strings.Contains(escaped.String(), "<>&") {
t.Fatalf("default JSON output should escape HTML-sensitive characters, got %q", escaped.String())
}
var raw bytes.Buffer
rawWriter := CreateJsonWriter(&raw, false).
SetFormatter(NewFormatter().SetFormat("%m").SetJSONEscapeHTML(false))
if err := rawWriter.Print(INFO, "TEST", nil, "<>&"); err != nil {
t.Fatalf("Print() with SetJSONEscapeHTML(false) error = %v", err)
}
if !strings.Contains(raw.String(), "<>&") {
t.Fatalf("SetJSONEscapeHTML(false) should preserve HTML-sensitive characters, got %q", raw.String())
}
}
func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) { func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
writer := CreateJsonWriter(&buf, false) writer := CreateJsonWriter(&buf, false)
@@ -179,6 +390,9 @@ func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) {
if !bytes.HasSuffix(data, []byte("\n")) { if !bytes.HasSuffix(data, []byte("\n")) {
t.Fatalf("JSON output should end with newline, got %q", data) t.Fatalf("JSON output should end with newline, got %q", data)
} }
if bytes.HasSuffix(bytes.TrimSuffix(data, []byte("\n")), []byte("\n")) {
t.Fatalf("JSON output should end with exactly one newline, got %q", data)
}
var message LoggerJsonMessage var message LoggerJsonMessage
if err := json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &message); err != nil { if err := json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &message); err != nil {
+1
View File
@@ -2,6 +2,7 @@ package sneklog
import "strings" import "strings"
// Predefined strftime-like time layouts.
const ( const (
Layout = "%m/%d %I:%M:%S%p '%y %z" Layout = "%m/%d %I:%M:%S%p '%y %z"
ANSIC = "%a %b %e %H:%M:%S %Y" ANSIC = "%a %b %e %H:%M:%S %Y"
+15 -8
View File
@@ -1,6 +1,7 @@
package sneklog package sneklog
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -145,21 +146,27 @@ func (w *LoggerJsonWriter) Print(level LogLevel, prefix string, traceback []*Met
Message: s, Message: s,
Traceback: traceback, Traceback: traceback,
} }
var data []byte
var err error buf := bytes.NewBuffer(nil)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(f.JSONEscapeHTML)
if w.pretty { if w.pretty {
data, err = json.MarshalIndent(m, "", " ") enc.SetIndent("", " ")
} else {
data, err = json.Marshal(m)
} }
err := enc.Encode(m)
if err != nil { if err != nil {
return err return err
} }
if newline { data := buf.Bytes()
data = append(data, '\n') if !newline {
data = bytes.TrimSuffix(data, []byte("\n"))
} }
_, err = w.Write(data) _, err = w.writer.Write(data)
if err != nil {
return err return err
}
return nil
} }
// Close closes the owned writer, if any. // Close closes the owned writer, if any.