diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a6d18ac..13dcc99e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `riverlog.LoggerSafely` which provides a non-panic variant of `riverlog.Logger` for use when code may or may not have a context logger available. [PR #1093](https://github.com/riverqueue/river/pull/1093). + ## [0.27.1] - 2025-11-21 ### Fixed diff --git a/riverlog/river_log.go b/riverlog/river_log.go index 73c1dd1b..7528118e 100644 --- a/riverlog/river_log.go +++ b/riverlog/river_log.go @@ -27,14 +27,27 @@ type contextKey struct{} // Logger extracts a logger from context from within the Work body of a worker. // Middleware must be installed on either the worker or client for this function // to be usable. +// +// This variant panics if no logger was available in context. func Logger(ctx context.Context) *slog.Logger { - logger, ok := ctx.Value(contextKey{}).(*slog.Logger) + logger, ok := LoggerSafely(ctx) if !ok { panic("no logger in context; do you have riverlog.Middleware configured?") } return logger } +// LoggerSafely extracts a logger from context from within the Work body of a +// worker. Middleware must be installed on either the worker or client for this +// function to be usable. +// +// This variant returns a boolean that's true if a logger was available in +// context and false otherwise. +func LoggerSafely(ctx context.Context) (*slog.Logger, bool) { + logger, ok := ctx.Value(contextKey{}).(*slog.Logger) + return logger, ok +} + // Middleware injects a context logger into the Work function of workers it's // installed on (or workers of the client it's installed on) which is accessible // with Logger, and which collates all log output to store it to metadata after @@ -79,6 +92,12 @@ type MiddlewareConfig struct { // riverlog.NewMiddleware(func(w io.Writer) slog.Handler { // return slog.NewJSONHandler(w, nil) // }, nil) +// +// With the middleware in place, the logger is available in a work function's +// context: +// +// func (w *MyWorker) Work(ctx context.Context, job *river.Job[MyArgs]) error { +// Logger(ctx).InfoContext(ctx, "Hello from work") func NewMiddleware(newSlogHandler func(w io.Writer) slog.Handler, config *MiddlewareConfig) *Middleware { return &Middleware{ config: defaultConfig(config), diff --git a/riverlog/river_log_test.go b/riverlog/river_log_test.go index 335ff32c..a20d4baf 100644 --- a/riverlog/river_log_test.go +++ b/riverlog/river_log_test.go @@ -16,11 +16,57 @@ import ( "github.com/riverqueue/river/riverdbtest" "github.com/riverqueue/river/riverdriver" "github.com/riverqueue/river/riverdriver/riverpgxv5" + "github.com/riverqueue/river/rivershared/riversharedtest" "github.com/riverqueue/river/rivershared/util/slogutil" "github.com/riverqueue/river/rivertest" "github.com/riverqueue/river/rivertype" ) +func TestLogger(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("Success", func(t *testing.T) { + t.Parallel() + + ctx := context.WithValue(ctx, contextKey{}, riversharedtest.Logger(t)) + + Logger(ctx).InfoContext(ctx, "Hello from logger") + }) + + t.Run("PanicIfNotSet", func(t *testing.T) { + t.Parallel() + + require.PanicsWithValue(t, "no logger in context; do you have riverlog.Middleware configured?", func() { + Logger(ctx).InfoContext(ctx, "This will panic") + }) + }) +} + +func TestLoggerSafely(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("Success", func(t *testing.T) { + t.Parallel() + + ctx := context.WithValue(ctx, contextKey{}, riversharedtest.Logger(t)) + + logger, ok := LoggerSafely(ctx) + require.True(t, ok) + logger.InfoContext(ctx, "Hello from logger") + }) + + t.Run("PanicIfNotSet", func(t *testing.T) { + t.Parallel() + + _, ok := LoggerSafely(ctx) + require.False(t, ok) + }) +} + var _ rivertype.WorkerMiddleware = &Middleware{} type loggingArgs struct {