Skip to content

Commit 2e097a8

Browse files
authored
Add lazy slice formatters to slogutil (#1028)
Adds a couple slice helpers to `slogutil` that implement `slog.LogValue` to achieve the effect of formatting slices that are used as values in slog attributes, but only when we actually need to do so because a slog log line is being printed. Otherwise, `LogValue` is never called and we avoid the associated work and allocations.
1 parent ed11967 commit 2e097a8

File tree

2 files changed

+95
-0
lines changed

2 files changed

+95
-0
lines changed

rivershared/util/slogutil/slog_util.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,33 @@ import (
66
"io"
77
"log/slog"
88
"os"
9+
"strconv"
10+
"strings"
11+
12+
"github.com/riverqueue/river/rivershared/util/sliceutil"
913
)
1014

15+
// SliceInt64 is a type that implements slog.LogValue and which will format a
16+
// slice for inclusion in logging, but lazily so that no work is done unless a
17+
// log line is actually emitted.
18+
type SliceInt64 []int64
19+
20+
func (s SliceInt64) LogValue() slog.Value {
21+
return slog.StringValue(strings.Join(
22+
sliceutil.Map(s, func(i int64) string { return strconv.FormatInt(i, 10) }),
23+
",",
24+
))
25+
}
26+
27+
// SliceString is a type that implements slog.LogValue and which will format a
28+
// slice for inclusion in logging, but lazily so that no work is done unless a
29+
// log line is actually emitted.
30+
type SliceString []string
31+
32+
func (s SliceString) LogValue() slog.Value {
33+
return slog.StringValue(strings.Join(s, ","))
34+
}
35+
1136
// SlogMessageOnlyHandler is a trivial slog handler that prints only messages.
1237
// All attributes and groups are ignored. It's useful in example tests where it
1338
// produces output that's normalized so we match against it (normally, all log
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package slogutil
2+
3+
import (
4+
"bytes"
5+
"log/slog"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestSliceInt64(t *testing.T) {
12+
t.Parallel()
13+
14+
t.Run("Empty", func(t *testing.T) {
15+
t.Parallel()
16+
17+
logger, buf := plainLoggerAndBuffer()
18+
logger.Info("log_entry", slog.Any("values", SliceInt64(nil)))
19+
20+
require.Equal(t, `msg=log_entry values=""`+"\n", buf.String())
21+
})
22+
23+
t.Run("Values", func(t *testing.T) {
24+
t.Parallel()
25+
26+
logger, buf := plainLoggerAndBuffer()
27+
logger.Info("log_entry", slog.Any("values", SliceInt64([]int64{1, 2, 3})))
28+
29+
require.Equal(t, "msg=log_entry values=1,2,3\n", buf.String())
30+
})
31+
}
32+
33+
func TestSliceString(t *testing.T) {
34+
t.Parallel()
35+
36+
t.Run("Empty", func(t *testing.T) {
37+
t.Parallel()
38+
39+
logger, buf := plainLoggerAndBuffer()
40+
logger.Info("log_entry", slog.Any("values", SliceString(nil)))
41+
42+
require.Equal(t, `msg=log_entry values=""`+"\n", buf.String())
43+
})
44+
45+
t.Run("Values", func(t *testing.T) {
46+
t.Parallel()
47+
48+
logger, buf := plainLoggerAndBuffer()
49+
logger.Info("log_entry", slog.Any("values", SliceString([]string{"foo", "bar"})))
50+
51+
require.Equal(t, "msg=log_entry values=foo,bar\n", buf.String())
52+
})
53+
}
54+
55+
func plainLoggerAndBuffer() (*slog.Logger, *bytes.Buffer) {
56+
var buf bytes.Buffer
57+
return slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
58+
// Removes the `level` and `time` keys so that we have clean and stable
59+
// output to match against in assertions.
60+
ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
61+
if len(groups) < 1 {
62+
switch attr.Key {
63+
case slog.LevelKey, slog.TimeKey:
64+
return slog.Attr{}
65+
}
66+
}
67+
return attr
68+
},
69+
})), &buf
70+
}

0 commit comments

Comments
 (0)