From: Sergey Matveev Date: Mon, 18 Sep 2023 13:35:09 +0000 (+0300) Subject: log.slog handler X-Git-Url: http://www.git.cypherpunks.ru/?p=gorecfile.git;a=commitdiff_plain;h=530284958d3d776e249514dccacaba53468ff24a log.slog handler --- diff --git a/README b/README index 719e154..7d38e94 100644 --- a/README +++ b/README @@ -11,4 +11,7 @@ Limitations: * leading spaces in the first line of the value are ignored * trailing backslash at the end of lines is followed by space +Also there is go.cypherpunks.ru/recfile/slog.NewRecfileHandler log/slog +handler to write your logs in recfile format directly. + It is free software: see the file COPYING for copying conditions. diff --git a/go.mod b/go.mod index 6a9343d..6f8d7f4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module go.cypherpunks.ru/recfile -go 1.12 +go 1.21 diff --git a/slog/handler.go b/slog/handler.go new file mode 100644 index 0000000..0a2acd4 --- /dev/null +++ b/slog/handler.go @@ -0,0 +1,128 @@ +package slog + +import ( + "bytes" + "context" + "io" + "log/slog" + "sync" + "time" + + "go.cypherpunks.ru/recfile" +) + +type RecfileHandler struct { + W io.Writer + Level slog.Level + LevelKey string + MsgKey string + TimeKey string + attrs []slog.Attr + group string + m *sync.Mutex +} + +func NewRecfileHandler( + w io.Writer, + level slog.Level, + levelKey, msgKey, timeKey string, +) *RecfileHandler { + return &RecfileHandler{ + W: w, + Level: level, + LevelKey: levelKey, + MsgKey: msgKey, + TimeKey: timeKey, + m: new(sync.Mutex), + } +} + +func (h *RecfileHandler) Enabled(ctx context.Context, level slog.Level) bool { + return level >= h.Level +} + +func (h *RecfileHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &RecfileHandler{ + W: h.W, + Level: h.Level, + LevelKey: h.LevelKey, + MsgKey: h.MsgKey, + TimeKey: h.TimeKey, + attrs: append(h.attrs, attrs...), + group: h.group, + m: h.m, + } +} + +func (h *RecfileHandler) WithGroup(name string) slog.Handler { + neu := RecfileHandler{ + W: h.W, + Level: h.Level, + LevelKey: h.LevelKey, + MsgKey: h.MsgKey, + TimeKey: h.TimeKey, + attrs: h.attrs, + group: h.group + name + "_", + m: h.m, + } + return &neu +} + +func writeValue(w *recfile.Writer, group string, attr slog.Attr) (err error) { + if attr.Value.Kind() == slog.KindAny { + multiline, ok := attr.Value.Any().([]string) + if ok { + if len(multiline) > 0 { + _, err = w.WriteFieldMultiline(group+attr.Key, multiline) + return + } + return + } + } + _, err = w.WriteFields(recfile.Field{ + Name: group + attr.Key, + Value: attr.Value.String(), + }) + return +} + +func (h *RecfileHandler) Handle(ctx context.Context, rec slog.Record) (err error) { + var b bytes.Buffer + w := recfile.NewWriter(&b) + _, err = w.RecordStart() + if err != nil { + panic(err) + } + var fields []recfile.Field + if h.LevelKey != "" { + fields = append(fields, recfile.Field{ + Name: h.LevelKey, + Value: rec.Level.String(), + }) + } + if h.TimeKey != "" { + fields = append(fields, recfile.Field{ + Name: h.TimeKey, + Value: rec.Time.UTC().Format(time.RFC3339Nano), + }) + } + fields = append(fields, recfile.Field{Name: h.MsgKey, Value: rec.Message}) + _, err = w.WriteFields(fields...) + if err != nil { + return + } + for _, attr := range h.attrs { + writeValue(w, h.group, attr) + } + rec.Attrs(func(attr slog.Attr) bool { return writeValue(w, h.group, attr) == nil }) + h.m.Lock() + n, err := h.W.Write(b.Bytes()) + h.m.Unlock() + if err != nil { + return + } + if n != b.Len() { + return io.EOF + } + return +} diff --git a/slog/handler_test.go b/slog/handler_test.go new file mode 100644 index 0000000..19fdd82 --- /dev/null +++ b/slog/handler_test.go @@ -0,0 +1,92 @@ +package slog + +import ( + "bytes" + "log/slog" + "testing" + "time" + + "go.cypherpunks.ru/recfile" +) + +func TestBasic(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(NewRecfileHandler( + &buf, + slog.LevelWarn, + "Urgency", + "Message", + "Time", + )) + if !logger.Enabled(nil, slog.LevelWarn) { + t.FailNow() + } + logger.Info("won't catch me") + logger.Warn("catch me") + + r := recfile.NewReader(&buf) + m, err := r.NextMap() + if err != nil { + t.Fatal(err) + } + if m["Message"] != "catch me" { + t.FailNow() + } + if m["Urgency"] != "WARN" { + t.FailNow() + } + if _, err = time.Parse(time.RFC3339Nano, m["Time"]); err != nil { + t.FailNow() + } +} + +func TestTrimmed(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(NewRecfileHandler(&buf, slog.LevelWarn, "", "Message", "")) + logger.Warn("catch me") + r := recfile.NewReader(&buf) + m, err := r.NextMap() + if err != nil { + t.Fatal(err) + } + if m["Message"] != "catch me" { + t.FailNow() + } + if m["Urgency"] != "" { + t.FailNow() + } + if m["Time"] != "" { + t.FailNow() + } +} + +func TestFeatured(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(NewRecfileHandler(&buf, slog.LevelInfo, "L", "M", "T")) + logger.WithGroup("grou").WithGroup("py").With("foo", "bar").With("bar", "baz").Info( + "catch me", "baz", []string{"multi", "line"}, + ) + r := recfile.NewReader(&buf) + m, err := r.NextMap() + if err != nil { + t.Fatal(err) + } + if m["M"] != "catch me" { + t.Fatal("M") + } + if m["L"] != "INFO" { + t.Fatal("L") + } + if _, err = time.Parse(time.RFC3339Nano, m["T"]); err != nil { + t.Fatal("T") + } + if m["grou_py_foo"] != "bar" { + t.Fatal("foo") + } + if m["grou_py_bar"] != "baz" { + t.Fatal("bar") + } + if m["grou_py_baz"] != "multi\nline" { + t.Fatal("baz") + } +}