package journslog import ( "log/slog" "net/http" "path/filepath" "strings" "testing" "testing/slogtest" "time" "uuid" "src.lwithers.me.uk/go/journal" "src.lwithers.me.uk/go/journal/testsink" ) // TestHandler emits a log message with various attributes / groups and ensures // the result is logged successfully. func TestHandler(t *testing.T) { sockpath := filepath.Join(t.TempDir(), "socket") sink, err := testsink.New(sockpath) if err != nil { t.Fatal(err) } defer sink.Stop() conn, err := journal.Connect(sockpath) if err != nil { t.Fatal(err) } defer conn.Close() jh, err := NewHandler(WithConn(conn)) if err != nil { t.Fatal(err) } httpGroup := slog.Group("http", "path", "/foo", "status", http.StatusOK) logger := slog.New(jh) logger = logger.With("global_attr_key", "global_attr_val", httpGroup).WithGroup("app") logger.Info("hello, journal", "slowkey", slowValue("slowval"), slog.Group("g1", "attr1", "val1", "attr2", 123)) msg, err := sink.Message(0) if err != nil { t.Fatal(err) } msgText, attrs, err := msg.Decode() if err != nil { t.Error(err) } if msgText != "hello, journal" { t.Errorf("unexpected message text %q", msgText) } expAttrs := []testsink.DecodedAttr{ testsink.DecodedAttr{Key: "PRIORITY", Val: "6"}, testsink.DecodedAttr{Key: "GLOBAL_ATTR_KEY", Val: "global_attr_val"}, testsink.DecodedAttr{Key: "HTTP_PATH", Val: "/foo"}, testsink.DecodedAttr{Key: "HTTP_STATUS", Val: "200"}, testsink.DecodedAttr{Key: "APP_SLOWKEY", Val: "slowval"}, testsink.DecodedAttr{Key: "APP_G1_ATTR1", Val: "val1"}, testsink.DecodedAttr{Key: "APP_G1_ATTR2", Val: "123"}, } for i := range expAttrs { key := expAttrs[i].Key val, ok := testsink.GetAttr(attrs, key) switch { case !ok: t.Errorf("missing %s attribute", key) case val != expAttrs[i].Val: t.Errorf("unexpected %s value", key) } } if t.Failed() { for i := range attrs { t.Errorf("attr %q=%q", attrs[i].Key, attrs[i].Val) } } } type slowValue string func (s slowValue) LogValue() slog.Value { 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) }