Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom levels and colorizing #70

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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=
48 changes: 48 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,13 @@ 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()
} else {
h.levelColors = &LevelColors{}
}

h.noColor = opts.NoColor
return h
}
Expand All @@ -141,6 +179,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 +193,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 +323,14 @@ 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 h.levelColors != nil {
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
125 changes: 125 additions & 0 deletions levels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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 {
if len(lc.levels) == 0 {
return nil
}

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 {
if len(lc.levels) == 0 {
return &LevelColors{
levels: []*LevelColor{},
}
}

lcCopy := LevelColors{
levels: make([]*LevelColor, len(lc.levels)),
offset: lc.offset,
}
copy(lcCopy.levels, lc.levels)
return &lcCopy
}