(refactor): prepare breaking v2 API

- rename package to sneklog and move module to /v2
- replace text output flags with writer formatters
- remove external color dependencies
- update migration notes and coverage
This commit is contained in:
2026-04-24 15:43:14 +03:00
parent 14b787710c
commit d534f92d48
18 changed files with 845 additions and 171 deletions
+12
View File
@@ -0,0 +1,12 @@
name: Golang lint
run-name: Linting code
on: [push]
jobs:
lint:
runs-on: go-latest
steps:
- name: Checkout repository code
uses: actions/checkout@v6
- name: Run golangci-lint
run: golangci-lint run
+2
View File
@@ -1,3 +1,5 @@
.idea/ .idea/
.vscode/
.codex
test/ test/
*.log *.log
+13 -15
View File
@@ -1,4 +1,4 @@
# slog # SNekLog (ScuroNeko Logger)
Small structured logger for Go with text and JSON output, multiple writers, and optional traceback metadata. Small structured logger for Go with text and JSON output, multiple writers, and optional traceback metadata.
@@ -17,7 +17,7 @@ Russian version: [README_ru.md](README_ru.md)
## Installation ## Installation
```bash ```bash
go get git.scuroneko.dev/scuroneko/slog go get git.scuroneko.dev/scuroneko/slog/v2
``` ```
## Quick start ## Quick start
@@ -28,15 +28,13 @@ package main
import ( import (
"log" "log"
"git.scuroneko.dev/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog/v2"
) )
func main() { func main() {
logger := slog.CreateLogger(). logger := sneklog.CreateLogger().
Prefix("API"). Prefix("API").
Level(slog.DEBUG). Level(sneklog.DEBUG).
PrintTraceback(true).
JsonPretty(true).
AddReplacer("SOME_SECRET", "<redacted>") AddReplacer("SOME_SECRET", "<redacted>")
text := logger.CreateTextStdoutWriter() text := logger.CreateTextStdoutWriter()
@@ -64,12 +62,12 @@ func main() {
`CreateLogger()` starts with: `CreateLogger()` starts with:
- `Prefix("LOG")` - `Prefix("LOG")`
- `Level(slog.FATAL)` - `Level(sneklog.FATAL)`
- `PrintTime(true)`
- `PrintTraceback(false)`
- `JsonPretty(false)` - `JsonPretty(false)`
- text formatter: `sneklog.DefaultTextFormatter`
- JSON formatter: `sneklog.DefaultJsonFormatter`
Important: with the current level ordering, `Level(slog.FATAL)` allows `INFO`, `WARN`, `ERROR`, and `FATAL`, but not `DEBUG`. Use `Level(slog.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.
## Writers and ownership ## Writers and ownership
@@ -92,10 +90,10 @@ This makes it safe to plug in `bytes.Buffer`, network writers, and other externa
Text writers render records like: Text writers render records like:
```text ```text
[API] [INFO] [main.go:main:27] [17.03.26 14:05:09] service started 2026-03-17T14:05:09+03:00 info API: service started
``` ```
The traceback field is included only when `PrintTraceback(true)` is enabled. Use `SetFormatter` on a writer to customize timestamps, traceback fields, colors, and message layout.
JSON writers emit objects with this shape: JSON writers emit objects with this shape:
@@ -126,8 +124,8 @@ record reaches any writer. Replacement rules are applied in the order they are
added. added.
```go ```go
logger := slog.CreateLogger(). logger := sneklog.CreateLogger().
Level(slog.DEBUG). Level(sneklog.DEBUG).
AddReplacer("SOME_SECRET", "<redacted>"). AddReplacer("SOME_SECRET", "<redacted>").
AddReplacer("user@example.com", "<email>") AddReplacer("user@example.com", "<email>")
+13 -15
View File
@@ -1,4 +1,4 @@
# slog # SNekLog (ScuroNeko Logger)
Небольшой структурированный логгер для Go с текстовым и JSON-выводом, несколькими writer'ами и настраиваемыми traceback-метаданными. Небольшой структурированный логгер для Go с текстовым и JSON-выводом, несколькими writer'ами и настраиваемыми traceback-метаданными.
@@ -17,7 +17,7 @@ English version: [README.md](README.md)
## Установка ## Установка
```bash ```bash
go get git.scuroneko.dev/scuroneko/slog go get git.scuroneko.dev/scuroneko/slog/v2
``` ```
## Быстрый старт ## Быстрый старт
@@ -28,15 +28,13 @@ package main
import ( import (
"log" "log"
"git.scuroneko.dev/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog/v2"
) )
func main() { func main() {
logger := slog.CreateLogger(). logger := sneklog.CreateLogger().
Prefix("API"). Prefix("API").
Level(slog.DEBUG). Level(sneklog.DEBUG).
PrintTraceback(true).
JsonPretty(true).
AddReplacer("SOME_SECRET", "<redacted>") AddReplacer("SOME_SECRET", "<redacted>")
text := logger.CreateTextStdoutWriter() text := logger.CreateTextStdoutWriter()
@@ -64,12 +62,12 @@ func main() {
`CreateLogger()` создает логгер со следующими настройками: `CreateLogger()` создает логгер со следующими настройками:
- `Prefix("LOG")` - `Prefix("LOG")`
- `Level(slog.FATAL)` - `Level(sneklog.FATAL)`
- `PrintTime(true)`
- `PrintTraceback(false)`
- `JsonPretty(false)` - `JsonPretty(false)`
- текстовый formatter: `sneklog.DefaultTextFormatter`
- JSON formatter: `sneklog.DefaultJsonFormatter`
Важно: в текущей модели уровней `Level(slog.FATAL)` пропускает `INFO`, `WARN`, `ERROR` и `FATAL`, но не `DEBUG`. Чтобы включить все сообщения, используйте `Level(slog.DEBUG)`. Важно: в текущей модели уровней `Level(sneklog.FATAL)` пропускает `INFO`, `WARN`, `ERROR` и `FATAL`, но не `DEBUG`. Чтобы включить все сообщения, используйте `Level(sneklog.DEBUG)`.
## Writer'ы и владение ## Writer'ы и владение
@@ -92,10 +90,10 @@ func main() {
Текстовый writer формирует записи вида: Текстовый writer формирует записи вида:
```text ```text
[API] [INFO] [main.go:main:27] [17.03.26 14:05:09] service started 2026-03-17T14:05:09+03:00 info API: service started
``` ```
Поле traceback появляется только если включен `PrintTraceback(true)`. Используйте `SetFormatter` у writer'а, чтобы настроить timestamp'ы, traceback-поля, цвета и шаблон сообщения.
JSON writer записывает объект со следующими полями: JSON writer записывает объект со следующими полями:
@@ -125,8 +123,8 @@ JSON writer записывает объект со следующими поля
как запись попадет в writer'ы. Правила замены применяются в порядке добавления. как запись попадет в writer'ы. Правила замены применяются в порядке добавления.
```go ```go
logger := slog.CreateLogger(). logger := sneklog.CreateLogger().
Level(slog.DEBUG). Level(sneklog.DEBUG).
AddReplacer("SOME_SECRET", "<redacted>"). AddReplacer("SOME_SECRET", "<redacted>").
AddReplacer("user@example.com", "<email>") AddReplacer("user@example.com", "<email>")
+91
View File
@@ -1,3 +1,94 @@
# v2.0.0
## Highlights
- Renamed the public Go package from `slog` to `sneklog` to avoid confusion with the standard library `log/slog` package.
- Moved the module path to `git.scuroneko.dev/scuroneko/slog/v2` for Go module major-version compatibility.
- Replaced fixed text writer settings with formatter-based output customization.
- Added built-in ANSI color support and removed the external `github.com/fatih/color` dependency tree.
- Formatters: a new way do display your logs!
## Breaking Changes
- Import paths must use the `/v2` module suffix:
```go
import sneklog "git.scuroneko.dev/scuroneko/slog/v2"
```
- Package references should use `sneklog` instead of `slog`.
- Removed `Logger.PrintTime` and `Logger.PrintTraceback`; configure output through writer formatters instead.
- Changed text writer constructors:
- `CreateTextWriter(w, printTraceback, printTime)` is now `CreateTextWriter(w)`.
- `CreateTextFileWriter(path, printTraceback, printTime)` is now `CreateTextFileWriter(path)`.
- `CreateTextStdoutWriter(printTraceback, printTime)` is now `CreateTextStdoutWriter()`.
- Removed the old text formatting helpers: `BuildString`, `FormatTime`, `FormatTraceback`, and `FormatFullTraceback`.
- JSON `time` values are now formatted through the configured formatter and stored as strings in `LoggerJsonMessage`.
## Added
- `Formatter` for configurable message layouts, timestamp layouts, traceback layouts, separators, and color behavior.
- `DefaultTextFormatter` and `DefaultJsonFormatter` defaults for text and JSON writers.
- `LoggerTextWriter.SetFormatter` and `LoggerJsonWriter.SetFormatter`.
- Strftime-like timestamp format constants such as `RFC3339`, `Kitchen`, `DateTime`, `DateOnly`, and `TimeOnly`.
- Built-in color types and helpers:
- `FgColor`, `BgColor`, and `Attribute`.
- `ColorStringBuilder`.
- Level color getters/setters on `LogLevel`.
- Tests covering formatter defaults, empty traceback placeholders, literal `%` sequences in messages, color controls, and JSON newline semantics.
## Changed
- Text output defaults to `DefaultTextFormatter` instead of the old hard-coded string builder.
- JSON output defaults to `DefaultJsonFormatter` and can now format the JSON `message` field through `Formatter`.
- Newline semantics are normalized before text and JSON formatting, so trailing `\n` controls record termination without becoming part of the message.
- Text color output is enabled by default only for stdout/stderr.
- Example code now demonstrates custom formatters and color configuration.
- Go dependencies were simplified by removing:
- `github.com/fatih/color`
- `github.com/mattn/go-colorable`
- `github.com/mattn/go-isatty`
- `golang.org/x/sys`
## Fixed
- `NewFormatter()` now returns a fresh copy instead of mutating `DefaultTextFormatter`.
- `Formatter.FormatMessage` no longer panics when called with an empty traceback.
- Placeholder-looking text inside log messages, such as `%L` or `%s`, is preserved literally.
- `SetColorOutput(false)` disables color output.
- `SetColorOnlyStdout(false)` allows color output for non-stdout writers.
## Migration
Before:
```go
import "git.scuroneko.dev/scuroneko/slog"
logger := slog.CreateLogger().
Prefix("API").
Level(slog.DEBUG).
PrintTraceback(true)
text := logger.CreateTextStdoutWriter()
```
After:
```go
import sneklog "git.scuroneko.dev/scuroneko/slog/v2"
logger := sneklog.CreateLogger().
Prefix("API").
Level(sneklog.DEBUG)
formatter := sneklog.NewFormatter().
SetFormat("%t [%L] %N: %m (%B)")
text := logger.CreateTextStdoutWriter().
SetFormatter(formatter)
```
# v1.2.0 # v1.2.0
## Highlights ## Highlights
+181
View File
@@ -0,0 +1,181 @@
package sneklog
import (
"fmt"
"strings"
)
type FgColor uint8
type BgColor uint8
type Attribute uint8
const (
Reset Attribute = iota
Bold
Faint
Italic
Underline
BlinkSlow
BlinkRapid
Invert
Conceal
CrossedOut
)
const (
DoubleUnderline Attribute = iota + 21 // Can be disable bold on some terminals
DisableBold
DisableItalic
DisableUnderline
DisableBlink
DisableInvert
DisableConceal
DisableCrossedOut
)
const (
FgBlack FgColor = iota + 30
FgRed
FgGreen
FgYellow
FgBlue
FgMagenta
FgCyan
FgWhite
FgDefault FgColor = 39
)
const (
BgBlack BgColor = iota + 40
BgRed
BgGreen
BgYellow
BgBlue
BgMagenta
BgCyan
BgWhite
BgDefault BgColor = 49
)
const (
FgHiBlack FgColor = iota + 90
FgHiRed
FgHiGreen
FgHiYellow
FgHiBlue
FgHiMagenta
FgHiCyan
FgHiWhite
)
const (
BgHiBlack BgColor = iota + 100
BgHiRed
BgHiGreen
BgHiYellow
BgHiBlue
BgHiMagenta
BgHiCyan
BgHiWhite
)
// This attributes rarely used and not supported by all terminals.
const (
Border Attribute = iota + 51
Outline
Upperline
DisableBorderOutline
DisableUpperline
)
type ColorStringBuilder struct {
str string
}
func NewColorStringBuilder() *ColorStringBuilder {
return &ColorStringBuilder{str: ""}
}
func (builder *ColorStringBuilder) AddAttribute(attr Attribute) *ColorStringBuilder {
builder.str += attr.String()
return builder
}
func (builder *ColorStringBuilder) AddFgColor(color FgColor) *ColorStringBuilder {
if color == 0 {
return builder
}
builder.str += color.String()
return builder
}
func (builder *ColorStringBuilder) AddBgColor(color BgColor) *ColorStringBuilder {
if color == 0 {
return builder
}
builder.str += color.String()
return builder
}
func (builder *ColorStringBuilder) AddColor256Fg(color uint8) *ColorStringBuilder {
builder.str += color256Fg(color)
return builder
}
func (builder *ColorStringBuilder) AddColor256Bg(color uint8) *ColorStringBuilder {
builder.str += color256Bg(color)
return builder
}
func (builder *ColorStringBuilder) AddColorRgbFg(r, g, b uint8) *ColorStringBuilder {
builder.str += colorRgbFg(r, g, b)
return builder
}
func (builder *ColorStringBuilder) AddColorRgbBg(r, g, b uint8) *ColorStringBuilder {
builder.str += colorRgbBg(r, g, b)
return builder
}
func (builder *ColorStringBuilder) AddReset() *ColorStringBuilder {
builder.str += "\x1b[0m"
return builder
}
func (builder *ColorStringBuilder) AddText(text string) *ColorStringBuilder {
builder.str += text
return builder
}
func (builder *ColorStringBuilder) String() string {
if strings.Contains(builder.str, "\x1b[") && !strings.HasSuffix(builder.str, "\x1b[0m") {
builder.AddReset()
}
return builder.str
}
func (a Attribute) String() string {
return fmt.Sprintf("\x1b[%dm", a)
}
func (a Attribute) Uint8() uint8 {
return uint8(a)
}
func (a Attribute) Int() int {
return int(a)
}
func (c FgColor) String() string {
return fmt.Sprintf("\x1b[%dm", c)
}
func (c FgColor) Uint8() uint8 {
return uint8(c)
}
func (c FgColor) Int() int {
return int(c)
}
func (c BgColor) String() string {
return fmt.Sprintf("\x1b[%dm", c)
}
func (c BgColor) Uint8() uint8 {
return uint8(c)
}
func (c BgColor) Int() int {
return int(c)
}
func color256Fg(color uint8) string {
return fmt.Sprintf("\x1b[38;5;%dm", color)
}
func color256Bg(color uint8) string {
return fmt.Sprintf("\x1b[48;5;%dm", color)
}
func colorRgbFg(r, g, b uint8) string {
return fmt.Sprintf("\x1b[38;2;%d;%d;%dm", r, g, b)
}
func colorRgbBg(r, g, b uint8) string {
return fmt.Sprintf("\x1b[48;2;%d;%d;%dm", r, g, b)
}
+8 -9
View File
@@ -1,4 +1,4 @@
// Package slog provides a small structured logger that can write the same // Package sneklog provides a small structured logger that can write the same
// record to text and JSON outputs. // record to text and JSON outputs.
// //
// A Logger can fan out records to stdout, files, or any external io.Writer. // A Logger can fan out records to stdout, files, or any external io.Writer.
@@ -8,21 +8,20 @@
// CreateLogger uses the following defaults: // CreateLogger uses the following defaults:
// - prefix: "LOG" // - prefix: "LOG"
// - level: FATAL, which enables info, warn, error, and fatal records // - level: FATAL, which enables info, warn, error, and fatal records
// - text timestamps: enabled
// - text traceback: disabled
// - pretty JSON: disabled // - pretty JSON: disabled
// - text formatter: DefaultTextFormatter
// - JSON formatter: DefaultJsonFormatter
// //
// 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 // AddReplacer can be used to mask or normalize message text before records are
// passed to writers. // passed to writers. Call SetFormatter on a writer to customize timestamps,
// traceback fields, colors, and message layout.
// //
// Basic usage: // Basic usage:
// //
// logger := slog.CreateLogger(). // logger := sneklog.CreateLogger().
// Prefix("API"). // Prefix("API").
// Level(slog.DEBUG). // Level(sneklog.DEBUG).
// PrintTraceback(true).
// JsonPretty(true).
// AddReplacer("secret-token", "<redacted>") // AddReplacer("secret-token", "<redacted>")
// //
// text := logger.CreateTextStdoutWriter() // text := logger.CreateTextStdoutWriter()
@@ -42,4 +41,4 @@
// 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.
package slog package sneklog
+26 -3
View File
@@ -4,19 +4,34 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"git.scuroneko.dev/scuroneko/slog" "git.scuroneko.dev/scuroneko/slog/v2"
) )
func main() { func main() {
logger := slog.CreateLogger(). logger := sneklog.CreateLogger().
Prefix("EXAMPLE"). Prefix("EXAMPLE").
Level(slog.DEBUG). Level(sneklog.DEBUG).
JsonPretty(true). JsonPretty(true).
AddReplacer("SOME_SECRET", "<Secret>") AddReplacer("SOME_SECRET", "<Secret>")
sneklog.INFO.SetBgColor(sneklog.BgBlue).SetFgColor(sneklog.FgWhite)
textStdout := logger.CreateTextStdoutWriter() textStdout := logger.CreateTextStdoutWriter()
jsonStdout := logger.CreateJsonStdoutWriter() jsonStdout := logger.CreateJsonStdoutWriter()
formatter := sneklog.NewFormatter().
SetFormat("[%t] [%L] [%N]: %m (%S)").
SetTimeStampFormat(sneklog.Kitchen).
SetTraceBackFormat("%s %f:%n %p")
jsonFormatter := sneklog.NewFormatter().
SetFormat("[%L] [%N] %m").
SetColorOutput(true).
SetTimeStampFormat(sneklog.Kitchen)
textStdout.SetFormatter(formatter)
jsonStdout.SetFormatter(jsonFormatter)
textFile, err := logger.CreateTextFileWriter("logs/text.log") textFile, err := logger.CreateTextFileWriter("logs/text.log")
if err != nil { if err != nil {
logger.Close() logger.Close()
@@ -52,6 +67,14 @@ func main() {
panic(err) panic(err)
} }
s := sneklog.NewColorStringBuilder().
AddColorRgbBg(31, 41, 40).
AddColorRgbFg(220, 215, 186).
AddAttribute(sneklog.Italic).AddAttribute(sneklog.Bold).
AddText("Some Very very very cool stuff, themed in Kanagawa colors!").String()
println(s)
fmt.Println("external buffer contents:") fmt.Println("external buffer contents:")
fmt.Println(externalBuffer.String()) fmt.Println(externalBuffer.String())
} }
+166
View File
@@ -0,0 +1,166 @@
package sneklog
import (
"fmt"
"strings"
"time"
)
// Formatter
// 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,
// %z - timezone(i.e. 0300), %Z - timezone(i.e. 03:00)
// Example: "%d.%m.%Y %H:%M:%S"
//
// TraceBackFormat: %M - method, %f - filename, %n - line number, %s - method signature, %p - full path
type Formatter struct {
Format string
MessageSeparator string
// JSONTraceBackPath
// If true, in traceback will be path to file, otherwise only filename.
JSONTraceBackPath bool
TimeStampFormat string
TraceBackFormat string
TraceBackSeparator string
ColorOutput bool
ColorOnlyStdout bool
}
var DefaultTextFormatter = &Formatter{
Format: "%t %l %N: %m",
MessageSeparator: " ",
TimeStampFormat: RFC3339,
TraceBackFormat: "%M (%f:%n %s)",
TraceBackSeparator: "->",
ColorOutput: true,
ColorOnlyStdout: true,
}
var DefaultJsonFormatter = &Formatter{
Format: "%m",
MessageSeparator: " ",
TimeStampFormat: RFC3339,
ColorOutput: false,
}
func NewFormatter() *Formatter {
return &Formatter{
Format: DefaultTextFormatter.Format,
MessageSeparator: DefaultTextFormatter.MessageSeparator,
TimeStampFormat: DefaultTextFormatter.TimeStampFormat,
TraceBackFormat: DefaultTextFormatter.TraceBackFormat,
TraceBackSeparator: DefaultTextFormatter.TraceBackSeparator,
ColorOutput: DefaultTextFormatter.ColorOutput,
ColorOnlyStdout: DefaultTextFormatter.ColorOnlyStdout,
}
}
func (f *Formatter) SetFormat(format string) *Formatter {
f.Format = format
return f
}
func (f *Formatter) SetMessageSeparator(separator string) *Formatter {
f.MessageSeparator = separator
return f
}
func (f *Formatter) SetTimeStampFormat(format string) *Formatter {
f.TimeStampFormat = format
return f
}
func (f *Formatter) SetTraceBackFormat(format string) *Formatter {
f.TraceBackFormat = format
return f
}
func (f *Formatter) SetTraceBackSeparator(separator string) *Formatter {
f.TraceBackSeparator = separator
return f
}
func (f *Formatter) SetColorOutput(color bool) *Formatter {
f.ColorOutput = color
return f
}
func (f *Formatter) SetColorOnlyStdout(only bool) *Formatter {
f.ColorOnlyStdout = only
return f
}
func (f *Formatter) FormatMessage(level LogLevel, prefix string, tb []*MethodTraceback, messages ...any) string {
if f == nil {
return fmt.Sprint(messages...)
}
output := f.Format
output = strings.ReplaceAll(output, "%t", f.FormatTime(time.Now()))
output = strings.ReplaceAll(output, "%N", prefix)
output = strings.ReplaceAll(output, "%l", level.GetName())
output = strings.ReplaceAll(output, "%L", strings.ToUpper(level.GetName()))
if len(tb) > 0 {
formattedTraceback := f.FormatTraceback(tb[0])
output = strings.ReplaceAll(output, "%b", formattedTraceback)
formattedTraceback = f.FormatTracebacks(tb)
output = strings.ReplaceAll(output, "%B", formattedTraceback)
} else {
output = strings.ReplaceAll(output, "%b", "")
output = strings.ReplaceAll(output, "%B", "")
}
if len(tb) > 0 {
output = strings.ReplaceAll(output, "%M", tb[0].Method)
output = strings.ReplaceAll(output, "%f", tb[0].Filename)
output = strings.ReplaceAll(output, "%n", fmt.Sprintf("%d", tb[0].Line))
output = strings.ReplaceAll(output, "%s", tb[0].Signature)
} else {
output = strings.ReplaceAll(output, "%M", "")
output = strings.ReplaceAll(output, "%f", "")
output = strings.ReplaceAll(output, "%n", "")
output = strings.ReplaceAll(output, "%s", "")
}
if f.JSONTraceBackPath && len(tb) > 0 {
output = strings.ReplaceAll(output, "%p", tb[0].FullPath)
} else {
output = strings.ReplaceAll(output, "%p", "")
}
message := Map(messages, func(m any) string { return fmt.Sprint(m) })
output = strings.ReplaceAll(output, "%m", strings.Join(message, f.MessageSeparator))
return output
}
func (f *Formatter) ColorizeString(s string, level LogLevel) string {
return NewColorStringBuilder().
AddFgColor(level.fg).AddBgColor(level.bg).
AddText(s).AddReset().String()
}
func (f *Formatter) FormatTime(t time.Time) string {
if f.TimeStampFormat == "" {
return t.Format(time.RFC3339)
}
return t.Format(StrftimeToGo(f.TimeStampFormat))
}
func (f *Formatter) FormatTraceback(traceback *MethodTraceback) string {
formattedTraceback := f.TraceBackFormat
if f.JSONTraceBackPath {
formattedTraceback = strings.ReplaceAll(formattedTraceback, "%p", traceback.FullPath)
} else {
formattedTraceback = strings.ReplaceAll(formattedTraceback, "%p", "")
}
formattedTraceback = strings.ReplaceAll(formattedTraceback, "%M", traceback.Method)
formattedTraceback = strings.ReplaceAll(formattedTraceback, "%f", traceback.Filename)
formattedTraceback = strings.ReplaceAll(formattedTraceback, "%n", fmt.Sprintf("%d", traceback.Line))
formattedTraceback = strings.ReplaceAll(formattedTraceback, "%s", traceback.Signature)
return formattedTraceback
}
func (f *Formatter) FormatTracebacks(traceback []*MethodTraceback) string {
var formattedTraceback []string
for _, frame := range traceback {
formattedTraceback = append(formattedTraceback, f.FormatTraceback(frame))
}
return strings.Join(formattedTraceback, f.TraceBackSeparator)
}
+1 -9
View File
@@ -1,11 +1,3 @@
module git.scuroneko.dev/scuroneko/slog module git.scuroneko.dev/scuroneko/slog/v2
go 1.26 go 1.26
require github.com/fatih/color v1.19.0
require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
golang.org/x/sys v0.43.0 // indirect
)
-15
View File
@@ -1,15 +0,0 @@
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.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/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/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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
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=
+1 -1
View File
@@ -1,4 +1,4 @@
package slog package sneklog
import ( import (
"fmt" "fmt"
+21 -72
View File
@@ -1,13 +1,10 @@
package slog package sneklog
import ( import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"strings" "strings"
"time"
"github.com/fatih/color"
) )
type replacer struct { type replacer struct {
@@ -26,8 +23,6 @@ type Logger struct {
writers []LoggerWriter writers []LoggerWriter
replacers []replacer replacers []replacer
printTraceback bool
printTime bool
jsonPretty bool jsonPretty bool
} }
@@ -35,12 +30,21 @@ type Logger struct {
type LogLevel struct { type LogLevel struct {
n uint8 n uint8
t string t string
c color.Attribute fg FgColor
bg BgColor
} }
// GetName returns the lowercase textual representation of the level. // GetName returns the lowercase textual representation of the level.
func (l *LogLevel) GetName() string { func (l *LogLevel) GetName() string { return l.t }
return l.t func (l *LogLevel) GetFgColor() FgColor { return l.fg }
func (l *LogLevel) SetFgColor(color FgColor) *LogLevel {
l.fg = color
return l
}
func (l *LogLevel) GetBgColor() BgColor { return l.bg }
func (l *LogLevel) SetBgColor(color BgColor) *LogLevel {
l.bg = color
return l
} }
// MethodTraceback describes a single stack frame attached to a log entry. // MethodTraceback describes a single stack frame attached to a log entry.
@@ -54,11 +58,11 @@ type MethodTraceback struct {
// Predefined log levels. // Predefined log levels.
var ( var (
INFO = LogLevel{n: 0, t: "info", c: color.FgWhite} INFO = LogLevel{n: 0, t: "info", fg: FgWhite}
WARN = LogLevel{n: 1, t: "warn", c: color.FgHiYellow} WARN = LogLevel{n: 1, t: "warn", fg: FgHiYellow}
ERROR = LogLevel{n: 2, t: "error", c: color.FgHiRed} ERROR = LogLevel{n: 2, t: "error", fg: FgHiRed}
FATAL = LogLevel{n: 3, t: "fatal", c: color.FgRed} FATAL = LogLevel{n: 3, t: "fatal", fg: FgRed}
DEBUG = LogLevel{n: 4, t: "debug", c: color.FgGreen} DEBUG = LogLevel{n: 4, t: "debug", fg: FgGreen}
) )
// CreateLogger creates a logger with default settings. // CreateLogger creates a logger with default settings.
@@ -66,8 +70,6 @@ func CreateLogger() *Logger {
return &Logger{ return &Logger{
prefix: "LOG", prefix: "LOG",
level: FATAL, level: FATAL,
printTraceback: false,
printTime: true,
} }
} }
@@ -83,18 +85,6 @@ func (l *Logger) Level(level LogLevel) *Logger {
return l return l
} }
// PrintTraceback enables traceback output for text writers.
func (l *Logger) PrintTraceback(b bool) *Logger {
l.printTraceback = b
return l
}
// PrintTime enables timestamps for text writers.
func (l *Logger) PrintTime(b bool) *Logger {
l.printTime = b
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
@@ -145,17 +135,17 @@ func (l *Logger) Close() error {
// CreateTextWriter wraps an external writer with the logger text settings. // CreateTextWriter wraps an external writer with the logger text settings.
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)
} }
// 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()
} }
// 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)
} }
// CreateJsonWriter wraps an external writer with the logger JSON settings. // CreateJsonWriter wraps an external writer with the logger JSON settings.
@@ -191,44 +181,3 @@ func (l *Logger) replaceAll(messages ...any) []any {
} }
return out return out
} }
// FormatTime converts time to the package text log timestamp format.
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())
}
// FormatTraceback converts a traceback frame to a compact string.
func FormatTraceback(mt *MethodTraceback) string {
return fmt.Sprintf("%s:%s:%d", mt.Filename, mt.Method, mt.Line)
}
// FormatFullTraceback joins multiple traceback frames into one string.
func FormatFullTraceback(tracebacks []*MethodTraceback) string {
formatted := make([]string, 0)
for _, tb := range tracebacks {
formatted = append(formatted, FormatTraceback(tb))
}
return strings.Join(formatted, "->")
}
// BuildString renders a text log record using the provided settings.
func BuildString(level LogLevel, prefix string, printTime, printTraceback bool, messages ...any) string {
args := []string{
fmt.Sprintf("[%s]", prefix),
fmt.Sprintf("[%s]", strings.ToUpper(level.t)),
}
if printTraceback {
args = append(args, fmt.Sprintf("[%s]", FormatTraceback(getTraceback())))
}
if printTime {
args = append(args, fmt.Sprintf("[%s]", FormatTime(time.Now())))
}
m := Map(messages, func(t any) string {
return fmt.Sprint(t)
})
s := fmt.Sprintf("%s %s", strings.Join(args, " "), strings.Join(m, " "))
return s
}
+69 -4
View File
@@ -1,4 +1,4 @@
package slog package sneklog
import ( import (
"bytes" "bytes"
@@ -17,6 +17,9 @@ type stubLoggerWriter struct {
messages []any messages []any
} }
func (w *stubLoggerWriter) Formatter() *Formatter {
return &Formatter{}
}
func (w *stubLoggerWriter) Close() error { func (w *stubLoggerWriter) Close() error {
w.closeCalls++ w.closeCalls++
return w.closeErr return w.closeErr
@@ -98,7 +101,7 @@ func TestLoggerPreservesMessageTypesWithoutReplacers(t *testing.T) {
} }
func TestCreateTextWriterCloseOnNonCloserIsNoOp(t *testing.T) { func TestCreateTextWriterCloseOnNonCloserIsNoOp(t *testing.T) {
writer := CreateTextWriter(&bytes.Buffer{}, false, false) writer := CreateTextWriter(&bytes.Buffer{})
if err := writer.Close(); err != nil { if err := writer.Close(); err != nil {
t.Fatalf("Close() error = %v", err) t.Fatalf("Close() error = %v", err)
} }
@@ -113,7 +116,7 @@ func TestCreateTextWriterDoesNotCloseExternalCloser(t *testing.T) {
_ = file.Close() _ = file.Close()
}) })
writer := CreateTextWriter(file, false, false) writer := CreateTextWriter(file)
if err := writer.Close(); err != nil { if err := writer.Close(); err != nil {
t.Fatalf("Close() error = %v", err) t.Fatalf("Close() error = %v", err)
} }
@@ -186,10 +189,72 @@ func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) {
} }
} }
func TestFormatterHandlesEmptyTracebackPlaceholders(t *testing.T) {
formatter := NewFormatter().
SetFormat("%m|%b|%B|%M|%f|%n|%s|%p")
got := formatter.FormatMessage(INFO, "TEST", nil, "hello")
if got != "hello|||||||" {
t.Fatalf("empty traceback placeholders should be empty, got %q", got)
}
}
func TestFormatterDoesNotInterpretPlaceholdersInsideMessages(t *testing.T) {
formatter := NewFormatter().
SetFormat("%L:%m")
message := "literal %L %s %p"
got := formatter.FormatMessage(INFO, "TEST", nil, message)
if got != "INFO:literal %L %s %p" {
t.Fatalf("message placeholder-looking text should stay literal, got %q", got)
}
}
func TestNewFormatterDoesNotMutateDefaultFormatter(t *testing.T) {
formatter := NewFormatter()
formatter.SetFormat("custom")
if DefaultTextFormatter.Format == "custom" {
t.Fatal("NewFormatter() should return a copy, not mutate DefaultTextFormatter")
}
}
func TestTextWriterColorOutputHonorsColorOnlyStdoutFalse(t *testing.T) {
var buf bytes.Buffer
formatter := NewFormatter().
SetFormat("%m").
SetColorOutput(true).
SetColorOnlyStdout(false)
writer := CreateTextWriter(&buf).SetFormatter(formatter)
if err := writer.Print(INFO, "TEST", nil, "hello"); err != nil {
t.Fatalf("Print() error = %v", err)
}
if !strings.Contains(buf.String(), "\x1b[") {
t.Fatalf("ColorOnlyStdout(false) should allow color for external writers, got %q", buf.String())
}
}
func TestTextWriterColorOutputCanBeDisabled(t *testing.T) {
var buf bytes.Buffer
formatter := NewFormatter().
SetFormat("%m").
SetColorOutput(false).
SetColorOnlyStdout(false)
writer := CreateTextWriter(&buf).SetFormatter(formatter)
if err := writer.Print(INFO, "TEST", nil, "hello"); err != nil {
t.Fatalf("Print() error = %v", err)
}
if strings.Contains(buf.String(), "\x1b[") {
t.Fatalf("SetColorOutput(false) should disable color output, got %q", buf.String())
}
}
func TestCreateTextStdoutWriterDoesNotCloseStdout(t *testing.T) { func TestCreateTextStdoutWriterDoesNotCloseStdout(t *testing.T) {
stdoutFile := swapStdout(t) stdoutFile := swapStdout(t)
writer := CreateTextStdoutWriter(false, false) writer := CreateTextStdoutWriter()
if err := writer.Close(); err != nil { if err := writer.Close(); err != nil {
t.Fatalf("Close() error = %v", err) t.Fatalf("Close() error = %v", err)
} }
+161
View File
@@ -0,0 +1,161 @@
package sneklog
import "strings"
const (
Layout = "%m/%d %I:%M:%S%p '%y %z"
ANSIC = "%a %b %e %H:%M:%S %Y"
UnixDate = "%a %b %e %H:%M:%S %Z %Y"
RubyDate = "%a %b %d %H:%M:%S %z %Y"
RFC822 = "%d %b %y %H:%M %Z"
RFC822Z = "%d %b %y %H:%M %z"
RFC850 = "%A, %d-%b-%y %H:%M:%S %Z"
RFC1123 = "%a, %d %b %Y %H:%M:%S %Z"
RFC1123Z = "%a, %d %b %Y %H:%M:%S %z"
RFC3339 = "%Y-%m-%dT%H:%M:%S%:z"
RFC3339Nano = "%Y-%m-%dT%H:%M:%S%#N%:z"
Kitchen = "%-I:%M%p"
Stamp = "%b %e %H:%M:%S"
StampMilli = "%b %e %H:%M:%S.%L"
StampMicro = "%b %e %H:%M:%S.%f"
StampNano = "%b %e %H:%M:%S.%N"
DateTime = "%Y-%m-%d %H:%M:%S"
DateOnly = "%Y-%m-%d"
TimeOnly = "%H:%M:%S"
)
type rule struct {
strf string
goLayout string
}
var strftimeToGoRules = []rule{
// Long/special tokens first.
{"%::z", "Z07:00:00"},
{"%:::z", "Z07"},
{"%:z", "Z07:00"},
{"%#N", ".999999999"},
{"%#f", ".999999"},
{"%#L", ".999"},
{"%-m", "1"},
{"%#m", "1"},
{"%-d", "2"},
{"%#d", "2"},
{"%-H", "15"},
{"%#H", "15"},
{"%-I", "3"},
{"%#I", "3"},
{"%-M", "4"},
{"%#M", "4"},
{"%-S", "5"},
{"%#S", "5"},
{"%%", "%"},
{"%Y", "2006"},
{"%y", "06"},
{"%B", "January"},
{"%b", "Jan"},
{"%h", "Jan"},
{"%m", "01"},
{"%d", "02"},
{"%e", "_2"},
{"%A", "Monday"},
{"%a", "Mon"},
{"%H", "15"},
{"%I", "03"},
{"%M", "04"},
{"%S", "05"},
{"%p", "PM"},
{"%z", "-0700"},
{"%Z", "MST"},
{"%N", "000000000"},
{"%f", "000000"},
{"%L", "000"},
{"%F", "2006-01-02"},
{"%T", "15:04:05"},
{"%R", "15:04"},
{"%D", "01/02/06"},
{"%r", "03:04:05 PM"},
}
func StrftimeToGo(format string) string {
var out strings.Builder
for i := 0; i < len(format); {
if format[i] != '%' {
out.WriteByte(format[i])
i++
continue
}
matched := false
for _, r := range strftimeToGoRules {
if strings.HasPrefix(format[i:], r.strf) {
out.WriteString(r.goLayout)
i += len(r.strf)
matched = true
break
}
}
if !matched {
// Можно заменить на ошибку, если хочешь strict mode.
out.WriteByte(format[i])
i++
}
}
return out.String()
}
func GoToStrftime(format string) string {
format = strings.ReplaceAll(format, "2006", "%Y")
format = strings.ReplaceAll(format, "06", "%y")
format = strings.ReplaceAll(format, "15", "%H")
format = strings.ReplaceAll(format, "03", "%I")
format = strings.ReplaceAll(format, "3", "%-I")
format = strings.ReplaceAll(format, "3", "%#I")
format = strings.ReplaceAll(format, "January", "%B")
format = strings.ReplaceAll(format, "Jan", "%b")
format = strings.ReplaceAll(format, "01", "%m")
format = strings.ReplaceAll(format, "1", "%-m")
format = strings.ReplaceAll(format, "02", "%d")
format = strings.ReplaceAll(format, "_2", "%e")
format = strings.ReplaceAll(format, "2", "%-d")
format = strings.ReplaceAll(format, "Monday", "%A")
format = strings.ReplaceAll(format, "Mon", "%a")
format = strings.ReplaceAll(format, "04", "%M")
format = strings.ReplaceAll(format, "05", "%S")
format = strings.ReplaceAll(format, ".999999999", "%#N")
format = strings.ReplaceAll(format, "000000000", "%N")
format = strings.ReplaceAll(format, ".999999", "%#f")
format = strings.ReplaceAll(format, "000000", "%f")
format = strings.ReplaceAll(format, ".999", "%#L")
format = strings.ReplaceAll(format, "000", "%L")
format = strings.ReplaceAll(format, "PM", "%p")
format = strings.ReplaceAll(format, "-0700", "%z")
format = strings.ReplaceAll(format, "MST", "%Z")
format = strings.ReplaceAll(format, "Z07:00", "%:z")
format = strings.ReplaceAll(format, "Z07:00:00", "%::z")
format = strings.ReplaceAll(format, "Z07", "%:::z")
return format
}
+1 -1
View File
@@ -1,4 +1,4 @@
package slog package sneklog
import ( import (
"runtime" "runtime"
+1 -1
View File
@@ -1,4 +1,4 @@
package slog package sneklog
// Map applies f to each element of s and returns the resulting slice. // Map applies f to each element of s and returns the resulting slice.
func Map[T, R any](s []T, f func(T) R) []R { func Map[T, R any](s []T, f func(T) R) []R {
+67 -15
View File
@@ -1,4 +1,4 @@
package slog package sneklog
import ( import (
"encoding/json" "encoding/json"
@@ -22,8 +22,18 @@ type LoggerTextWriter struct {
LoggerWriter LoggerWriter
writer io.Writer writer io.Writer
closer io.Closer closer io.Closer
printTraceback bool formatter *Formatter
printTime bool }
func (w *LoggerTextWriter) SetFormatter(formatter *Formatter) *LoggerTextWriter {
w.formatter = formatter
return w
}
func (w *LoggerTextWriter) Formatter() *Formatter {
if w.formatter == nil {
return DefaultTextFormatter
}
return w.formatter
} }
// Write forwards raw bytes to the underlying writer. // Write forwards raw bytes to the underlying writer.
@@ -32,10 +42,32 @@ func (w *LoggerTextWriter) Write(p []byte) (n int, err error) {
} }
// Print formats the provided record as text and writes it to the underlying writer. // Print formats the provided record as text and writes it to the underlying writer.
func (w *LoggerTextWriter) Print(level LogLevel, prefix string, _ []*MethodTraceback, messages ...any) error { func (w *LoggerTextWriter) Print(level LogLevel, prefix string, traceback []*MethodTraceback, messages ...any) error {
s := BuildString(level, prefix, w.printTime, w.printTraceback, messages...) msg := Map(messages, func(el any) string { return fmt.Sprint(el) })
newline := false
if len(msg) > 0 && strings.HasSuffix(msg[len(msg)-1], "\n") {
newline = true
msg[len(msg)-1] = strings.TrimSuffix(msg[len(msg)-1], "\n")
}
messages = Map(msg, func(el string) any { return any(el) })
f := w.Formatter()
s := f.FormatMessage(level, prefix, traceback, messages...)
if f.ColorOutput && (!f.ColorOnlyStdout || w.writer == os.Stdout || w.writer == os.Stderr) {
s = f.ColorizeString(s, level)
}
_, err := w.Write([]byte(s)) _, err := w.Write([]byte(s))
if err != nil {
return err return err
}
if newline {
_, err = w.Write([]byte("\n"))
if err != nil {
return err
}
}
return nil
} }
// Close closes the owned writer, if any. // Close closes the owned writer, if any.
@@ -52,14 +84,28 @@ type LoggerJsonWriter struct {
writer io.Writer writer io.Writer
closer io.Closer closer io.Closer
pretty bool pretty bool
formatter *Formatter
}
func (w *LoggerJsonWriter) Formatter() *Formatter {
if w.formatter == nil {
return DefaultJsonFormatter
}
return w.formatter
}
func (w *LoggerJsonWriter) SetFormatter(f *Formatter) *LoggerJsonWriter {
w.formatter = f
return w
} }
// LoggerJsonMessage is the JSON payload emitted by LoggerJsonWriter. // LoggerJsonMessage is the JSON payload emitted by LoggerJsonWriter.
type LoggerJsonMessage struct { type LoggerJsonMessage struct {
Time time.Time `json:"time"` Time string `json:"time"`
Level string `json:"level"` Level string `json:"level"`
Prefix string `json:"prefix"` Prefix string `json:"prefix"`
Message string `json:"message"` Message string `json:"message"`
Traceback []*MethodTraceback `json:"traceback"` Traceback []*MethodTraceback `json:"traceback"`
} }
@@ -71,18 +117,24 @@ 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 { return fmt.Sprint(el) }) msg := Map(messages, func(el any) string { return fmt.Sprint(el) })
newline := false newline := false
if len(msg) > 0 && strings.HasSuffix(msg[len(msg)-1], "\n") { if len(msg) > 0 && strings.HasSuffix(msg[len(msg)-1], "\n") {
newline = true newline = true
msg[len(msg)-1] = strings.TrimSuffix(msg[len(msg)-1], "\n") msg[len(msg)-1] = strings.TrimSuffix(msg[len(msg)-1], "\n")
} }
messages = Map(msg, func(el string) any { return any(el) })
f := w.Formatter()
s := f.FormatMessage(level, prefix, traceback, messages...)
if f.ColorOutput && (!f.ColorOnlyStdout || w.writer == os.Stdout || w.writer == os.Stderr) {
s = f.ColorizeString(s, level)
}
m := LoggerJsonMessage{ m := LoggerJsonMessage{
Time: time.Now(), Time: f.FormatTime(time.Now()),
Level: level.GetName(), Level: level.GetName(),
Prefix: prefix, Prefix: prefix,
Message: strings.TrimSpace(strings.Join(msg, " ")), Message: s,
Traceback: traceback, Traceback: traceback,
} }
var data []byte var data []byte
@@ -112,16 +164,16 @@ func (w *LoggerJsonWriter) Close() error {
// CreateTextWriter wraps an external writer for text output without taking // CreateTextWriter wraps an external writer for text output without taking
// ownership of it. // ownership of it.
func CreateTextWriter(w io.Writer, printTraceback, printTime bool) *LoggerTextWriter { func CreateTextWriter(w io.Writer) *LoggerTextWriter {
writer := &LoggerTextWriter{ writer := &LoggerTextWriter{
writer: w, printTraceback: printTraceback, printTime: printTime, writer: w,
} }
return writer return writer
} }
// CreateTextFileWriter creates a text writer for path, creating parent // CreateTextFileWriter creates a text writer for path, creating parent
// directories as needed. The returned writer owns the opened file. // directories as needed. The returned writer owns the opened file.
func CreateTextFileWriter(path string, printTraceback, printTime bool) (*LoggerTextWriter, error) { func CreateTextFileWriter(path string) (*LoggerTextWriter, error) {
err := os.MkdirAll(filepath.Dir(path), os.ModePerm) err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -130,15 +182,15 @@ func CreateTextFileWriter(path string, printTraceback, printTime bool) (*LoggerT
if err != nil { if err != nil {
return nil, err return nil, err
} }
writer := CreateTextWriter(file, printTraceback, printTime) writer := CreateTextWriter(file)
writer.closer = file writer.closer = file
return writer, nil return writer, nil
} }
// CreateTextStdoutWriter creates a text writer for os.Stdout without taking // CreateTextStdoutWriter creates a text writer for os.Stdout without taking
// ownership of stdout. // ownership of stdout.
func CreateTextStdoutWriter(printTraceback, printTime bool) *LoggerTextWriter { func CreateTextStdoutWriter() *LoggerTextWriter {
writer := CreateTextWriter(os.Stdout, printTraceback, printTime) writer := CreateTextWriter(os.Stdout)
writer.closer = nil writer.closer = nil
return writer return writer
} }