journslog: integrate with testing/slogtest

This commit is contained in:
Laurence Withers 2026-06-23 13:04:50 +01:00
parent df6107a67e
commit f4e1589ec3
3 changed files with 117 additions and 0 deletions

View File

@ -81,3 +81,11 @@ func WithAttrsFromTime(attrsFromTime func(time.Time) []slog.Attr) HandlerOption
jh.attrsFromTime = attrsFromTime jh.attrsFromTime = attrsFromTime
} }
} }
// WithAttrsFromLevel supplies a function which can optionally map the given
// log level onto zero or more attributes.
func WithAttrsFromLevel(attrsFromLevel func(slog.Level) []slog.Attr) HandlerOption {
return func(jh *JournalHandler) {
jh.attrsFromLevel = attrsFromLevel
}
}

View File

@ -62,6 +62,10 @@ type JournalHandler struct {
// Time from an [slog.Record]. It may be nil. // Time from an [slog.Record]. It may be nil.
attrsFromTime func(time.Time) []slog.Attr attrsFromTime func(time.Time) []slog.Attr
// attrsFromLevel may return zero or more attributes to represent the
// log level. It may be nil.
attrsFromLevel func(slog.Level) []slog.Attr
// groups are the ordered group names added by WithGroup. We need to // groups are the ordered group names added by WithGroup. We need to
// maintain the full list, not just a pre-rendered prefix string, // maintain the full list, not just a pre-rendered prefix string,
// because of the API of replaceAttr. // because of the API of replaceAttr.
@ -174,6 +178,13 @@ func (jh *JournalHandler) Handle(ctx context.Context, rec slog.Record) error {
} }
addAttrs = append(addAttrs, jh.attrsFromContext(ctx)...) addAttrs = append(addAttrs, jh.attrsFromContext(ctx)...)
} }
if jh.attrsFromLevel != nil {
if addAttrs == nil {
addAttrs = attrPoolGet()
defer attrPoolPut(addAttrs)
}
addAttrs = append(addAttrs, jh.attrsFromLevel(rec.Level)...)
}
// 2 for journal.Conn.Entry (MESSAGE and PRIORITY), // 2 for journal.Conn.Entry (MESSAGE and PRIORITY),
// 3 for SRC_* (optional but we always allocate space) // 3 for SRC_* (optional but we always allocate space)

View File

@ -4,7 +4,11 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"testing/slogtest"
"time"
"uuid"
"src.lwithers.me.uk/go/journal" "src.lwithers.me.uk/go/journal"
"src.lwithers.me.uk/go/journal/testsink" "src.lwithers.me.uk/go/journal/testsink"
@ -82,3 +86,97 @@ type slowValue string
func (s slowValue) LogValue() slog.Value { func (s slowValue) LogValue() slog.Value {
return slog.StringValue(string(s)) return slog.StringValue(string(s))
} }
// TestSlog integrates with [testing/slogtest].
func TestSlog(t *testing.T) {
tmpdir := t.TempDir()
attrsFromTime := func(t time.Time) []slog.Attr {
return []slog.Attr{
{
Key: "TEST_TIME",
Value: slog.StringValue(t.Format(time.RFC3339Nano)),
},
}
}
attrsFromLevel := func(l slog.Level) []slog.Attr {
return []slog.Attr{
{
Key: "TEST_LEVEL",
Value: slog.StringValue(l.String()),
},
}
}
// XXX: this relies on [slogtest.Run] executing its test cases
// completely sequentially
var curSink *testsink.Sink
newHandler := func(t *testing.T) slog.Handler {
sockpath := filepath.Join(tmpdir, uuid.New().String())
sink, err := testsink.New(sockpath)
if err != nil {
t.Fatalf("failed to create test sink: %v", err)
}
conn, err := journal.Connect(sockpath)
if err != nil {
sink.Stop()
t.Fatalf("failed to connect to test sink socket: %v", err)
}
h, err := NewHandler(WithConn(conn), WithAttrsFromTime(attrsFromTime), WithAttrsFromLevel(attrsFromLevel), WithHandlerOptions(slog.HandlerOptions{
Level: slog.LevelInfo,
}))
if err != nil {
sink.Stop()
t.Fatalf("failed to create handler: %v", err)
}
curSink = sink
return h
}
result := func(t *testing.T) map[string]any {
defer curSink.Stop()
m, err := curSink.Message(0)
if err != nil {
t.Fatalf("failed to fetch message from test sink: %v", err)
}
msg, attrs, err := m.Decode()
if err != nil {
t.Errorf("failed to decode message from test sink: %v", err)
}
out := make(map[string]any)
out[slog.MessageKey] = msg
for i := range attrs {
k, v := attrs[i].Key, attrs[i].Val
switch k {
case "TEST_TIME":
out[slog.TimeKey] = v
case "TEST_LEVEL":
out[slog.LevelKey] = v
default:
parts := strings.Split(k, "_")
Ng := len(parts) - 1 // number of groups
g := out
for i := range Ng {
next, _ := g[parts[i]].(map[string]any)
if next == nil {
next = make(map[string]any)
g[parts[i]] = next
}
g = next
}
g[strings.ToLower(parts[Ng])] = v
}
}
return out
}
slogtest.Run(t, newHandler, result)
}