Skip to content

Commit

Permalink
Add stream loggers.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmalloc committed Jul 18, 2024
1 parent 3721ab1 commit 7ecc6f6
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 37 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ The format is based on [Keep a Changelog], and this project adheres to
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html

## [Unreleased]

### Added

- Added `NewStreamLogger()` and `NewStreamHandler()` functions that create
loggers and handlers that write to an `io.Writer`.

### Changed

- **[BC]** Renamed `NewLogger` to `NewTestLogger`
- **[BC]** Renamed `NewHandler` to `NewTestHandler`

## [0.1.1] - 2024-04-09

- Include the elapsed duration since the logger was created in each log message.
Expand Down
32 changes: 4 additions & 28 deletions spruce.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,11 @@ import (
"log/slog"
"slices"
"strings"
"testing"
"time"
)

// TestingT is the subset of the [testing.TB] interface that is used to write
// logs.
type TestingT interface {
Log(...any)
}

var _ TestingT = (testing.TB)(nil)

// NewLogger returns a [slog.Logger] that writes to t.
func NewLogger(t TestingT) *slog.Logger {
return slog.New(NewHandler(t))
}

// NewHandler returns a new [slog.Handler] that writes to t.
func NewHandler(t TestingT) slog.Handler {
return &handler{
T: t,
epoch: time.Now(),
}
}

type handler struct {
T TestingT
log func(string) error
attrs []slog.Attr
groups []string
epoch time.Time
Expand Down Expand Up @@ -84,9 +62,7 @@ func (h *handler) Handle(_ context.Context, rec slog.Record) error {

writeAttrs(buf, 0, attrs, true)

h.T.Log(buf.String())

return nil
return h.log(buf.String())
}

func writeAttrs(
Expand Down Expand Up @@ -150,7 +126,7 @@ func writeAttrs(

func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &handler{
T: h.T,
log: h.log,
attrs: append(slices.Clone(h.attrs), attrs...),
groups: h.groups,
epoch: h.epoch,
Expand All @@ -159,7 +135,7 @@ func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {

func (h *handler) WithGroup(name string) slog.Handler {
return &handler{
T: h.T,
log: h.log,
attrs: h.attrs,
groups: append(slices.Clone(h.groups), name),
epoch: h.epoch,
Expand Down
19 changes: 10 additions & 9 deletions spruce_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import (
"time"

"github.com/dogmatiq/spruce"
. "github.com/dogmatiq/spruce"
"github.com/google/go-cmp/cmp"
)

func TestHandler_noAttributes(t *testing.T) {
s := &testingTStub{T: t}
l := spruce.NewLogger(s)
l := NewTestLogger(s)

l.Info("<message>")
s.Expect(
Expand All @@ -24,7 +25,7 @@ func TestHandler_noAttributes(t *testing.T) {

func TestHandler_stringAttribute(t *testing.T) {
s := &testingTStub{T: t}
l := spruce.NewLogger(s)
l := NewTestLogger(s)

l.Info("<message>", "<key>", "<value>")
s.Expect(
Expand All @@ -35,7 +36,7 @@ func TestHandler_stringAttribute(t *testing.T) {

func TestHandler_stringerAttribute(t *testing.T) {
s := &testingTStub{T: t}
l := spruce.NewLogger(s)
l := NewTestLogger(s)

l.Info(
"<message>",
Expand All @@ -50,7 +51,7 @@ func TestHandler_stringerAttribute(t *testing.T) {

func TestHandler_attributeAlignment(t *testing.T) {
s := &testingTStub{T: t}
l := spruce.NewLogger(s)
l := NewTestLogger(s)

l.Info(
"<message>",
Expand All @@ -66,7 +67,7 @@ func TestHandler_attributeAlignment(t *testing.T) {

func TestHandler_nestedAttributes(t *testing.T) {
s := &testingTStub{T: t}
l := spruce.NewLogger(s)
l := NewTestLogger(s)

l.Info(
"<message>",
Expand All @@ -92,7 +93,7 @@ func TestHandler_nestedAttributes(t *testing.T) {

func TestHandler_whitespaceEscaping(t *testing.T) {
s := &testingTStub{T: t}
l := spruce.NewLogger(s)
l := NewTestLogger(s)

l.Info(
"<message>",
Expand All @@ -107,7 +108,7 @@ func TestHandler_whitespaceEscaping(t *testing.T) {
func TestHandler_WithAttrs(t *testing.T) {
s := &testingTStub{T: t}
l := spruce.
NewLogger(s).
NewTestLogger(s).
With("<key>", "<value>")

l.Info("<message>")
Expand All @@ -120,7 +121,7 @@ func TestHandler_WithAttrs(t *testing.T) {
func TestHandler_WithAttrs_sameKey(t *testing.T) {
s := &testingTStub{T: t}
l := spruce.
NewLogger(s).
NewTestLogger(s).
With("<key>", "<value-1>")

l.Info("<message>", "<key>", "<value-2>")
Expand All @@ -134,7 +135,7 @@ func TestHandler_WithAttrs_sameKey(t *testing.T) {
func TestHandler_WithGroup(t *testing.T) {
s := &testingTStub{T: t}
l := spruce.
NewLogger(s).
NewTestLogger(s).
WithGroup("<group>")

l.Info("<message>", "<key>", "<value>")
Expand Down
30 changes: 30 additions & 0 deletions stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package spruce

import (
"io"
"log/slog"
"time"
)

var newLine = []byte{'\n'}

// NewStreamLogger returns a [slog.Logger] that writes to w.
func NewStreamLogger(w io.Writer) *slog.Logger {
return slog.New(NewStreamHandler(w))
}

// NewStreamHandler returns a new [slog.Handler] that writes to w.
func NewStreamHandler(w io.Writer) slog.Handler {
return &handler{
log: func(s string) error {
if _, err := w.Write([]byte(s)); err != nil {
return err
}
if _, err := w.Write(newLine); err != nil {
return err
}
return nil
},
epoch: time.Now(),
}
}
22 changes: 22 additions & 0 deletions stream_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package spruce_test

import (
"strings"
"testing"

. "github.com/dogmatiq/spruce"
)

func TestNewStreamLogger(t *testing.T) {
w := &strings.Builder{}
l := NewStreamLogger(w)

l.Info("<message>")

got := w.String()
want := "<message>\n"

if !strings.HasSuffix(got, want) {
t.Errorf("got %q, want suffix of %q", got, want)
}
}
30 changes: 30 additions & 0 deletions testing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package spruce

import (
"log/slog"
"testing"
"time"
)

// TestingT is the subset of [testing.TB] that is used to write logs.
type TestingT interface {
Log(...any)
}

var _ TestingT = (testing.TB)(nil)

// NewTestLogger returns a [slog.Logger] that writes to t.
func NewTestLogger(t TestingT) *slog.Logger {
return slog.New(NewTestHandler(t))
}

// NewTestHandler returns a new [slog.Handler] that writes to t.
func NewTestHandler(t TestingT) slog.Handler {
return &handler{
log: func(s string) error {
t.Log(s)
return nil
},
epoch: time.Now(),
}
}

0 comments on commit 7ecc6f6

Please sign in to comment.