]> Cypherpunks.ru repositories - gorecfile.git/commitdiff
log.slog handler
authorSergey Matveev <stargrave@stargrave.org>
Mon, 18 Sep 2023 13:35:09 +0000 (16:35 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Mon, 18 Sep 2023 13:36:29 +0000 (16:36 +0300)
README
go.mod
slog/handler.go [new file with mode: 0644]
slog/handler_test.go [new file with mode: 0644]

diff --git a/README b/README
index 719e1549487ac45a166dee0070a432e9fd9deabf..7d38e9467e33b97d1a80beabdc7b3f1ea9212419 100644 (file)
--- 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 6a9343df5ff870ad62b5a352daef1bedc7b935b0..6f8d7f445073113831283291d8373e07522ecd11 100644 (file)
--- 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 (file)
index 0000000..0a2acd4
--- /dev/null
@@ -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 (file)
index 0000000..19fdd82
--- /dev/null
@@ -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")
+       }
+}