Skip to content

Instantly share code, notes, and snippets.

@StevenACoffman
Forked from wijayaerick/slog_console_handler.go
Last active March 26, 2023 23:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save StevenACoffman/d51ddab0d8af65e94ef88852f9fc1551 to your computer and use it in GitHub Desktop.
Save StevenACoffman/d51ddab0d8af65e94ef88852f9fc1551 to your computer and use it in GitHub Desktop.
Example ConsoleHandler for golang.org/x/exp/slog Logger
// ConsoleHandler formats slog.Logger output in console format, a bit similar with Uber's zap
// ConsoleEncoder or Apex's CLI log
// The log format is designed to be human-readable.
//
// Performance can definitely be improved, however it's not my priority as
// this should only be used in development environment.
//
// e.g. log output:
// • ./main.go:21 Debug message {"hello":"world","!BADKEY":"bad kv"}
// ℹ ./main.go:217 Info message {"with_key_1":"with_value_1","group_1":{"with_key_2":"with_value_2","hello":"world"}}
// ⚠ ./main.go:218 ./main.go:168 Warn message {"with_key_1":"with_value_1","group_1":{"with_key_2":"with_value_2","hello":"world"}}
// ⨯ ./main.go:219 Error message {"with_key_1":"with_value_1","group_1":{"with_key_2":"with_value_2","hello":"world","err":"an error"}}
package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"golang.org/x/exp/slog"
)
type ConsoleHandler struct {
opts ConsoleHandlerOptions
internalHandler slog.Handler
mu sync.Mutex
w io.Writer
}
type ConsoleHandlerOptions struct {
SlogOpts slog.HandlerOptions
UseColor bool
}
func NewConsoleHandler(w io.Writer) *ConsoleHandler {
return ConsoleHandlerOptions{
UseColor: true,
SlogOpts: slog.HandlerOptions{Level: slog.LevelDebug},
}.NewConsoleHandler(w)
}
func (opts ConsoleHandlerOptions) NewConsoleHandler(w io.Writer) *ConsoleHandler {
internalOpts := opts.SlogOpts
internalOpts.AddSource = false
internalOpts.ReplaceAttr = func(group []string, a slog.Attr) slog.Attr {
if a.Key == "time" || a.Key == "level" || a.Key == "msg" {
return slog.String("", "")
}
rep := opts.SlogOpts.ReplaceAttr
if rep != nil {
return rep(group, a)
}
return a
}
return &ConsoleHandler{opts: opts, w: w, internalHandler: internalOpts.NewJSONHandler(w)}
}
func (h *ConsoleHandler) Enabled(ctx context.Context, level slog.Level) bool {
return h.internalHandler.Enabled(ctx, level)
}
func (h *ConsoleHandler) Handle(ctx context.Context, r slog.Record) error {
var buf bytes.Buffer
level := r.Level.String()
fs := runtime.CallersFrames([]uintptr{r.PC})
f, _ := fs.Next()
if h.opts.UseColor {
color := colorFromLevel(level)
buf.WriteString(colorIconFromLevel(level))
buf.WriteString(" ")
buf.WriteString(AddColor(Cyan, fmt.Sprintf("%s:%d", trimRootPath(f.File), f.Line)))
buf.WriteString(" ")
buf.WriteString(color.Add(r.Message))
buf.WriteString(" ")
} else {
buf.WriteString(level)
buf.WriteString(" ")
buf.WriteString(fmt.Sprintf("%s:%d", trimRootPath(f.File), f.Line))
buf.WriteString(" ")
buf.WriteString(r.Message)
buf.WriteString(" ")
}
h.mu.Lock()
defer h.mu.Unlock()
_, err := h.w.Write(buf.Bytes())
if err != nil {
return err
}
return h.internalHandler.Handle(ctx, r)
}
func (h *ConsoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &ConsoleHandler{
opts: h.opts,
w: h.w,
internalHandler: h.internalHandler.WithAttrs(attrs),
}
}
func (h *ConsoleHandler) WithGroup(name string) slog.Handler {
return &ConsoleHandler{
opts: h.opts,
w: h.w,
internalHandler: h.internalHandler.WithGroup(name),
}
}
var (
_, callerFile, _, _ = runtime.Caller(0)
rootPath = filepath.Dir(callerFile)
)
func trimRootPath(p string) string {
return strings.Replace(p, rootPath, ".", 1)
}
type Color uint8
const (
Black Color = iota + 30
Red
Green
Yellow
Blue
Magenta
Cyan
White
)
// Add adds the coloring to the given string.
func (c Color) Add(s string) string {
return fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(c), s)
}
func AddColor(c Color, s string) string {
return c.Add(s)
}
var (
levelToColor = map[string]Color{
slog.LevelDebug.String(): Green,
slog.LevelInfo.String(): Magenta,
slog.LevelWarn.String(): Yellow,
slog.LevelError.String(): Red,
}
unknownLevelColor = Red
levelToIcon = map[string]string{
slog.LevelDebug.String(): "•", // U+2022
slog.LevelInfo.String(): "ℹ", // U+2139
slog.LevelWarn.String(): "⚠", // U+26A0
slog.LevelError.String(): "⨯", // U+2A2F
}
unknownLevelIcon = "⨯"
)
func colorIconFromLevel(level string) string {
color := colorFromLevel(level)
icon, ok := levelToIcon[level]
if !ok {
icon = unknownLevelIcon
}
return color.Add(icon)
}
func colorFromLevel(level string) Color {
color, ok := levelToColor[level]
if !ok {
color = unknownLevelColor
}
return color
}
func main() {
logger := slog.New(NewConsoleHandler(os.Stderr))
logger.Debug("Debug message", "hello", "world", "bad kv")
logger = logger.
With("with_key_1", "with_value_1").
WithGroup("group_1").
With("with_key_2", "with_value_2")
logger.Info("Info message", "hello", "world")
logger.Warn("Warn message", "hello", "world")
logger.Error("Error message", errors.New("an error"), "hello", "world")
}
@StevenACoffman
Copy link
Author

Screen Shot 2023-03-26 at 5 56 49 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment