Skip to content

Instantly share code, notes, and snippets.

@ttys3
Forked from wijayaerick/slog_console_handler.go
Created February 2, 2023 15:29
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 ttys3/5d05e5eb8cda55d0795b83b2df78d083 to your computer and use it in GitHub Desktop.
Save ttys3/5d05e5eb8cda55d0795b83b2df78d083 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
// The log format is designed to be human-readable.
//
// Performance can definitely be improved, however it's not in my priority as
// this should only be used in development environment.
//
// e.g. log output:
// 2022-11-24T11:40:20+08:00 DEBUG ./main.go:162 Debug message {"hello":"world","!BADKEY":"bad kv"}
// 2022-11-24T11:40:20+08:00 INFO ./main.go:167 Info message {"with_key_1":"with_value_1","group_1":{"with_key_2":"with_value_2","hello":"world"}}
// 2022-11-24T11:40:20+08:00 WARN ./main.go:168 Warn message {"with_key_1":"with_value_1","group_1":{"with_key_2":"with_value_2","hello":"world"}}
// 2022-11-24T11:40:20+08:00 ERROR ./main.go:169 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"
"errors"
"fmt"
"golang.org/x/exp/slog"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
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{}.NewConsoleHandler(w)
}
func (opts ConsoleHandlerOptions) NewConsoleHandler(w io.Writer) *ConsoleHandler {
internalOpts := opts.SlogOpts
internalOpts.AddSource = false
internalOpts.ReplaceAttr = func(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(a)
}
return a
}
return &ConsoleHandler{opts: opts, w: w, internalHandler: internalOpts.NewJSONHandler(w)}
}
func (h *ConsoleHandler) Enabled(level slog.Level) bool {
return h.internalHandler.Enabled(level)
}
func (h *ConsoleHandler) Handle(r slog.Record) error {
var buf bytes.Buffer
buf.WriteString(r.Time.Format(time.RFC3339))
buf.WriteString(" ")
level := r.Level.String()
if h.opts.UseColor {
level = addColorToLevel(level)
}
buf.WriteString(level)
buf.WriteString(" ")
if h.opts.SlogOpts.AddSource {
file, line := r.SourceLine()
if file != "" {
buf.WriteString(trimRootPath(file))
buf.WriteString(":")
buf.Write([]byte(strconv.Itoa(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(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)
}
var (
levelToColor = map[string]Color{
slog.DebugLevel.String(): Magenta,
slog.InfoLevel.String(): Blue,
slog.WarnLevel.String(): Yellow,
slog.ErrorLevel.String(): Red,
}
unknownLevelColor = Red
)
func addColorToLevel(level string) string {
color, ok := levelToColor[level]
if !ok {
color = unknownLevelColor
}
return color.Add(level)
}
func main() {
logHandler := ConsoleHandlerOptions{
SlogOpts: slog.HandlerOptions{
AddSource: true,
Level: slog.DebugLevel,
},
UseColor: true,
}.NewConsoleHandler(os.Stderr)
logger := slog.New(logHandler)
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")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment