From 438be2e99fb7dcf7ea1be24a9e3848ffddf41c4c Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Thu, 18 Jan 2024 15:03:09 +0100 Subject: [PATCH] initial commit --- README.md | 25 ++++++++ example.go | 50 +++++++++++++++ handler.go | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+) create mode 100644 README.md create mode 100644 example.go create mode 100644 handler.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..12a30a2 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# YamlDumpHandler - a human readable yaml based slog.Handler + +Example output: + +```sh +2024-01-18T02:57.41 CET INFO: info + enemy: + alive: true + health: 10 + name: Bodo + ammo: + - forweapon: Railgun + impact: 100 + cost: 100000 + range: 400 + spawn: 199 +2024-01-18T02:57.41 CET INFO: connecting + enemies: 100 + players: 2 + world: 600x800 +2024-01-18T02:57.41 CET DEBUG: debug text +2024-01-18T02:57.41 CET ERROR: error +``` + +See `example.go` for usage. diff --git a/example.go b/example.go new file mode 100644 index 0000000..2b9ec12 --- /dev/null +++ b/example.go @@ -0,0 +1,50 @@ +package main + +import ( + "log/slog" + "os" +) + +type body string + +type Ammo struct { + Forweapon string + Impact int + Cost int + Range int +} + +type Enemy struct { + Alive bool + Health int + Name string + Body body `yaml:"-"` + Ammo []Ammo +} + +func removeTime(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a +} + +func main() { + opts := &YamlDumpHandlerOptions{ + Level: slog.LevelDebug, + //ReplaceAttr: removeTime, + } + + logger := slog.New(NewYamlDumpHandler(os.Stdout, opts)) + + slog.SetDefault(logger) + + e := &Enemy{Alive: true, Health: 10, Name: "Bodo", Body: "body\nbody\n", + Ammo: []Ammo{{Forweapon: "Railgun", Range: 400, Impact: 100, Cost: 100000}}, + } + + slog.Info("info", "enemy", e, "spawn", 199) + slog.Info("connecting", "enemies", 100, "players", 2, "world", "600x800") + slog.Debug("debug text") + slog.Error("error") +} diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..8965a2d --- /dev/null +++ b/handler.go @@ -0,0 +1,182 @@ +package main + +import ( + "bytes" + "context" + "io" + "log" + "log/slog" + "regexp" + "slices" + "strings" + "sync" + + "gopkg.in/yaml.v3" + + "github.com/fatih/color" +) + +const defaultTimeFormat = "2006-01-02T03:04.05 MST" + +type YamlDumpHandler struct { + l *log.Logger + writer io.Writer + mu *sync.Mutex + level slog.Leveler + groups []string + attrs string + timeFormat string + replaceAttr func(groups []string, a slog.Attr) slog.Attr + addSource bool + indenter *regexp.Regexp +} + +type YamlDumpHandlerOptions struct { + Level slog.Leveler + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr + TimeFormat string + AddSource bool +} + +func (h *YamlDumpHandler) Handle(ctx context.Context, r slog.Record) error { + level := r.Level.String() + ":" + + switch r.Level { + case slog.LevelDebug: + level = color.MagentaString(level) + case slog.LevelInfo: + level = color.BlueString(level) + case slog.LevelWarn: + level = color.YellowString(level) + case slog.LevelError: + level = color.RedString(level) + } + + fields := make(map[string]interface{}, r.NumAttrs()) + r.Attrs(func(a slog.Attr) bool { + fields[a.Key] = a.Value.Any() + return true + }) + + tree := h.attrs + + if len(fields) > 0 { + bytetree, err := yaml.Marshal(&fields) + if err != nil { + return err + } + + tree = h.Postprocess(bytetree) + } + + timeStr := "" + timeAttr := slog.Time(slog.TimeKey, r.Time) + + if h.replaceAttr != nil { + timeAttr = h.replaceAttr(nil, timeAttr) + } + + if !r.Time.IsZero() && !timeAttr.Equal(slog.Attr{}) { + timeStr = r.Time.Format(h.timeFormat) + } + + msg := color.CyanString(r.Message) + + buf := bytes.Buffer{} + + if len(timeStr) > 0 { + buf.WriteString(timeStr) + buf.WriteString(" ") + } + buf.WriteString(level) + buf.WriteString(" ") + buf.WriteString(msg) + buf.WriteString(" ") + buf.WriteString(color.WhiteString(tree)) + buf.WriteString("\n") + + h.mu.Lock() + defer h.mu.Unlock() + _, err := h.writer.Write(buf.Bytes()) + + // h.l.Println(timeStr, level, msg, color.WhiteString(tree)) + + return err +} + +func (h *YamlDumpHandler) Postprocess(yamlstr []byte) string { + return "\n " + strings.TrimSpace(h.indenter.ReplaceAllString(string(yamlstr), " ")) +} + +func NewYamlDumpHandler(out io.Writer, opts *YamlDumpHandlerOptions) *YamlDumpHandler { + if opts == nil { + opts = &YamlDumpHandlerOptions{} + } + + h := &YamlDumpHandler{ + writer: out, + mu: &sync.Mutex{}, + level: opts.Level, + timeFormat: opts.TimeFormat, + replaceAttr: opts.ReplaceAttr, + addSource: opts.AddSource, + indenter: regexp.MustCompile(`(?m)^`), + } + + if h.timeFormat == "" { + h.timeFormat = defaultTimeFormat + } + + return h +} + +// Enabled indicates whether the receiver logs at the given level. +func (h *YamlDumpHandler) Enabled(_ context.Context, l slog.Level) bool { + return l >= h.level.Level() +} + +// attributes plus attrs. +func (h *YamlDumpHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + if len(attrs) == 0 { + return h + } + + fields := make(map[string]interface{}, len(attrs)) + for _, a := range attrs { + fields[a.Key] = a.Value.Any() + } + + bytetree, err := yaml.Marshal(&fields) + if err != nil { + panic(err) + } + + h2 := h.clone() + + h2.attrs += string(bytetree) + return h2 +} + +// WithGroup returns a new [log/slog.Handler] with name appended to the +// receiver's groups. +func (h *YamlDumpHandler) WithGroup(name string) slog.Handler { + if name == "" { + return h + } + h2 := h.clone() + h2.groups = append(h2.groups, name) + return h2 +} + +func (h *YamlDumpHandler) clone() *YamlDumpHandler { + return &YamlDumpHandler{ + writer: h.writer, + mu: h.mu, + level: h.level, + groups: slices.Clip(h.groups), + attrs: h.attrs, + timeFormat: h.timeFormat, + replaceAttr: h.replaceAttr, + addSource: h.addSource, + } +}