Skip to content

Commit

Permalink
Add support for custom levels and colorizing
Browse files Browse the repository at this point in the history
Users via the `LevelColorsMap` can now map their (custom)
slog.Level => name and color.

Fixes: #61
  • Loading branch information
synfinatic committed Aug 1, 2024
1 parent 368de75 commit 855df9c
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 0 deletions.
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
module github.com/lmittmann/tint

go 1.21

require github.com/fatih/color v1.17.0

require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.18.0 // indirect
)
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
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=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
44 changes: 44 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,34 @@ See [slog.HandlerOptions] for details.
}),
)
# Customize Level and Colors
Options.LevelColorsMap can be used to customize the log level and colors.
w := os.Stderr
logger := slog.New(
tint.NewHandler(w, &tint.Options{
LevelColorsMap: tint.LevelColorsMapping{
slog.LevelDebug: {
Name: "DEBUG",
Color: color.FgBlue,
},
slog.LevelInfo: {
Name: "INFO",
Color: color.FgGreen,
},
slog.LevelWarn: {
Name: "WARN",
Color: color.FgYellow,
},
slog.LevelError: {
Name: "ERROR",
Color: color.FgRed,
},
},
}),
)
# Automatically Enable Colors
Colors are enabled by default and can be disabled using the Options.NoColor
Expand Down Expand Up @@ -99,6 +127,9 @@ type Options struct {
// See https://pkg.go.dev/log/slog#HandlerOptions for details.
ReplaceAttr func(groups []string, attr slog.Attr) slog.Attr

// LevelColorsMap maps slog.Level to their LevelColor
LevelColorsMap LevelColorsMapping

// Time format (Default: time.StampMilli)
TimeFormat string

Expand Down Expand Up @@ -126,6 +157,11 @@ func NewHandler(w io.Writer, opts *Options) slog.Handler {
if opts.TimeFormat != "" {
h.timeFormat = opts.TimeFormat
}

if len(opts.LevelColorsMap) > 0 {
h.levelColors = opts.LevelColorsMap.LevelColors()
}

h.noColor = opts.NoColor
return h
}
Expand All @@ -141,6 +177,7 @@ type handler struct {

addSource bool
level slog.Leveler
levelColors *LevelColors
replaceAttr func([]string, slog.Attr) slog.Attr
timeFormat string
noColor bool
Expand All @@ -154,6 +191,7 @@ func (h *handler) clone() *handler {
w: h.w,
addSource: h.addSource,
level: h.level,
levelColors: h.levelColors.Copy(),
replaceAttr: h.replaceAttr,
timeFormat: h.timeFormat,
noColor: h.noColor,
Expand Down Expand Up @@ -283,6 +321,12 @@ func (h *handler) appendTime(buf *buffer, t time.Time) {
}

func (h *handler) appendLevel(buf *buffer, level slog.Level) {
// check if a LevelColor is defined
if levelColor := h.levelColors.LevelColor(level); levelColor != nil {
buf.WriteString(levelColor.String(!h.noColor))
return
}

switch {
case level < slog.LevelInfo:
buf.WriteString("DBG")
Expand Down
115 changes: 115 additions & 0 deletions levels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package tint

import (
"log/slog"

"github.com/fatih/color"
)

// LevelColors defines the name as displayed to the user and color of a log level.
type LevelColor struct {
// Name is the name of the log level
Name string
// Color is the color of the log level
Color color.Attribute
serialized string
colored bool
}

// String returns the level name, optionally with color applied.
func (lc *LevelColor) String(colored bool) string {
if len(lc.serialized) == 0 || lc.colored != colored {
if colored {
lc.serialized = color.New(lc.Color).SprintFunc()(lc.Name)
} else {
lc.serialized = lc.Name
}
}
return lc.serialized
}

// Copy returns a copy of the LevelColor.
func (lc *LevelColor) Copy() *LevelColor {
return &LevelColor{
Name: lc.Name,
Color: lc.Color,
serialized: lc.serialized,
colored: lc.colored,
}
}

// LevelColorsMapping is a map of log levels to their colors and is what
// the user defines in their configuration.
type LevelColorsMapping map[slog.Level]LevelColor

// min returns the mapped minimum index
func (lm *LevelColorsMapping) min() int {
idx := 1000
for check, _ := range *lm {
if int(check) < idx {
idx = int(check)
}
}
return idx
}

// size returns the size of the slice needed to store the LevelColors.
func (lm *LevelColorsMapping) size(offset int) int {
maxIdx := -1000
for check, _ := range *lm {
if int(check) > maxIdx {
maxIdx = int(check)
}
}
return offset + maxIdx + 1
}

// offset returns the index offset needed to map negative log levels.
func (lm *LevelColorsMapping) offset() int {
min := lm.min()
if min < 0 {
min = -min
}
return min
}

// LevelColors returns the LevelColors for the LevelColorsMapping.
func (lm *LevelColorsMapping) LevelColors() *LevelColors {
lcList := make([]*LevelColor, lm.size(lm.offset()))
for idx, lc := range *lm {
lcList[int(idx)+lm.offset()] = lc.Copy()
}
lc := LevelColors{
levels: lcList,
offset: lm.offset(),
}
return &lc
}

// LevelColors is our internal representation of the user-defined LevelColorsMapping.
// We map the log levels via their slog.Level to their LevelColor using an offset
// to ensure we can map negative level values to our slice.
type LevelColors struct {
levels []*LevelColor
offset int
}

// LevelColor returns the LevelColor for the given log level.
// Returns nil indicating if the log level was not found.
func (lc *LevelColors) LevelColor(level slog.Level) *LevelColor {
idx := int(level.Level()) + lc.offset
if len(lc.levels) < idx {
return &LevelColor{}
}
return lc.levels[idx]
}

// Copy returns a copy of the LevelColors.
func (lc *LevelColors) Copy() *LevelColors {
lcCopy := LevelColors{
levels: make([]*LevelColor, len(lc.levels)),
offset: lc.offset,
}
copy(lcCopy.levels, lc.levels)
return &lcCopy
}

0 comments on commit 855df9c

Please sign in to comment.