Skip to content

Commit 9077797

Browse files
Add FromContext and WithContext (#11)
* Add `FromContext` and `WithContext` * Add tests for the missing logger case
1 parent 62e19a4 commit 9077797

2 files changed

Lines changed: 69 additions & 6 deletions

File tree

log.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ package log
1010

1111
import (
1212
"bytes"
13+
"context"
1314
"fmt"
1415
"io"
16+
"os"
1517
"slices"
1618
"strconv"
1719
"sync"
@@ -34,8 +36,15 @@ const (
3436
errorStyle = hue.Red | hue.Bold
3537
)
3638

39+
// missingValue is the placeholder text for a missing value in a log line's key value pair.
3740
const missingValue = "<MISSING>"
3841

42+
// ctxKey is the unexported type used for context key so this key never collides with another.
43+
type ctxKey struct{}
44+
45+
// contextKey is the actual key used to store and retrieve a Logger from a Context.
46+
var contextKey = ctxKey{}
47+
3948
// Logger is a command line logger. It is safe to use across concurrently
4049
// executing goroutines.
4150
type Logger struct {
@@ -58,7 +67,7 @@ func New(w io.Writer, options ...Option) *Logger {
5867
w: w,
5968
level: LevelInfo,
6069
timeFormat: time.RFC3339,
61-
timeFunc: now,
70+
timeFunc: func() time.Time { return time.Now().UTC() },
6271
mu: &sync.Mutex{},
6372
}
6473

@@ -71,6 +80,25 @@ func New(w io.Writer, options ...Option) *Logger {
7180
return logger
7281
}
7382

83+
// WithContext stores the given logger in a [context.Context].
84+
//
85+
// The logger may be retrieved from the context with [FromContext].
86+
func WithContext(ctx context.Context, logger *Logger) context.Context {
87+
return context.WithValue(ctx, contextKey, logger)
88+
}
89+
90+
// FromContext returns the [Logger] from a [context.Context].
91+
//
92+
// If the context does not contain a logger, a default logger is returned.
93+
func FromContext(ctx context.Context) *Logger {
94+
logger, ok := ctx.Value(contextKey).(*Logger)
95+
if !ok || logger == nil {
96+
return New(os.Stderr)
97+
}
98+
99+
return logger
100+
}
101+
74102
// With returns a new [Logger] with the given persistent key value pairs.
75103
//
76104
// The returned logger is otherwise an exact clone of the caller.
@@ -220,11 +248,6 @@ func putBuffer(buf *bytes.Buffer) {
220248
bufPool.Put(buf)
221249
}
222250

223-
// now returns the current time with UTC.
224-
func now() time.Time {
225-
return time.Now().UTC()
226-
}
227-
228251
// needsQuotes returns whether s should be displayed as "s".
229252
func needsQuotes(s string) bool {
230253
for _, char := range s {

log_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,46 @@ func TestRace(t *testing.T) {
214214
test.Equal(t, len(lines), n*2, test.Context("expected %d log lines", n*2))
215215
}
216216

217+
func TestContext(t *testing.T) {
218+
t.Run("present", func(t *testing.T) {
219+
buf := &bytes.Buffer{}
220+
221+
// Constantly return the same time
222+
fixedTime := func() time.Time {
223+
fixed, err := time.Parse(time.RFC3339, "2025-04-01T13:34:03Z")
224+
test.Ok(t, err)
225+
226+
return fixed
227+
}
228+
229+
// Configure it a bit so we know we're getting the right one
230+
logger := log.New(buf, log.TimeFunc(fixedTime), log.TimeFormat(time.Kitchen))
231+
232+
logger.Info("Before")
233+
234+
ctx := t.Context()
235+
236+
ctx = log.WithContext(ctx, logger)
237+
238+
after := log.FromContext(ctx)
239+
240+
after.Info("After")
241+
242+
got := buf.String()
243+
244+
test.Diff(t, got, "1:34PM INFO: Before\n1:34PM INFO: After\n")
245+
})
246+
247+
t.Run("missing", func(t *testing.T) {
248+
_, stderr := test.CaptureOutput(t, func() error {
249+
log.FromContext(t.Context()).Info("FromContext")
250+
return nil
251+
})
252+
253+
test.True(t, strings.Contains(stderr, "FromContext"))
254+
})
255+
}
256+
217257
func BenchmarkLogger(b *testing.B) {
218258
hue.Enabled(true) // Force colour
219259

0 commit comments

Comments
 (0)