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
25 changes: 25 additions & 0 deletions rivershared/util/slogutil/slog_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,33 @@ import (
"io"
"log/slog"
"os"
"strconv"
"strings"

"github.com/riverqueue/river/rivershared/util/sliceutil"
)

// SliceInt64 is a type that implements slog.LogValue and which will format a
// slice for inclusion in logging, but lazily so that no work is done unless a
// log line is actually emitted.
type SliceInt64 []int64

func (s SliceInt64) LogValue() slog.Value {
return slog.StringValue(strings.Join(
sliceutil.Map(s, func(i int64) string { return strconv.FormatInt(i, 10) }),
",",
))
}

// SliceString is a type that implements slog.LogValue and which will format a
// slice for inclusion in logging, but lazily so that no work is done unless a
// log line is actually emitted.
type SliceString []string

func (s SliceString) LogValue() slog.Value {
return slog.StringValue(strings.Join(s, ","))
}

// SlogMessageOnlyHandler is a trivial slog handler that prints only messages.
// All attributes and groups are ignored. It's useful in example tests where it
// produces output that's normalized so we match against it (normally, all log
Expand Down
70 changes: 70 additions & 0 deletions rivershared/util/slogutil/slog_util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package slogutil

import (
"bytes"
"log/slog"
"testing"

"github.com/stretchr/testify/require"
)

func TestSliceInt64(t *testing.T) {
t.Parallel()

t.Run("Empty", func(t *testing.T) {
t.Parallel()

logger, buf := plainLoggerAndBuffer()
logger.Info("log_entry", slog.Any("values", SliceInt64(nil)))

require.Equal(t, `msg=log_entry values=""`+"\n", buf.String())
})

t.Run("Values", func(t *testing.T) {
t.Parallel()

logger, buf := plainLoggerAndBuffer()
logger.Info("log_entry", slog.Any("values", SliceInt64([]int64{1, 2, 3})))

require.Equal(t, "msg=log_entry values=1,2,3\n", buf.String())
})
}

func TestSliceString(t *testing.T) {
t.Parallel()

t.Run("Empty", func(t *testing.T) {
t.Parallel()

logger, buf := plainLoggerAndBuffer()
logger.Info("log_entry", slog.Any("values", SliceString(nil)))

require.Equal(t, `msg=log_entry values=""`+"\n", buf.String())
})

t.Run("Values", func(t *testing.T) {
t.Parallel()

logger, buf := plainLoggerAndBuffer()
logger.Info("log_entry", slog.Any("values", SliceString([]string{"foo", "bar"})))

require.Equal(t, "msg=log_entry values=foo,bar\n", buf.String())
})
}

func plainLoggerAndBuffer() (*slog.Logger, *bytes.Buffer) {
var buf bytes.Buffer
return slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
// Removes the `level` and `time` keys so that we have clean and stable
// output to match against in assertions.
ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
if len(groups) < 1 {
switch attr.Key {
case slog.LevelKey, slog.TimeKey:
return slog.Attr{}
}
}
return attr
},
})), &buf
}
Loading