REPOSITORY / ScuroNeko/SNekLog
Compare commits
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14b787710c
|
||
|
|
9268e222ab
|
@@ -11,6 +11,7 @@ Russian version: [README_ru.md](README_ru.md)
|
|||||||
- `stdout`, files, and arbitrary external `io.Writer` values.
|
- `stdout`, files, and arbitrary external `io.Writer` values.
|
||||||
- Optional timestamps for text output.
|
- Optional timestamps for text output.
|
||||||
- Compact traceback metadata for text writers and full traceback slices for JSON.
|
- Compact traceback metadata for text writers and full traceback slices for JSON.
|
||||||
|
- Message replacement for masking secrets or normalizing output.
|
||||||
- Explicit ownership rules for writer closing.
|
- Explicit ownership rules for writer closing.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -35,7 +36,8 @@ func main() {
|
|||||||
Prefix("API").
|
Prefix("API").
|
||||||
Level(slog.DEBUG).
|
Level(slog.DEBUG).
|
||||||
PrintTraceback(true).
|
PrintTraceback(true).
|
||||||
JsonPretty(true)
|
JsonPretty(true).
|
||||||
|
AddReplacer("SOME_SECRET", "<redacted>")
|
||||||
|
|
||||||
text := logger.CreateTextStdoutWriter()
|
text := logger.CreateTextStdoutWriter()
|
||||||
jsonFile, err := logger.CreateJsonFileWriter("logs/app.json")
|
jsonFile, err := logger.CreateJsonFileWriter("logs/app.json")
|
||||||
@@ -49,6 +51,7 @@ func main() {
|
|||||||
logger.Warnln("cache miss")
|
logger.Warnln("cache miss")
|
||||||
logger.Errorln("request failed")
|
logger.Errorln("request failed")
|
||||||
logger.Debugln("debug details")
|
logger.Debugln("debug details")
|
||||||
|
logger.Infoln("token", "SOME_SECRET")
|
||||||
|
|
||||||
if err := logger.Close(); err != nil {
|
if err := logger.Close(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -116,12 +119,31 @@ JSON writers emit objects with this shape:
|
|||||||
|
|
||||||
When `JsonPretty(true)` is enabled, JSON is indented.
|
When `JsonPretty(true)` is enabled, JSON is indented.
|
||||||
|
|
||||||
|
## Message replacement
|
||||||
|
|
||||||
|
`AddReplacer(old, new)` replaces matching text in every message before the
|
||||||
|
record reaches any writer. Replacement rules are applied in the order they are
|
||||||
|
added.
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger := slog.CreateLogger().
|
||||||
|
Level(slog.DEBUG).
|
||||||
|
AddReplacer("SOME_SECRET", "<redacted>").
|
||||||
|
AddReplacer("user@example.com", "<email>")
|
||||||
|
|
||||||
|
logger.Infoln("login token:", "SOME_SECRET")
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes `<redacted>` instead of `SOME_SECRET` in both text and JSON output.
|
||||||
|
An empty `old` value is ignored.
|
||||||
|
|
||||||
## API summary
|
## API summary
|
||||||
|
|
||||||
- `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`.
|
||||||
- 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.
|
||||||
|
|
||||||
## Traceback behavior
|
## Traceback behavior
|
||||||
|
|
||||||
|
|||||||
+22
-1
@@ -11,6 +11,7 @@ English version: [README.md](README.md)
|
|||||||
- `stdout`, файлы и любые внешние `io.Writer`.
|
- `stdout`, файлы и любые внешние `io.Writer`.
|
||||||
- Опциональные timestamp'ы для текстового вывода.
|
- Опциональные timestamp'ы для текстового вывода.
|
||||||
- Компактный traceback для текстовых writer'ов и полный traceback для JSON.
|
- Компактный traceback для текстовых writer'ов и полный traceback для JSON.
|
||||||
|
- Замена текста в сообщениях для маскирования секретов или нормализации вывода.
|
||||||
- Явные правила владения writer'ами при `Close()`.
|
- Явные правила владения writer'ами при `Close()`.
|
||||||
|
|
||||||
## Установка
|
## Установка
|
||||||
@@ -35,7 +36,8 @@ func main() {
|
|||||||
Prefix("API").
|
Prefix("API").
|
||||||
Level(slog.DEBUG).
|
Level(slog.DEBUG).
|
||||||
PrintTraceback(true).
|
PrintTraceback(true).
|
||||||
JsonPretty(true)
|
JsonPretty(true).
|
||||||
|
AddReplacer("SOME_SECRET", "<redacted>")
|
||||||
|
|
||||||
text := logger.CreateTextStdoutWriter()
|
text := logger.CreateTextStdoutWriter()
|
||||||
jsonFile, err := logger.CreateJsonFileWriter("logs/app.json")
|
jsonFile, err := logger.CreateJsonFileWriter("logs/app.json")
|
||||||
@@ -49,6 +51,7 @@ func main() {
|
|||||||
logger.Warnln("cache miss")
|
logger.Warnln("cache miss")
|
||||||
logger.Errorln("request failed")
|
logger.Errorln("request failed")
|
||||||
logger.Debugln("debug details")
|
logger.Debugln("debug details")
|
||||||
|
logger.Infoln("token", "SOME_SECRET")
|
||||||
|
|
||||||
if err := logger.Close(); err != nil {
|
if err := logger.Close(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -116,12 +119,30 @@ JSON writer записывает объект со следующими поля
|
|||||||
|
|
||||||
Если включен `JsonPretty(true)`, JSON выводится с отступами.
|
Если включен `JsonPretty(true)`, JSON выводится с отступами.
|
||||||
|
|
||||||
|
## Замена сообщений
|
||||||
|
|
||||||
|
`AddReplacer(old, new)` заменяет найденный текст в каждом сообщении до того,
|
||||||
|
как запись попадет в writer'ы. Правила замены применяются в порядке добавления.
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger := slog.CreateLogger().
|
||||||
|
Level(slog.DEBUG).
|
||||||
|
AddReplacer("SOME_SECRET", "<redacted>").
|
||||||
|
AddReplacer("user@example.com", "<email>")
|
||||||
|
|
||||||
|
logger.Infoln("login token:", "SOME_SECRET")
|
||||||
|
```
|
||||||
|
|
||||||
|
В текстовом и JSON-выводе вместо `SOME_SECRET` будет записано `<redacted>`.
|
||||||
|
Пустое значение `old` игнорируется.
|
||||||
|
|
||||||
## API кратко
|
## API кратко
|
||||||
|
|
||||||
- `Info`, `Warn`, `Error`, `Debug`, `Fatal` принимают список значений.
|
- `Info`, `Warn`, `Error`, `Debug`, `Fatal` принимают список значений.
|
||||||
- `Infof`, `Warnf`, `Errorf`, `Debugf`, `Fatalf` используют `fmt.Sprintf`.
|
- `Infof`, `Warnf`, `Errorf`, `Debugf`, `Fatalf` используют `fmt.Sprintf`.
|
||||||
- Методы `*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'ы.
|
||||||
|
|
||||||
## Поведение traceback
|
## Поведение traceback
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# v1.2.0
|
||||||
|
|
||||||
|
## Highlights
|
||||||
|
|
||||||
|
- Added message replacement with `Logger.AddReplacer(old, new)` for masking secrets or normalizing log output.
|
||||||
|
- Preserved the existing `LoggerWriter.Print(...any)` contract, so custom writers do not need a signature update.
|
||||||
|
- Improved JSON writer newline handling for messages that already end with `\n`.
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- `Logger.AddReplacer(old, new)` appends a replacement rule that is applied before records are sent to writers.
|
||||||
|
- README and package GoDoc now document message replacement and include usage examples.
|
||||||
|
- Tests covering message type preservation for custom writers, JSON empty messages, and JSON trailing newline behavior.
|
||||||
|
|
||||||
|
## Fixed
|
||||||
|
|
||||||
|
- `LoggerJsonWriter.Print` no longer panics when called with no message arguments.
|
||||||
|
- `LoggerJsonWriter.Print` now treats a trailing newline at the end of the last message as newline semantics and does not include that newline in the JSON `message` field.
|
||||||
|
- Log messages keep their original argument types for custom writers when no replacement rules are configured.
|
||||||
|
|
||||||
|
## Changed
|
||||||
|
|
||||||
|
- Updated the example program to demonstrate `AddReplacer`.
|
||||||
|
- Updated dependencies:
|
||||||
|
- `github.com/fatih/color` from `v1.18.0` to `v1.19.0`
|
||||||
|
- `github.com/mattn/go-isatty` from `v0.0.20` to `v0.0.21`
|
||||||
|
- `golang.org/x/sys` from `v0.42.0` to `v0.43.0`
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
No breaking API changes are intended in this release. The public `LoggerWriter` interface continues to accept `messages ...any`.
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
// - pretty JSON: disabled
|
// - pretty JSON: disabled
|
||||||
//
|
//
|
||||||
// Call Level(DEBUG) to enable all records, including debug messages.
|
// Call Level(DEBUG) to enable all records, including debug messages.
|
||||||
|
// AddReplacer can be used to mask or normalize message text before records are
|
||||||
|
// passed to writers.
|
||||||
//
|
//
|
||||||
// Basic usage:
|
// Basic usage:
|
||||||
//
|
//
|
||||||
@@ -20,7 +22,8 @@
|
|||||||
// Prefix("API").
|
// Prefix("API").
|
||||||
// Level(slog.DEBUG).
|
// Level(slog.DEBUG).
|
||||||
// PrintTraceback(true).
|
// PrintTraceback(true).
|
||||||
// JsonPretty(true)
|
// JsonPretty(true).
|
||||||
|
// AddReplacer("secret-token", "<redacted>")
|
||||||
//
|
//
|
||||||
// text := logger.CreateTextStdoutWriter()
|
// text := logger.CreateTextStdoutWriter()
|
||||||
// jsonFile, err := logger.CreateJsonFileWriter("logs/app.json")
|
// jsonFile, err := logger.CreateJsonFileWriter("logs/app.json")
|
||||||
@@ -29,7 +32,7 @@
|
|||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// logger.AddWriters(text, jsonFile)
|
// logger.AddWriters(text, jsonFile)
|
||||||
// logger.Infoln("service started")
|
// logger.Infoln("service started", "secret-token")
|
||||||
//
|
//
|
||||||
// if err := logger.Close(); err != nil {
|
// if err := logger.Close(); err != nil {
|
||||||
// panic(err)
|
// panic(err)
|
||||||
|
|||||||
+5
-1
@@ -11,18 +11,21 @@ func main() {
|
|||||||
logger := slog.CreateLogger().
|
logger := slog.CreateLogger().
|
||||||
Prefix("EXAMPLE").
|
Prefix("EXAMPLE").
|
||||||
Level(slog.DEBUG).
|
Level(slog.DEBUG).
|
||||||
JsonPretty(true)
|
JsonPretty(true).
|
||||||
|
AddReplacer("SOME_SECRET", "<Secret>")
|
||||||
|
|
||||||
textStdout := logger.CreateTextStdoutWriter()
|
textStdout := logger.CreateTextStdoutWriter()
|
||||||
jsonStdout := logger.CreateJsonStdoutWriter()
|
jsonStdout := logger.CreateJsonStdoutWriter()
|
||||||
|
|
||||||
textFile, err := logger.CreateTextFileWriter("logs/text.log")
|
textFile, err := logger.CreateTextFileWriter("logs/text.log")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Close()
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonFile, err := logger.CreateJsonFileWriter("logs/json.log")
|
jsonFile, err := logger.CreateJsonFileWriter("logs/json.log")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Close()
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ func main() {
|
|||||||
logger.Warnln("cache miss")
|
logger.Warnln("cache miss")
|
||||||
logger.Errorln("request failed")
|
logger.Errorln("request failed")
|
||||||
logger.Debugln("debug details")
|
logger.Debugln("debug details")
|
||||||
|
logger.Infoln("sensitive info, SOME_SECRET")
|
||||||
|
|
||||||
if err := logger.Close(); err != nil {
|
if err := logger.Close(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ module git.scuroneko.dev/scuroneko/slog
|
|||||||
|
|
||||||
go 1.26
|
go 1.26
|
||||||
|
|
||||||
require github.com/fatih/color v1.18.0
|
require github.com/fatih/color v1.19.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||||
|
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import (
|
|||||||
func (l *Logger) Infof(format string, args ...any) {
|
func (l *Logger) Infof(format string, args ...any) {
|
||||||
l.print(INFO, fmt.Sprintf(format, args...))
|
l.print(INFO, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs an info message.
|
// Info logs an info message.
|
||||||
func (l *Logger) Info(m ...any) {
|
func (l *Logger) Info(m ...any) {
|
||||||
l.print(INFO, m...)
|
l.print(INFO, m...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infoln logs an info message and appends a newline semantic for writers that need it.
|
// Infoln logs an info message and appends a newline semantic for writers that need it.
|
||||||
func (l *Logger) Infoln(m ...any) {
|
func (l *Logger) Infoln(m ...any) {
|
||||||
l.println(INFO, m...)
|
l.println(INFO, m...)
|
||||||
@@ -22,10 +24,12 @@ func (l *Logger) Infoln(m ...any) {
|
|||||||
func (l *Logger) Warnf(format string, args ...any) {
|
func (l *Logger) Warnf(format string, args ...any) {
|
||||||
l.print(WARN, fmt.Sprintf(format, args...))
|
l.print(WARN, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs a warning message.
|
// Warn logs a warning message.
|
||||||
func (l *Logger) Warn(m ...any) {
|
func (l *Logger) Warn(m ...any) {
|
||||||
l.print(WARN, m...)
|
l.print(WARN, m...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warnln logs a warning message with newline semantic.
|
// Warnln logs a warning message with newline semantic.
|
||||||
func (l *Logger) Warnln(m ...any) {
|
func (l *Logger) Warnln(m ...any) {
|
||||||
l.println(WARN, m...)
|
l.println(WARN, m...)
|
||||||
@@ -35,10 +39,12 @@ func (l *Logger) Warnln(m ...any) {
|
|||||||
func (l *Logger) Errorf(format string, args ...any) {
|
func (l *Logger) Errorf(format string, args ...any) {
|
||||||
l.print(ERROR, fmt.Sprintf(format, args...))
|
l.print(ERROR, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs an error message.
|
// Error logs an error message.
|
||||||
func (l *Logger) Error(m ...any) {
|
func (l *Logger) Error(m ...any) {
|
||||||
l.print(ERROR, m...)
|
l.print(ERROR, m...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorln logs an error message with newline semantic.
|
// Errorln logs an error message with newline semantic.
|
||||||
func (l *Logger) Errorln(m ...any) {
|
func (l *Logger) Errorln(m ...any) {
|
||||||
l.println(ERROR, m...)
|
l.println(ERROR, m...)
|
||||||
@@ -49,11 +55,13 @@ func (l *Logger) Fatalf(format string, args ...any) {
|
|||||||
l.print(FATAL, fmt.Sprintf(format, args...))
|
l.print(FATAL, fmt.Sprintf(format, args...))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs a fatal message and exits the process with code 1.
|
// Fatal logs a fatal message and exits the process with code 1.
|
||||||
func (l *Logger) Fatal(m ...any) {
|
func (l *Logger) Fatal(m ...any) {
|
||||||
l.print(FATAL, m...)
|
l.print(FATAL, m...)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatalln logs a fatal message with newline semantic and exits the process with code 1.
|
// Fatalln logs a fatal message with newline semantic and exits the process with code 1.
|
||||||
func (l *Logger) Fatalln(m ...any) {
|
func (l *Logger) Fatalln(m ...any) {
|
||||||
l.println(FATAL, m...)
|
l.println(FATAL, m...)
|
||||||
@@ -64,10 +72,12 @@ func (l *Logger) Fatalln(m ...any) {
|
|||||||
func (l *Logger) Debugf(format string, args ...any) {
|
func (l *Logger) Debugf(format string, args ...any) {
|
||||||
l.print(DEBUG, fmt.Sprintf(format, args...))
|
l.print(DEBUG, fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs a debug message.
|
// Debug logs a debug message.
|
||||||
func (l *Logger) Debug(m ...any) {
|
func (l *Logger) Debug(m ...any) {
|
||||||
l.print(DEBUG, m...)
|
l.print(DEBUG, m...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debugln logs a debug message with newline semantic.
|
// Debugln logs a debug message with newline semantic.
|
||||||
func (l *Logger) Debugln(m ...any) {
|
func (l *Logger) Debugln(m ...any) {
|
||||||
l.println(DEBUG, m...)
|
l.println(DEBUG, m...)
|
||||||
@@ -88,7 +98,7 @@ func (l *Logger) print(level LogLevel, m ...any) {
|
|||||||
if writer == nil {
|
if writer == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err := writer.Print(level, l.prefix, tb, m...)
|
err := writer.Print(level, l.prefix, tb, l.replaceAll(m...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.reportWriterError(err)
|
l.reportWriterError(err)
|
||||||
}
|
}
|
||||||
@@ -111,7 +121,7 @@ func (l *Logger) println(level LogLevel, m ...any) {
|
|||||||
if writer == nil {
|
if writer == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err := writer.Print(level, l.prefix, tb, messages...)
|
err := writer.Print(level, l.prefix, tb, l.replaceAll(messages...)...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.reportWriterError(err)
|
l.reportWriterError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,21 @@ import (
|
|||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type replacer struct {
|
||||||
|
old string
|
||||||
|
new string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r replacer) replace(s string) string {
|
||||||
|
return strings.ReplaceAll(s, r.old, r.new)
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
printTraceback bool
|
printTraceback bool
|
||||||
printTime bool
|
printTime bool
|
||||||
@@ -66,36 +76,57 @@ func (l *Logger) Prefix(prefix string) *Logger {
|
|||||||
l.prefix = prefix
|
l.prefix = prefix
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
// Level sets the maximum enabled level and returns the logger for chaining.
|
// Level sets the maximum enabled level and returns the logger for chaining.
|
||||||
func (l *Logger) Level(level LogLevel) *Logger {
|
func (l *Logger) Level(level LogLevel) *Logger {
|
||||||
l.level = level
|
l.level = level
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintTraceback enables traceback output for text writers.
|
// PrintTraceback enables traceback output for text writers.
|
||||||
func (l *Logger) PrintTraceback(b bool) *Logger {
|
func (l *Logger) PrintTraceback(b bool) *Logger {
|
||||||
l.printTraceback = b
|
l.printTraceback = b
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrintTime enables timestamps for text writers.
|
// PrintTime enables timestamps for text writers.
|
||||||
func (l *Logger) PrintTime(b bool) *Logger {
|
func (l *Logger) PrintTime(b bool) *Logger {
|
||||||
l.printTime = b
|
l.printTime = b
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
// JsonPretty enables indented JSON output for JSON writers.
|
// JsonPretty enables indented JSON output for JSON writers.
|
||||||
func (l *Logger) JsonPretty(b bool) *Logger {
|
func (l *Logger) JsonPretty(b bool) *Logger {
|
||||||
l.jsonPretty = b
|
l.jsonPretty = b
|
||||||
return l
|
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...)
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddWriter appends a single writer to the logger.
|
// AddWriter appends a single writer to the logger.
|
||||||
func (l *Logger) AddWriter(writer LoggerWriter) *Logger {
|
func (l *Logger) AddWriter(writer LoggerWriter) *Logger {
|
||||||
l.writers = append(l.writers, writer)
|
l.writers = append(l.writers, writer)
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddReplacer appends a message replacer and returns the logger for chaining.
|
||||||
|
//
|
||||||
|
// Replacement rules are applied before records are passed to writers. Empty old
|
||||||
|
// values are ignored because replacing an empty string would insert the
|
||||||
|
// replacement between every UTF-8 sequence.
|
||||||
|
func (l *Logger) AddReplacer(old, new string) *Logger {
|
||||||
|
if old == "" {
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
r := replacer{old: old, new: new}
|
||||||
|
l.replacers = append(l.replacers, r)
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// Close closes all owned writers and returns a joined error, if any.
|
// Close closes all owned writers and returns a joined error, if any.
|
||||||
func (l *Logger) Close() error {
|
func (l *Logger) Close() error {
|
||||||
var errs []error
|
var errs []error
|
||||||
@@ -116,35 +147,61 @@ func (l *Logger) Close() error {
|
|||||||
func (l *Logger) CreateTextWriter(w io.Writer) *LoggerTextWriter {
|
func (l *Logger) CreateTextWriter(w io.Writer) *LoggerTextWriter {
|
||||||
return CreateTextWriter(w, l.printTraceback, l.printTime)
|
return CreateTextWriter(w, l.printTraceback, l.printTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTextStdoutWriter creates a non-owning text writer for os.Stdout.
|
// CreateTextStdoutWriter creates a non-owning text writer for os.Stdout.
|
||||||
func (l *Logger) CreateTextStdoutWriter() *LoggerTextWriter {
|
func (l *Logger) CreateTextStdoutWriter() *LoggerTextWriter {
|
||||||
return CreateTextStdoutWriter(l.printTraceback, l.printTime)
|
return CreateTextStdoutWriter(l.printTraceback, l.printTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTextFileWriter creates an owning text writer for a file.
|
// CreateTextFileWriter creates an owning text writer for a file.
|
||||||
func (l *Logger) CreateTextFileWriter(filename string) (*LoggerTextWriter, error) {
|
func (l *Logger) CreateTextFileWriter(filename string) (*LoggerTextWriter, error) {
|
||||||
return CreateTextFileWriter(filename, l.printTraceback, l.printTime)
|
return CreateTextFileWriter(filename, l.printTraceback, l.printTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateJsonWriter wraps an external writer with the logger JSON settings.
|
// CreateJsonWriter wraps an external writer with the logger JSON settings.
|
||||||
func (l *Logger) CreateJsonWriter(w io.Writer) *LoggerJsonWriter {
|
func (l *Logger) CreateJsonWriter(w io.Writer) *LoggerJsonWriter {
|
||||||
return CreateJsonWriter(w, l.jsonPretty)
|
return CreateJsonWriter(w, l.jsonPretty)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateJsonStdoutWriter creates a non-owning JSON writer for os.Stdout.
|
// CreateJsonStdoutWriter creates a non-owning JSON writer for os.Stdout.
|
||||||
func (l *Logger) CreateJsonStdoutWriter() *LoggerJsonWriter {
|
func (l *Logger) CreateJsonStdoutWriter() *LoggerJsonWriter {
|
||||||
return CreateJsonStdoutWriter(l.jsonPretty)
|
return CreateJsonStdoutWriter(l.jsonPretty)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateJsonFileWriter creates an owning JSON writer for a file.
|
// CreateJsonFileWriter creates an owning JSON writer for a file.
|
||||||
func (l *Logger) CreateJsonFileWriter(filename string) (*LoggerJsonWriter, error) {
|
func (l *Logger) CreateJsonFileWriter(filename string) (*LoggerJsonWriter, error) {
|
||||||
return CreateJsonFileWriter(filename, l.jsonPretty)
|
return CreateJsonFileWriter(filename, l.jsonPretty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Logger) replace(s string) string {
|
||||||
|
out := s
|
||||||
|
for _, repl := range l.replacers {
|
||||||
|
out = repl.replace(out)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) replaceAll(messages ...any) []any {
|
||||||
|
if len(l.replacers) == 0 {
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
out := make([]any, len(messages))
|
||||||
|
for i, msg := range messages {
|
||||||
|
out[i] = l.replace(fmt.Sprint(msg))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// FormatTime converts time to the package text log timestamp format.
|
// FormatTime converts time to the package text log timestamp format.
|
||||||
func FormatTime(t time.Time) string {
|
func FormatTime(t time.Time) string {
|
||||||
return fmt.Sprintf("%02d.%02d.%02d %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second())
|
return fmt.Sprintf("%02d.%02d.%02d %02d:%02d:%02d", t.Day(), t.Month(), t.Year(), t.Hour(), t.Minute(), t.Second())
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatTraceback converts a traceback frame to a compact string.
|
// FormatTraceback converts a traceback frame to a compact string.
|
||||||
func FormatTraceback(mt *MethodTraceback) string {
|
func FormatTraceback(mt *MethodTraceback) string {
|
||||||
return fmt.Sprintf("%s:%s:%d", mt.Filename, mt.Method, mt.Line)
|
return fmt.Sprintf("%s:%s:%d", mt.Filename, mt.Method, mt.Line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatFullTraceback joins multiple traceback frames into one string.
|
// FormatFullTraceback joins multiple traceback frames into one string.
|
||||||
func FormatFullTraceback(tracebacks []*MethodTraceback) string {
|
func FormatFullTraceback(tracebacks []*MethodTraceback) string {
|
||||||
formatted := make([]string, 0)
|
formatted := make([]string, 0)
|
||||||
@@ -153,8 +210,9 @@ func FormatFullTraceback(tracebacks []*MethodTraceback) string {
|
|||||||
}
|
}
|
||||||
return strings.Join(formatted, "->")
|
return strings.Join(formatted, "->")
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildString renders a text log record using the provided settings.
|
// BuildString renders a text log record using the provided settings.
|
||||||
func BuildString(level LogLevel, prefix string, printTime, printTraceback bool, m ...any) string {
|
func BuildString(level LogLevel, prefix string, printTime, printTraceback bool, messages ...any) string {
|
||||||
args := []string{
|
args := []string{
|
||||||
fmt.Sprintf("[%s]", prefix),
|
fmt.Sprintf("[%s]", prefix),
|
||||||
fmt.Sprintf("[%s]", strings.ToUpper(level.t)),
|
fmt.Sprintf("[%s]", strings.ToUpper(level.t)),
|
||||||
@@ -168,9 +226,9 @@ func BuildString(level LogLevel, prefix string, printTime, printTraceback bool,
|
|||||||
args = append(args, fmt.Sprintf("[%s]", FormatTime(time.Now())))
|
args = append(args, fmt.Sprintf("[%s]", FormatTime(time.Now())))
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := Map(m, func(el any) string {
|
m := Map(messages, func(t any) string {
|
||||||
return fmt.Sprintf("%v", el)
|
return fmt.Sprint(t)
|
||||||
})
|
})
|
||||||
s := fmt.Sprintf("%s %s", strings.Join(args, " "), strings.Join(msg, " "))
|
s := fmt.Sprintf("%s %s", strings.Join(args, " "), strings.Join(m, " "))
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-1
@@ -2,6 +2,7 @@ package slog
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,6 +14,7 @@ type stubLoggerWriter struct {
|
|||||||
closeErr error
|
closeErr error
|
||||||
printCalls int
|
printCalls int
|
||||||
closeCalls int
|
closeCalls int
|
||||||
|
messages []any
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *stubLoggerWriter) Close() error {
|
func (w *stubLoggerWriter) Close() error {
|
||||||
@@ -24,8 +26,9 @@ func (w *stubLoggerWriter) Write(p []byte) (int, error) {
|
|||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *stubLoggerWriter) Print(_ LogLevel, _ string, _ []*MethodTraceback, _ ...any) error {
|
func (w *stubLoggerWriter) Print(_ LogLevel, _ string, _ []*MethodTraceback, messages ...any) error {
|
||||||
w.printCalls++
|
w.printCalls++
|
||||||
|
w.messages = append([]any(nil), messages...)
|
||||||
return w.printErr
|
return w.printErr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +83,20 @@ func TestLoggerPrintDoesNotRecurseOnWriterError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoggerPreservesMessageTypesWithoutReplacers(t *testing.T) {
|
||||||
|
writer := &stubLoggerWriter{}
|
||||||
|
logger := CreateLogger().AddWriter(writer)
|
||||||
|
|
||||||
|
logger.Error("status", 500)
|
||||||
|
|
||||||
|
if len(writer.messages) != 2 {
|
||||||
|
t.Fatalf("writer should receive two messages, got %d", len(writer.messages))
|
||||||
|
}
|
||||||
|
if _, ok := writer.messages[1].(int); !ok {
|
||||||
|
t.Fatalf("writer should receive original int message type, got %T", writer.messages[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateTextWriterCloseOnNonCloserIsNoOp(t *testing.T) {
|
func TestCreateTextWriterCloseOnNonCloserIsNoOp(t *testing.T) {
|
||||||
writer := CreateTextWriter(&bytes.Buffer{}, false, false)
|
writer := CreateTextWriter(&bytes.Buffer{}, false, false)
|
||||||
if err := writer.Close(); err != nil {
|
if err := writer.Close(); err != nil {
|
||||||
@@ -130,6 +147,45 @@ func TestCreateJsonWriterDoesNotCloseExternalCloser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestJsonWriterPrintAllowsEmptyMessages(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer := CreateJsonWriter(&buf, false)
|
||||||
|
|
||||||
|
if err := writer.Print(INFO, "TEST", nil); err != nil {
|
||||||
|
t.Fatalf("Print() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var message LoggerJsonMessage
|
||||||
|
if err := json.Unmarshal(buf.Bytes(), &message); err != nil {
|
||||||
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
||||||
|
}
|
||||||
|
if message.Message != "" {
|
||||||
|
t.Fatalf("message should be empty, got %q", message.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
writer := CreateJsonWriter(&buf, false)
|
||||||
|
|
||||||
|
if err := writer.Print(INFO, "TEST", nil, "hello\n"); err != nil {
|
||||||
|
t.Fatalf("Print() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := buf.Bytes()
|
||||||
|
if !bytes.HasSuffix(data, []byte("\n")) {
|
||||||
|
t.Fatalf("JSON output should end with newline, got %q", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var message LoggerJsonMessage
|
||||||
|
if err := json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &message); err != nil {
|
||||||
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
||||||
|
}
|
||||||
|
if message.Message != "hello" {
|
||||||
|
t.Fatalf("message should not include trailing newline, got %q", message.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateTextStdoutWriterDoesNotCloseStdout(t *testing.T) {
|
func TestCreateTextStdoutWriterDoesNotCloseStdout(t *testing.T) {
|
||||||
stdoutFile := swapStdout(t)
|
stdoutFile := swapStdout(t)
|
||||||
|
|
||||||
|
|||||||
+3
-5
@@ -70,14 +70,12 @@ func (w *LoggerJsonWriter) Write(data []byte) (int, error) {
|
|||||||
|
|
||||||
// Print encodes the provided record as JSON and writes it to the underlying writer.
|
// Print encodes the provided record as JSON and writes it to the underlying writer.
|
||||||
func (w *LoggerJsonWriter) Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error {
|
func (w *LoggerJsonWriter) Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error {
|
||||||
msg := Map(messages, func(el any) string {
|
msg := Map(messages, func(el any) string { return fmt.Sprint(el) })
|
||||||
return fmt.Sprintf("%v", el)
|
|
||||||
})
|
|
||||||
|
|
||||||
newline := false
|
newline := false
|
||||||
if msg[len(msg)-1] == "\n" {
|
if len(msg) > 0 && strings.HasSuffix(msg[len(msg)-1], "\n") {
|
||||||
newline = true
|
newline = true
|
||||||
msg = msg[:len(msg)-1]
|
msg[len(msg)-1] = strings.TrimSuffix(msg[len(msg)-1], "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
m := LoggerJsonMessage{
|
m := LoggerJsonMessage{
|
||||||
|
|||||||
Reference in New Issue
Block a user