--- /dev/null
+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
+}
--- /dev/null
+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")
+ }
+}