journslog: integrate with testing/slogtest
This commit is contained in:
parent
df6107a67e
commit
f4e1589ec3
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue