Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions internal/logring/logring.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,18 @@ func (h *Handler) WithGroup(name string) slog.Handler {
}

// flatten renders an attr subtree into "key=value" strings; group keys are
// dotted (group.subkey=value).
// dotted (group.subkey=value) and empty-key groups are inlined.
func flatten(prefix string, a slog.Attr) []string {
if a.Value.Kind() == slog.KindGroup {
// Recurse with the GROUP's own key as the new prefix so
// slog.Group("http", slog.Int("status", 200)) renders
// "http.status=200" — the child keys extend the group, they do not
// replace it.
key := a.Key
if prefix != "" {
key = prefix + "." + key
// The group's own key extends the prefix ("http.status=200"); an
// empty key inlines the group, so children keep the current prefix
// with no extra separator.
key := prefix
if a.Key != "" {
key = a.Key
if prefix != "" {
key = prefix + "." + a.Key
}
}
var out []string
for _, child := range a.Value.Group() {
Expand Down
52 changes: 52 additions & 0 deletions internal/logring/logring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,55 @@ func TestCountsConcurrent(t *testing.T) {
t.Errorf("Counts()[warn|retry] = %d, want %d", got, goroutines*per)
}
}

// TestRingEmptyGroupInlined is the empty-key group regression: a group with
// an empty key must be inlined (slog contract: "If a group's key is empty,
// inline the group's Attrs"), so its children keep the current prefix with
// no extra separator — "svc.status=200", never "svc..status=200".
func TestRingEmptyGroupInlined(t *testing.T) {
cases := []struct {
name string
withGroup string
group slog.Attr
want string
}{
{
name: "empty group under named prefix",
withGroup: "svc",
group: slog.Group("", slog.Int("status", 200)),
want: "svc.status=200",
},
{
name: "nested empty group",
withGroup: "svc",
group: slog.Group("", slog.Group("inner", slog.Int("status", 200))),
want: "svc.inner.status=200",
},
{
name: "consecutive empty groups",
withGroup: "svc",
group: slog.Group("", slog.Group("", slog.Int("status", 200))),
want: "svc.status=200",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
h := NewHandler(discarding{}, 10)
logger := slog.New(h)
if tc.withGroup != "" {
logger = logger.WithGroup(tc.withGroup)
}
logger.Info("msg", tc.group)
recent := h.Recent(1)
if len(recent) != 1 {
t.Fatalf("Recent(1) = %d entries, want 1", len(recent))
}
// One record with one attr: Fields must equal the expected
// single field exactly, so any malformed "svc..status=200"
// shape fails the equality implicitly.
if got := recent[0].Fields; len(got) != 1 || got[0] != tc.want {
t.Errorf("fields = %v, want [%s]", got, tc.want)
}
})
}
}