Skip to content

Commit

Permalink
Added ability to customize color of different log elements
Browse files Browse the repository at this point in the history
  • Loading branch information
topi314 committed Nov 4, 2023
1 parent 2027ee7 commit 49b10b1
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 28 deletions.
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,53 @@ slog.SetDefault(slog.New(
))
```

### Customize Colors

Colors can be customized using the `Options.LevelColors` and `Options.Colors` attributes.
See [`tint.Options`](https://pkg.go.dev/github.com/lmittmann/tint#Options) for details.

```go
// ANSI escape codes: https://en.wikipedia.org/wiki/ANSI_escape_code
const (
ansiFaint = "\033[2m"
ansiBrightRed = "\033[91m"
ansiBrightRedFaint = "\033[91;2m"
ansiBrightGreen = "\033[92m"
ansiBrightYellow = "\033[93m"
ansiBrightYellowBold = "\033[93;1m"
ansiBrightBlueBold = "\033[94;1m"
ansiBrightMagenta = "\033[95m"
)

// create a new logger with custom colors
w := os.Stderr
logger := slog.New(
tint.NewHandler(w, &tint.Options{
LevelColors: map[slog.Level]string{
slog.LevelDebug: ansiBrightMagenta,
slog.LevelInfo: ansiBrightGreen,
slog.LevelWarn: ansiBrightYellow,
slog.LevelError: ansiBrightRed,
},
Colors: map[tint.Kind]string{
tint.KindTime: ansiBrightYellowBold,
tint.KindSourceFile: ansiFaint,
tint.KindSourceSeparator: ansiFaint,
tint.KindSourceLine: ansiFaint,
tint.KindMessage: ansiBrightBlueBold,
tint.KindGroup: ansiFaint,
tint.KindKey: ansiFaint,
tint.KindSeparator: ansiFaint,
tint.KindValue: ansiBrightBlueBold,
tint.KindErrorKey: ansiBrightRedFaint,
tint.KindErrorSeparator: ansiBrightRedFaint,
tint.KindErrorValue: ansiBrightRed,
},
}),
)
```


### Customize Attributes

`ReplaceAttr` can be used to alter or drop attributes. If set, it is called on
Expand Down
162 changes: 134 additions & 28 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,50 @@ and [slog.TextHandler].
The output format can be customized using [Options], which is a drop-in
replacement for [slog.HandlerOptions].
# Customizing Colors
Colors can be customized using the `Options.LevelColors` and `Options.Colors` attributes.
See [tint.Options] for details.
// ANSI escape codes: https://en.wikipedia.org/wiki/ANSI_escape_code
const (
ansiFaint = "\033[2m"
ansiBrightRed = "\033[91m"
ansiBrightRedFaint = "\033[91;2m"
ansiBrightGreen = "\033[92m"
ansiBrightYellow = "\033[93m"
ansiBrightYellowBold = "\033[93;1m"
ansiBrightBlueBold = "\033[94;1m"
ansiBrightMagenta = "\033[95m"
)
// create a new logger with custom colors
w := os.Stderr
logger := slog.New(
tint.NewHandler(w, &tint.Options{
LevelColors: map[slog.Level]string{
slog.LevelDebug: ansiBrightMagenta,
slog.LevelInfo: ansiBrightGreen,
slog.LevelWarn: ansiBrightYellow,
slog.LevelError: ansiBrightRed,
},
Colors: map[tint.Kind]string{
tint.KindTime: ansiBrightYellowBold,
tint.KindSourceFile: ansiFaint,
tint.KindSourceSeparator: ansiFaint,
tint.KindSourceLine: ansiFaint,
tint.KindMessage: ansiBrightBlueBold,
tint.KindGroup: ansiFaint,
tint.KindKey: ansiFaint,
tint.KindSeparator: ansiFaint,
tint.KindValue: ansiBrightBlueBold,
tint.KindErrorKey: ansiBrightRedFaint,
tint.KindErrorSeparator: ansiBrightRedFaint,
tint.KindErrorValue: ansiBrightRed,
},
}),
)
# Customize Attributes
Options.ReplaceAttr can be used to alter or drop attributes. If set, it is
Expand Down Expand Up @@ -70,7 +114,6 @@ import (
const (
ansiReset = "\033[0m"
ansiFaint = "\033[2m"
ansiResetFaint = "\033[22m"
ansiBrightRed = "\033[91m"
ansiBrightGreen = "\033[92m"
ansiBrightYellow = "\033[93m"
Expand All @@ -80,8 +123,45 @@ const (
const errKey = "err"

var (
defaultLevel = slog.LevelInfo
defaultTimeFormat = time.StampMilli
defaultLevel = slog.LevelInfo
defaultTimeFormat = time.StampMilli
defaultLevelColors = map[slog.Level]string{
slog.LevelDebug: ansiFaint,
slog.LevelInfo: ansiBrightGreen,
slog.LevelWarn: ansiBrightYellow,
slog.LevelError: ansiBrightRed,
}
defaultColors = map[Kind]string{
KindTime: ansiFaint,
KindSourceFile: ansiFaint,
KindSourceSeparator: ansiFaint,
KindSourceLine: ansiFaint,
KindMessage: "",
KindGroup: ansiFaint,
KindKey: ansiFaint,
KindSeparator: ansiFaint,
KindValue: "",
KindErrorKey: ansiBrightRedFaint,
KindErrorSeparator: ansiBrightRedFaint,
KindErrorValue: ansiBrightRed,
}
)

type Kind int

const (
KindTime Kind = iota
KindSourceFile
KindSourceSeparator
KindSourceLine
KindMessage
KindGroup
KindKey
KindSeparator
KindValue
KindErrorKey
KindErrorSeparator
KindErrorValue
)

// Options for a slog.Handler that writes tinted logs. A zero Options consists
Expand All @@ -104,15 +184,23 @@ type Options struct {

// Disable color (Default: false)
NoColor bool

// Level colors (Default: debug: faint, info: green, warn: yellow, error: red)
LevelColors map[slog.Level]string

// Colors of certain parts of the log message
Colors map[Kind]string
}

// NewHandler creates a [slog.Handler] that writes tinted logs to Writer w,
// using the default options. If opts is nil, the default options are used.
func NewHandler(w io.Writer, opts *Options) slog.Handler {
h := &handler{
w: w,
level: defaultLevel,
timeFormat: defaultTimeFormat,
w: w,
level: defaultLevel,
timeFormat: defaultTimeFormat,
levelColors: defaultLevelColors,
colors: defaultColors,
}
if opts == nil {
return h
Expand All @@ -127,6 +215,12 @@ func NewHandler(w io.Writer, opts *Options) slog.Handler {
h.timeFormat = opts.TimeFormat
}
h.noColor = opts.NoColor
if opts.LevelColors != nil {
h.levelColors = opts.LevelColors
}
if opts.Colors != nil {
h.colors = opts.Colors
}
return h
}

Expand All @@ -144,6 +238,8 @@ type handler struct {
replaceAttr func([]string, slog.Attr) slog.Attr
timeFormat string
noColor bool
levelColors map[slog.Level]string
colors map[Kind]string
}

func (h *handler) clone() *handler {
Expand All @@ -157,6 +253,8 @@ func (h *handler) clone() *handler {
replaceAttr: h.replaceAttr,
timeFormat: h.timeFormat,
noColor: h.noColor,
levelColors: h.levelColors,
colors: h.colors,
}
}

Expand Down Expand Up @@ -219,7 +317,9 @@ func (h *handler) Handle(_ context.Context, r slog.Record) error {

// write message
if rep == nil {
buf.WriteStringIf(!h.noColor, h.colors[KindMessage])
buf.WriteString(r.Message)
buf.WriteStringIf(!h.noColor, ansiReset)
buf.WriteByte(' ')
} else if a := rep(nil /* groups */, slog.String(slog.MessageKey, r.Message)); a.Key != "" {
h.appendValue(buf, a.Value, false)
Expand Down Expand Up @@ -277,28 +377,30 @@ func (h *handler) WithGroup(name string) slog.Handler {
}

func (h *handler) appendTime(buf *buffer, t time.Time) {
buf.WriteStringIf(!h.noColor, ansiFaint)
buf.WriteStringIf(!h.noColor, h.colors[KindTime])
*buf = t.AppendFormat(*buf, h.timeFormat)
buf.WriteStringIf(!h.noColor, ansiReset)
}

func (h *handler) appendLevel(buf *buffer, level slog.Level) {
switch {
case level < slog.LevelInfo:
buf.WriteStringIf(!h.noColor, h.levelColors[level])
buf.WriteString("DBG")
appendLevelDelta(buf, level-slog.LevelDebug)
buf.WriteStringIf(!h.noColor, ansiReset)
case level < slog.LevelWarn:
buf.WriteStringIf(!h.noColor, ansiBrightGreen)
buf.WriteStringIf(!h.noColor, h.levelColors[level])
buf.WriteString("INF")
appendLevelDelta(buf, level-slog.LevelInfo)
buf.WriteStringIf(!h.noColor, ansiReset)
case level < slog.LevelError:
buf.WriteStringIf(!h.noColor, ansiBrightYellow)
buf.WriteStringIf(!h.noColor, h.levelColors[level])
buf.WriteString("WRN")
appendLevelDelta(buf, level-slog.LevelWarn)
buf.WriteStringIf(!h.noColor, ansiReset)
default:
buf.WriteStringIf(!h.noColor, ansiBrightRed)
buf.WriteStringIf(!h.noColor, h.levelColors[level])
buf.WriteString("ERR")
appendLevelDelta(buf, level-slog.LevelError)
buf.WriteStringIf(!h.noColor, ansiReset)
Expand All @@ -317,9 +419,11 @@ func appendLevelDelta(buf *buffer, delta slog.Level) {
func (h *handler) appendSource(buf *buffer, src *slog.Source) {
dir, file := filepath.Split(src.File)

buf.WriteStringIf(!h.noColor, ansiFaint)
buf.WriteStringIf(!h.noColor, h.colors[KindSourceFile])
buf.WriteString(filepath.Join(filepath.Base(dir), file))
buf.WriteStringIf(!h.noColor, ansiReset+h.colors[KindSourceSeparator])
buf.WriteByte(':')
buf.WriteStringIf(!h.noColor, ansiReset+h.colors[KindSourceLine])
buf.WriteString(strconv.Itoa(src.Line))
buf.WriteStringIf(!h.noColor, ansiReset)
}
Expand All @@ -337,15 +441,21 @@ func (h *handler) appendAttr(buf *buffer, attr slog.Attr, groupsPrefix string, g

if attr.Value.Kind() == slog.KindGroup {
if attr.Key != "" {
groupsPrefix += attr.Key + "."
if !h.noColor {
groupsPrefix += h.colors[KindGroup]
}
groupsPrefix += attr.Key
if !h.noColor {
groupsPrefix += ansiReset
}
groupsPrefix += "."
groups = append(groups, attr.Key)
}
for _, groupAttr := range attr.Value.Group() {
h.appendAttr(buf, groupAttr, groupsPrefix, groups)
}
} else if err, ok := attr.Value.Any().(tintError); ok {
// append tintError
h.appendTintError(buf, err, groupsPrefix)
} else if err, ok := attr.Value.Any().(error); ok && attr.Key == errKey {
h.appendError(buf, err, groupsPrefix)
buf.WriteByte(' ')
} else {
h.appendKey(buf, attr.Key, groupsPrefix)
Expand All @@ -355,13 +465,15 @@ func (h *handler) appendAttr(buf *buffer, attr slog.Attr, groupsPrefix string, g
}

func (h *handler) appendKey(buf *buffer, key, groups string) {
buf.WriteStringIf(!h.noColor, ansiFaint)
buf.WriteStringIf(!h.noColor, h.colors[KindKey])
appendString(buf, groups+key, true)
buf.WriteStringIf(!h.noColor, ansiReset+h.colors[KindSeparator])
buf.WriteByte('=')
buf.WriteStringIf(!h.noColor, ansiReset)
}

func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) {
buf.WriteStringIf(!h.noColor, h.colors[KindValue])
switch v.Kind() {
case slog.KindString:
appendString(buf, v.String(), quote)
Expand Down Expand Up @@ -393,13 +505,15 @@ func (h *handler) appendValue(buf *buffer, v slog.Value, quote bool) {
appendString(buf, fmt.Sprint(v.Any()), quote)
}
}
buf.WriteStringIf(!h.noColor, ansiReset)
}

func (h *handler) appendTintError(buf *buffer, err error, groupsPrefix string) {
buf.WriteStringIf(!h.noColor, ansiBrightRedFaint)
func (h *handler) appendError(buf *buffer, err error, groupsPrefix string) {
buf.WriteStringIf(!h.noColor, h.colors[KindErrorKey])
appendString(buf, groupsPrefix+errKey, true)
buf.WriteStringIf(!h.noColor, ansiReset+h.colors[KindErrorSeparator])
buf.WriteByte('=')
buf.WriteStringIf(!h.noColor, ansiResetFaint)
buf.WriteStringIf(!h.noColor, ansiReset+h.colors[KindErrorValue])
appendString(buf, err.Error(), true)
buf.WriteStringIf(!h.noColor, ansiReset)
}
Expand All @@ -424,15 +538,7 @@ func needsQuoting(s string) bool {
return false
}

type tintError struct{ error }

// 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)
// Err returns slog.Any("err", err)
func Err(err error) slog.Attr {
if err != nil {
err = tintError{err}
}
return slog.Any(errKey, err)
}

0 comments on commit 49b10b1

Please sign in to comment.