From 926f731a644a7a60479794936093bbb1d1552ebf Mon Sep 17 00:00:00 2001 From: lmittmann Date: Mon, 17 Mar 2025 11:11:13 +0100 Subject: [PATCH 1/3] add `tint.Err(..)` test - verify `Err(..)` behaves like `slog.Any("err", err)` with non-tint handlers --- handler_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/handler_test.go b/handler_test.go index 8fc022c..7d96ad1 100644 --- a/handler_test.go +++ b/handler_test.go @@ -511,6 +511,30 @@ func TestReplaceAttr(t *testing.T) { } } +func TestErr(t *testing.T) { + t.Run("text", func(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(slog.NewTextHandler(&buf, nil)) + logger.Info("test", tint.Err(errTest)) + + want := `time=2009-11-10T23:00:00.000Z level=INFO msg=test err=fail` + if got := strings.TrimSpace(buf.String()); want != got { + t.Fatalf("(-want +got)\n- %s\n+ %s", want, got) + } + }) + + t.Run("json", func(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(slog.NewJSONHandler(&buf, nil)) + logger.Info("test", tint.Err(errTest)) + + want := `{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"test","err":"fail"}` + if got := strings.TrimSpace(buf.String()); want != got { + t.Fatalf("(-want +got)\n- %s\n+ %s", want, got) + } + }) +} + // See https://github.com/golang/exp/blob/master/slog/benchmarks/benchmarks_test.go#L25 // // Run e.g.: From 9d56d7d374101742249cfca42dd12eac08c55948 Mon Sep 17 00:00:00 2001 From: lmittmann <3458786+lmittmann@users.noreply.github.com> Date: Mon, 19 May 2025 15:35:02 +0200 Subject: [PATCH 2/3] workflows: simplify Go checks (#92) * simplify workflow * lint using go1.24 --------- Co-authored-by: lmittmann --- .github/workflows/go.yml | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d34e75a..c6e345f 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -14,18 +14,13 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.21" - - name: go fmt - run: | - output=$(gofmt -s -d .) - echo "$output" - test -z "$output" - - name: go vet + go-version: "1.24" + - name: fmt + run: diff -u <(echo -n) <(gofmt -s -d .) + - name: vet run: go vet ./... - - name: install staticcheck - run: go install honnef.co/go/tools/cmd/staticcheck@latest - name: staticcheck - run: staticcheck ./... + run: go run honnef.co/go/tools/cmd/staticcheck@latest ./... test: name: Test @@ -35,5 +30,5 @@ jobs: - uses: actions/setup-go@v5 with: go-version: "1.21" - - name: go test + - name: test run: TZ="" go test ./... -tags=faketime From c6e7ac566b24cb5623d6a91d61e17bae6b56b3ed Mon Sep 17 00:00:00 2001 From: lmittmann <3458786+lmittmann@users.noreply.github.com> Date: Mon, 19 May 2025 16:38:39 +0200 Subject: [PATCH 3/3] add colored attributes: `tint.Attr(color, attr)` (#93) * added `tintValue` * fix time * added time test * fix non-tint handler behaviour * improved `Resolve` + improved doc * refactored test * refactor + added support for tinted level + src * buffer: dropped `WriteStringIf` * reduced allocs * fix * dropped color attr shortcuts * Attr: reduced complexity * fixed level coloring issue * doc update * changed tintValue attribute order * improved doc * refactored tests * small doc fix * cleanup * doc: switched tabs for spaces * levelTrace as const --------- Co-authored-by: lmittmann --- README.md | 45 +++++--- buffer.go | 8 +- handler.go | 278 +++++++++++++++++++++++++++++++++--------------- handler_test.go | 254 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 456 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 46ba30a..07f1ad6 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ go get github.com/lmittmann/tint ```go w := os.Stderr -// create a new logger +// Create a new logger logger := slog.New(tint.NewHandler(w, nil)) -// set global logger with custom options +// Set global logger with custom options slog.SetDefault(slog.New( tint.NewHandler(w, &tint.Options{ Level: slog.LevelDebug, @@ -46,7 +46,26 @@ each non-group attribute before it is logged. See [`slog.HandlerOptions`](https: for details. ```go -// create a new logger that doesn't write the time +// Create a new logger with a custom TRACE level: +const LevelTrace = slog.LevelDebug - 4 + +w := os.Stderr +logger := slog.New(tint.NewHandler(w, &tint.Options{ + Level: LevelTrace, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey && len(groups) == 0 { + level, ok := a.Value.Any().(slog.Level) + if ok && level <= LevelTrace { + return tint.Attr(13, slog.String(a.Key, "TRC")) + } + } + return a + }, +})) +``` + +```go +// Create a new logger that doesn't write the time w := os.Stderr logger := slog.New( tint.NewHandler(w, &tint.Options{ @@ -61,15 +80,15 @@ logger := slog.New( ``` ```go -// create a new logger that writes all errors in red +// Create a new logger that writes all errors in red w := os.Stderr logger := slog.New( tint.NewHandler(w, &tint.Options{ ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - if err, ok := a.Value.Any().(error); ok { - aErr := tint.Err(err) - aErr.Key = a.Key - return aErr + if a.Value.Kind() == slog.KindAny { + if _, ok := a.Value.Any().(error); ok { + return tint.Attr(9, a) + } } return a }, @@ -79,9 +98,9 @@ logger := slog.New( ### Automatically Enable Colors -Colors are enabled by default and can be disabled using the `Options.NoColor` -attribute. To automatically enable colors based on the terminal capabilities, -use e.g. the [`go-isatty`](https://github.com/mattn/go-isatty) package. +Colors are enabled by default. Use the `Options.NoColor` field to disable +color output. To automatically enable colors based on terminal capabilities, use +e.g., the [`go-isatty`](https://github.com/mattn/go-isatty) package: ```go w := os.Stderr @@ -94,8 +113,8 @@ logger := slog.New( ### Windows Support -Color support on Windows can be added by using e.g. the -[`go-colorable`](https://github.com/mattn/go-colorable) package. +Color support on Windows can be added by using e.g., the +[`go-colorable`](https://github.com/mattn/go-colorable) package: ```go w := os.Stderr diff --git a/buffer.go b/buffer.go index 4d7321a..93668fc 100644 --- a/buffer.go +++ b/buffer.go @@ -23,6 +23,7 @@ func (b *buffer) Free() { bufPool.Put(b) } } + func (b *buffer) Write(bytes []byte) (int, error) { *b = append(*b, bytes...) return len(bytes), nil @@ -37,10 +38,3 @@ func (b *buffer) WriteString(str string) (int, error) { *b = append(*b, str...) return len(str), nil } - -func (b *buffer) WriteStringIf(ok bool, str string) (int, error) { - if !ok { - return 0, nil - } - return b.WriteString(str) -} diff --git a/handler.go b/handler.go index 8d3f340..fb7203b 100644 --- a/handler.go +++ b/handler.go @@ -12,6 +12,24 @@ Options.ReplaceAttr can be used to alter or drop attributes. If set, it is called on each non-group attribute before it is logged. See [slog.HandlerOptions] for details. +Create a new logger with a custom TRACE level: + + const LevelTrace = slog.LevelDebug - 4 + + w := os.Stderr + logger := slog.New(tint.NewHandler(w, &tint.Options{ + Level: LevelTrace, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey && len(groups) == 0 { + level, ok := a.Value.Any().(slog.Level) + if ok && level <= LevelTrace { + return tint.Attr(13, slog.String(a.Key, "TRC")) + } + } + return a + }, + })) + Create a new logger that doesn't write the time: w := os.Stderr @@ -32,10 +50,10 @@ Create a new logger that writes all errors in red: logger := slog.New( tint.NewHandler(w, &tint.Options{ ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - if err, ok := a.Value.Any().(error); ok { - aErr := tint.Err(err) - aErr.Key = a.Key - return aErr + if a.Value.Kind() == slog.KindAny { + if _, ok := a.Value.Any().(error); ok { + return tint.Attr(9, a) + } } return a }, @@ -44,9 +62,9 @@ Create a new logger that writes all errors in red: # Automatically Enable Colors -Colors are enabled by default and can be disabled using the Options.NoColor -attribute. To automatically enable colors based on the terminal capabilities, -use e.g. the [go-isatty] package. +Colors are enabled by default. Use the Options.NoColor field to disable +color output. To automatically enable colors based on terminal capabilities, use +e.g., the [go-isatty] package: w := os.Stderr logger := slog.New( @@ -57,7 +75,7 @@ use e.g. the [go-isatty] package. # Windows Support -Color support on Windows can be added by using e.g. the [go-colorable] package. +Color support on Windows can be added by using e.g., the [go-colorable] package: w := os.Stderr logger := slog.New( @@ -89,20 +107,19 @@ import ( // ANSI modes const ( - ansiReset = "\u001b[0m" - ansiFaint = "\u001b[2m" - ansiResetFaint = "\u001b[22m" - ansiBrightRed = "\u001b[91m" - ansiBrightGreen = "\u001b[92m" - ansiBrightYellow = "\u001b[93m" - ansiBrightRedFaint = "\u001b[91;2m" + ansiReset = "\u001b[0m" + ansiFaint = "\u001b[2m" + ansiResetFaint = "\u001b[22m" + ansiBrightRed = "\u001b[91m" + ansiBrightGreen = "\u001b[92m" + ansiBrightYellow = "\u001b[93m" ansiEsc = '\u001b' ) const errKey = "err" -var ( +const ( defaultLevel = slog.LevelInfo defaultTimeFormat = time.StampMilli ) @@ -198,13 +215,14 @@ func (h *handler) Handle(_ context.Context, r slog.Record) error { if !r.Time.IsZero() { val := r.Time.Round(0) // strip monotonic to match Attr behavior if rep == nil { - h.appendTime(buf, r.Time) + h.appendTintTime(buf, r.Time, -1) buf.WriteByte(' ') } else if a := rep(nil /* groups */, slog.Time(slog.TimeKey, val)); a.Key != "" { - if a.Value.Kind() == slog.KindTime { - h.appendTime(buf, a.Value.Time()) + val, color := h.resolve(a.Value) + if val.Kind() == slog.KindTime { + h.appendTintTime(buf, val.Time(), color) } else { - h.appendValue(buf, a.Value, false) + h.appendTintValue(buf, val, false, color, true) } buf.WriteByte(' ') } @@ -212,10 +230,19 @@ func (h *handler) Handle(_ context.Context, r slog.Record) error { // write level if rep == nil { - h.appendLevel(buf, r.Level) + h.appendTintLevel(buf, r.Level, -1) buf.WriteByte(' ') } else if a := rep(nil /* groups */, slog.Any(slog.LevelKey, r.Level)); a.Key != "" { - h.appendValue(buf, a.Value, false) + val, color := h.resolve(a.Value) + if val.Kind() == slog.KindAny { + if lvlVal, ok := val.Any().(slog.Level); ok { + h.appendTintLevel(buf, lvlVal, color) + } else { + h.appendTintValue(buf, val, false, color, false) + } + } else { + h.appendTintValue(buf, val, false, color, false) + } buf.WriteByte(' ') } @@ -231,10 +258,17 @@ func (h *handler) Handle(_ context.Context, r slog.Record) error { } if rep == nil { - h.appendSource(buf, src) + if h.noColor { + appendSource(buf, src) + } else { + buf.WriteString(ansiFaint) + appendSource(buf, src) + buf.WriteString(ansiReset) + } buf.WriteByte(' ') } else if a := rep(nil /* groups */, slog.Any(slog.SourceKey, src)); a.Key != "" { - h.appendValue(buf, a.Value, false) + val, color := h.resolve(a.Value) + h.appendTintValue(buf, val, false, color, true) buf.WriteByte(' ') } } @@ -245,7 +279,8 @@ func (h *handler) Handle(_ context.Context, r slog.Record) error { buf.WriteString(r.Message) buf.WriteByte(' ') } else if a := rep(nil /* groups */, slog.String(slog.MessageKey, r.Message)); a.Key != "" { - h.appendValue(buf, a.Value, false) + val, color := h.resolve(a.Value) + h.appendTintValue(buf, val, false, color, false) buf.WriteByte(' ') } @@ -299,73 +334,92 @@ func (h *handler) WithGroup(name string) slog.Handler { return h2 } -func (h *handler) appendTime(buf *buffer, t time.Time) { - buf.WriteStringIf(!h.noColor, ansiFaint) - *buf = t.AppendFormat(*buf, h.timeFormat) - buf.WriteStringIf(!h.noColor, ansiReset) +func (h *handler) appendTintTime(buf *buffer, t time.Time, color int16) { + if h.noColor { + *buf = t.AppendFormat(*buf, h.timeFormat) + } else { + if color >= 0 { + appendAnsi(buf, uint8(color), true) + } else { + buf.WriteString(ansiFaint) + } + *buf = t.AppendFormat(*buf, h.timeFormat) + buf.WriteString(ansiReset) + } } -func (h *handler) appendLevel(buf *buffer, level slog.Level) { +func (h *handler) appendTintLevel(buf *buffer, level slog.Level, color int16) { + str := func(base string, val slog.Level) []byte { + if val == 0 { + return []byte(base) + } else if val > 0 { + return strconv.AppendInt(append([]byte(base), '+'), int64(val), 10) + } + return strconv.AppendInt([]byte(base), int64(val), 10) + } + + if !h.noColor { + if color >= 0 { + appendAnsi(buf, uint8(color), false) + } else { + switch { + case level < slog.LevelInfo: + case level < slog.LevelWarn: + buf.WriteString(ansiBrightGreen) + case level < slog.LevelError: + buf.WriteString(ansiBrightYellow) + default: + buf.WriteString(ansiBrightRed) + } + } + } + switch { case level < slog.LevelInfo: - buf.WriteString("DBG") - appendLevelDelta(buf, level-slog.LevelDebug) + buf.Write(str("DBG", level-slog.LevelDebug)) case level < slog.LevelWarn: - buf.WriteStringIf(!h.noColor, ansiBrightGreen) - buf.WriteString("INF") - appendLevelDelta(buf, level-slog.LevelInfo) - buf.WriteStringIf(!h.noColor, ansiReset) + buf.Write(str("INF", level-slog.LevelInfo)) case level < slog.LevelError: - buf.WriteStringIf(!h.noColor, ansiBrightYellow) - buf.WriteString("WRN") - appendLevelDelta(buf, level-slog.LevelWarn) - buf.WriteStringIf(!h.noColor, ansiReset) + buf.Write(str("WRN", level-slog.LevelWarn)) default: - buf.WriteStringIf(!h.noColor, ansiBrightRed) - buf.WriteString("ERR") - appendLevelDelta(buf, level-slog.LevelError) - buf.WriteStringIf(!h.noColor, ansiReset) + buf.Write(str("ERR", level-slog.LevelError)) } -} -func appendLevelDelta(buf *buffer, delta slog.Level) { - if delta == 0 { - return - } else if delta > 0 { - buf.WriteByte('+') + if !h.noColor && level >= slog.LevelInfo { + buf.WriteString(ansiReset) } - *buf = strconv.AppendInt(*buf, int64(delta), 10) } -func (h *handler) appendSource(buf *buffer, src *slog.Source) { +func appendSource(buf *buffer, src *slog.Source) { dir, file := filepath.Split(src.File) - buf.WriteStringIf(!h.noColor, ansiFaint) buf.WriteString(filepath.Join(filepath.Base(dir), file)) buf.WriteByte(':') - buf.WriteString(strconv.Itoa(src.Line)) - buf.WriteStringIf(!h.noColor, ansiReset) + *buf = strconv.AppendInt(*buf, int64(src.Line), 10) +} + +func (h *handler) resolve(val slog.Value) (resolvedVal slog.Value, color int16) { + if !h.noColor && val.Kind() == slog.KindLogValuer { + if tintVal, ok := val.Any().(tintValue); ok { + return tintVal.Value.Resolve(), int16(tintVal.Color) + } + } + return val.Resolve(), -1 } func (h *handler) appendAttr(buf *buffer, attr slog.Attr, groupsPrefix string, groups []string) { - attr.Value = attr.Value.Resolve() + var color int16 // -1 if no color + attr.Value, color = h.resolve(attr.Value) if rep := h.replaceAttr; rep != nil && attr.Value.Kind() != slog.KindGroup { attr = rep(groups, attr) - attr.Value = attr.Value.Resolve() + attr.Value, color = h.resolve(attr.Value) } if attr.Equal(slog.Attr{}) { return } - switch attr.Value.Kind() { - case slog.KindAny: - if err, ok := attr.Value.Any().(tintError); ok { - h.appendTintError(buf, err, attr.Key, groupsPrefix) - buf.WriteByte(' ') - return - } - case slog.KindGroup: + if attr.Value.Kind() == slog.KindGroup { if attr.Key != "" { groupsPrefix += attr.Key + "." groups = append(groups, attr.Key) @@ -376,16 +430,29 @@ func (h *handler) appendAttr(buf *buffer, attr slog.Attr, groupsPrefix string, g return } - h.appendKey(buf, attr.Key, groupsPrefix) - h.appendValue(buf, attr.Value, true) + if h.noColor { + h.appendKey(buf, attr.Key, groupsPrefix) + h.appendValue(buf, attr.Value, true) + } else { + if color >= 0 { + appendAnsi(buf, uint8(color), true) + h.appendKey(buf, attr.Key, groupsPrefix) + buf.WriteString(ansiResetFaint) + h.appendValue(buf, attr.Value, true) + buf.WriteString(ansiReset) + } else { + buf.WriteString(ansiFaint) + h.appendKey(buf, attr.Key, groupsPrefix) + buf.WriteString(ansiReset) + h.appendValue(buf, attr.Value, true) + } + } buf.WriteByte(' ') } func (h *handler) appendKey(buf *buffer, key, groups string) { - buf.WriteStringIf(!h.noColor, ansiFaint) appendString(buf, groups+key, true, !h.noColor) buf.WriteByte('=') - buf.WriteStringIf(!h.noColor, ansiReset) } func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) { @@ -424,8 +491,6 @@ func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) { }() switch cv := v.Any().(type) { - case slog.Level: - h.appendLevel(buf, cv) case encoding.TextMarshaler: data, err := cv.MarshalText() if err != nil { @@ -433,20 +498,43 @@ func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) { } appendString(buf, string(data), quote, !h.noColor) case *slog.Source: - h.appendSource(buf, cv) + appendSource(buf, cv) default: appendString(buf, fmt.Sprintf("%+v", cv), quote, !h.noColor) } } } -func (h *handler) appendTintError(buf *buffer, err tintError, attrKey, groupsPrefix string) { - buf.WriteStringIf(!h.noColor, ansiBrightRedFaint) - appendString(buf, groupsPrefix+attrKey, true, !h.noColor) - buf.WriteByte('=') - buf.WriteStringIf(!h.noColor, ansiResetFaint) - appendString(buf, err.Error(), true, !h.noColor) - buf.WriteStringIf(!h.noColor, ansiReset) +func (h *handler) appendTintValue(buf *buffer, val slog.Value, quote bool, color int16, faint bool) { + if h.noColor { + h.appendValue(buf, val, quote) + } else { + if color >= 0 { + appendAnsi(buf, uint8(color), faint) + } else if faint { + buf.WriteString(ansiFaint) + } + h.appendValue(buf, val, quote) + if color >= 0 || faint { + buf.WriteString(ansiReset) + } + } +} + +func appendAnsi(buf *buffer, color uint8, faint bool) { + buf.WriteString("\u001b[") + if faint { + buf.WriteString("2;") + } + if color < 8 { + *buf = strconv.AppendUint(*buf, uint64(color)+30, 10) + } else if color < 16 { + *buf = strconv.AppendUint(*buf, uint64(color)+82, 10) + } else { + buf.WriteString("38;5;") + *buf = strconv.AppendUint(*buf, uint64(color), 10) + } + buf.WriteByte('m') } func appendString(buf *buffer, s string, quote, color bool) { @@ -621,15 +709,37 @@ var safeSet = [utf8.RuneSelf]bool{ '\u001b': true, } -type tintError struct{ error } +type tintValue struct { + slog.Value + Color uint8 +} + +// LogValue implements the [slog.LogValuer] interface. +func (v tintValue) LogValue() slog.Value { + return v.Value +} // Err returns a tinted (colorized) [slog.Attr] that will be written in red color // by the [tint.Handler]. When used with any other [slog.Handler], it behaves as // // slog.Any("err", err) func Err(err error) slog.Attr { - if err != nil { - err = tintError{err} - } - return slog.Any(errKey, err) + return Attr(9, slog.Any(errKey, err)) +} + +// Attr returns a tinted (colorized) [slog.Attr] that will be written in the +// specified color by the [tint.Handler]. When used with any other [slog.Handler], it behaves as a +// plain [slog.Attr]. +// +// Use the uint8 color value to specify the color of the attribute: +// +// - 0-7: standard ANSI colors +// - 8-15: high intensity ANSI colors +// - 16-231: 216 colors (6×6×6 cube) +// - 232-255: grayscale from dark to light in 24 steps +// +// See https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit +func Attr(color uint8, attr slog.Attr) slog.Attr { + attr.Value = slog.AnyValue(tintValue{attr.Value, color}) + return attr } diff --git a/handler_test.go b/handler_test.go index 7d96ad1..8a24150 100644 --- a/handler_test.go +++ b/handler_test.go @@ -19,15 +19,53 @@ import ( var faketime = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) func Example() { - slog.SetDefault(slog.New(tint.NewHandler(os.Stderr, &tint.Options{ + w := os.Stderr + logger := slog.New(tint.NewHandler(w, &tint.Options{ Level: slog.LevelDebug, TimeFormat: time.Kitchen, - }))) + })) + logger.Info("Starting server", "addr", ":8080", "env", "production") + logger.Debug("Connected to DB", "db", "myapp", "host", "localhost:5432") + logger.Warn("Slow request", "method", "GET", "path", "/users", "duration", 497*time.Millisecond) + logger.Error("DB connection lost", tint.Err(errors.New("connection reset")), "db", "myapp") + // Output: +} - slog.Info("Starting server", "addr", ":8080", "env", "production") - slog.Debug("Connected to DB", "db", "myapp", "host", "localhost:5432") - slog.Warn("Slow request", "method", "GET", "path", "/users", "duration", 497*time.Millisecond) - slog.Error("DB connection lost", tint.Err(errors.New("connection reset")), "db", "myapp") +// Create a new logger that writes all errors in red: +func Example_redErrors() { + w := os.Stderr + logger := slog.New(tint.NewHandler(w, &tint.Options{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Value.Kind() == slog.KindAny { + if _, ok := a.Value.Any().(error); ok { + return tint.Attr(9, a) + } + } + return a + }, + })) + logger.Error("DB connection lost", "error", errors.New("connection reset"), "db", "myapp") + // Output: +} + +// Create a new logger with a custom TRACE level: +func Example_traceLevel() { + const LevelTrace = slog.LevelDebug - 4 + + w := os.Stderr + logger := slog.New(tint.NewHandler(w, &tint.Options{ + Level: LevelTrace, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey && len(groups) == 0 { + level, ok := a.Value.Any().(slog.Level) + if ok && level <= LevelTrace { + return tint.Attr(13, slog.String(a.Key, "TRC")) + } + } + return a + }, + })) + logger.Log(context.Background(), LevelTrace, "DB query", "query", "SELECT * FROM users", "duration", 543*time.Microsecond) // Output: } @@ -100,7 +138,7 @@ func TestHandler(t *testing.T) { F: func(l *slog.Logger) { l.Info("test", "key", "val") }, - Want: `Nov 10 23:00:00.000 INF tint/handler_test.go:101 test key=val`, + Want: `Nov 10 23:00:00.000 INF tint/handler_test.go:139 test key=val`, }, { Opts: &tint.Options{ @@ -257,6 +295,168 @@ func TestHandler(t *testing.T) { }, Want: `Nov 10 23:00:00.000 INF test ""=""`, }, + { + Opts: &tint.Options{ + TimeFormat: time.DateOnly, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if len(groups) == 0 && a.Key == slog.TimeKey { + return slog.Time(slog.TimeKey, a.Value.Time().Add(24*time.Hour)) + } + return a + }, + NoColor: true, + }, + F: func(l *slog.Logger) { + l.Info("test") + }, + Want: `2009-11-11 INF test`, + }, + { + F: func(l *slog.Logger) { + l.Info("test", "lvl", slog.LevelWarn) + }, + Want: `Nov 10 23:00:00.000 INF test lvl=WARN`, + }, + { + Opts: &tint.Options{NoColor: false}, + F: func(l *slog.Logger) { + l.Info("test", "lvl", slog.LevelWarn) + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m test \033[2mlvl=\033[0mWARN", + }, + { + Opts: &tint.Options{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + return tint.Attr(13, a) + }, + }, + F: func(l *slog.Logger) { + l.Info("test") + }, + Want: "\033[2;95mNov 10 23:00:00.000\033[0m \033[95mINF\033[0m \033[95mtest\033[0m", + }, + { + Opts: &tint.Options{NoColor: false}, + F: func(l *slog.Logger) { + l.Error("test", tint.Err(errors.New("fail"))) + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m \033[91mERR\033[0m test \033[2;91merr=\033[22mfail\033[0m", + }, + { + Opts: &tint.Options{NoColor: false}, + F: func(l *slog.Logger) { + l.Info("test", tint.Attr(10, slog.String("key", "value"))) + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m test \033[2;92mkey=\033[22mvalue\033[0m", + }, + { + Opts: &tint.Options{NoColor: false}, + F: func(l *slog.Logger) { + l.Info("test", tint.Attr(226, slog.String("key", "value"))) + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m test \033[2;38;5;226mkey=\033[22mvalue\033[0m", + }, + { + Opts: &tint.Options{ + NoColor: false, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.MessageKey && len(groups) == 0 { + return tint.Attr(10, a) + } + return a + }, + }, + F: func(l *slog.Logger) { + l.Info("test", "key", "value") + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m \033[92mtest\033[0m \033[2mkey=\033[0mvalue", + }, + { + Opts: &tint.Options{ + NoColor: false, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey && len(groups) == 0 { + return tint.Attr(10, a) + } + return a + }, + }, + F: func(l *slog.Logger) { + l.Info("test", "key", "value") + }, + Want: "\033[2;92mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m test \033[2mkey=\033[0mvalue", + }, + { + Opts: &tint.Options{ + NoColor: false, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey && len(groups) == 0 { + return tint.Attr(10, slog.String(a.Key, a.Value.Time().Format(time.StampMilli))) + } + return a + }, + }, + F: func(l *slog.Logger) { + l.Info("test", "key", "value") + }, + Want: "\033[2;92mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m test \033[2mkey=\033[0mvalue", + }, + { + Opts: &tint.Options{ + AddSource: true, + NoColor: false, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.SourceKey && len(groups) == 0 { + return tint.Attr(10, a) + } + return a + }, + }, + F: func(l *slog.Logger) { + l.Info("test") + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m \033[2;92mtint/handler_test.go:416\033[0m test", + }, + { + Opts: &tint.Options{ + NoColor: false, + Level: slog.LevelDebug - 4, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey && len(groups) == 0 { + level, ok := a.Value.Any().(slog.Level) + if ok && level <= slog.LevelDebug-4 { + return slog.String(a.Key, "TRC") + } + } + return a + }, + }, + F: func(l *slog.Logger) { + const levelTrace = slog.LevelDebug - 4 + l.Log(context.TODO(), levelTrace, "test") + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m TRC test", + }, + { + Opts: &tint.Options{ + NoColor: false, + Level: slog.LevelDebug - 4, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey && len(groups) == 0 { + level, ok := a.Value.Any().(slog.Level) + if ok && level <= slog.LevelDebug-4 { + return tint.Attr(13, slog.String(a.Key, "TRC")) + } + } + return a + }, + }, + F: func(l *slog.Logger) { + const levelTrace = slog.LevelDebug - 4 + l.Log(context.TODO(), levelTrace, "test") + }, + Want: "\033[2mNov 10 23:00:00.000\033[0m \033[95mTRC\033[0m test", + }, { // https://github.com/lmittmann/tint/issues/8 F: func(l *slog.Logger) { @@ -345,7 +545,7 @@ func TestHandler(t *testing.T) { F: func(l *slog.Logger) { l.Info("test") }, - Want: `Nov 10 23:00:00.000 INF tint/handler_test.go:346 test`, + Want: `Nov 10 23:00:00.000 INF tint/handler_test.go:546 test`, }, { // https://github.com/lmittmann/tint/issues/44 F: func(l *slog.Logger) { @@ -364,36 +564,28 @@ func TestHandler(t *testing.T) { Want: `Nov 10 23:00:00.000 INF test key="{A:123 B:}"`, }, { // https://github.com/lmittmann/tint/issues/59 - Opts: &tint.Options{ - NoColor: false, - }, + Opts: &tint.Options{NoColor: false}, F: func(l *slog.Logger) { l.Info("test", "color", "\033[92mgreen\033[0m") }, Want: "\033[2mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m test \033[2mcolor=\033[0m\033[92mgreen\033[0m", }, { - Opts: &tint.Options{ - NoColor: false, - }, + Opts: &tint.Options{NoColor: false}, F: func(l *slog.Logger) { l.Info("test", "color", "\033[92mgreen quoted\033[0m") }, Want: "\033[2mNov 10 23:00:00.000\033[0m \033[92mINF\033[0m test \033[2mcolor=\033[0m\"\033[92mgreen quoted\033[0m\"", }, { - Opts: &tint.Options{ - NoColor: true, - }, + Opts: &tint.Options{NoColor: true}, F: func(l *slog.Logger) { l.Info("test", "color", "\033[92mgreen\033[0m") }, Want: `Nov 10 23:00:00.000 INF test color=green`, }, { - Opts: &tint.Options{ - NoColor: true, - }, + Opts: &tint.Options{NoColor: true}, F: func(l *slog.Logger) { l.Info("test", "color", "\033[92mgreen quoted\033[0m") }, @@ -511,13 +703,17 @@ func TestReplaceAttr(t *testing.T) { } } -func TestErr(t *testing.T) { +func TestAttr(t *testing.T) { + if !faketime.Equal(time.Now()) { + t.Skip(`skipping test; run with "-tags=faketime"`) + } + t.Run("text", func(t *testing.T) { var buf bytes.Buffer logger := slog.New(slog.NewTextHandler(&buf, nil)) - logger.Info("test", tint.Err(errTest)) + logger.Info("test", tint.Attr(10, slog.String("key", "val"))) - want := `time=2009-11-10T23:00:00.000Z level=INFO msg=test err=fail` + want := `time=2009-11-10T23:00:00.000Z level=INFO msg=test key=val` if got := strings.TrimSpace(buf.String()); want != got { t.Fatalf("(-want +got)\n- %s\n+ %s", want, got) } @@ -526,9 +722,9 @@ func TestErr(t *testing.T) { t.Run("json", func(t *testing.T) { var buf bytes.Buffer logger := slog.New(slog.NewJSONHandler(&buf, nil)) - logger.Info("test", tint.Err(errTest)) + logger.Info("test", tint.Attr(10, slog.String("key", "val"))) - want := `{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"test","err":"fail"}` + want := `{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"test","key":"val"}` if got := strings.TrimSpace(buf.String()); want != got { t.Fatalf("(-want +got)\n- %s\n+ %s", want, got) } @@ -651,6 +847,14 @@ func BenchmarkLogAttrs(b *testing.B) { ) }, }, + { + "attr", + func(logger *slog.Logger) { + logger.LogAttrs(context.TODO(), slog.LevelError, testMessage, + tint.Attr(9, slog.String("string", testString)), + ) + }, + }, } for _, h := range handler {