From c14463fb8e0004717be39c556bdaeb4b4fd54796 Mon Sep 17 00:00:00 2001 From: Noooste <83548733+Noooste@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:32:27 +0200 Subject: [PATCH] feat(logger): add IntoCtx/FromCtx for per-request loggers --- backend/pkg/logger/ctx.go | 29 +++++++++++++++++++++++ backend/pkg/logger/ctx_test.go | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 backend/pkg/logger/ctx.go create mode 100644 backend/pkg/logger/ctx_test.go diff --git a/backend/pkg/logger/ctx.go b/backend/pkg/logger/ctx.go new file mode 100644 index 0000000..70f2ace --- /dev/null +++ b/backend/pkg/logger/ctx.go @@ -0,0 +1,29 @@ +package logger + +import ( + "context" + + "github.com/rs/zerolog" +) + +// ctxKey is unexported so other packages can't collide. +type ctxKey struct{} + +// IntoCtx returns a new context carrying the given logger. Retrieve it later +// with FromCtx. Middleware that builds a per-request logger should call this +// once per request. +func IntoCtx(ctx context.Context, l zerolog.Logger) context.Context { + return context.WithValue(ctx, ctxKey{}, l) +} + +// FromCtx returns the logger bound to ctx. If no logger is bound (e.g. the +// call is outside any middleware, or ctx is nil), it returns the global +// logger. Never returns a zero-value zerolog.Logger. +func FromCtx(ctx context.Context) zerolog.Logger { + if ctx != nil { + if l, ok := ctx.Value(ctxKey{}).(zerolog.Logger); ok { + return l + } + } + return Get().Logger +} diff --git a/backend/pkg/logger/ctx_test.go b/backend/pkg/logger/ctx_test.go new file mode 100644 index 0000000..4d12032 --- /dev/null +++ b/backend/pkg/logger/ctx_test.go @@ -0,0 +1,42 @@ +package logger + +import ( + "bytes" + "context" + "encoding/json" + "strings" + "testing" + + "github.com/rs/zerolog" +) + +func TestFromCtx_ReturnsBoundLogger(t *testing.T) { + var buf bytes.Buffer + l := zerolog.New(&buf).With().Str("request_id", "req-1").Logger() + + ctx := IntoCtx(context.Background(), l) + got := FromCtx(ctx) + got.Info().Msg("hello") + + var parsed map[string]any + line := strings.TrimSpace(buf.String()) + if err := json.Unmarshal([]byte(line), &parsed); err != nil { + t.Fatalf("not JSON: %v — %s", err, line) + } + if parsed["request_id"] != "req-1" { + t.Errorf("request_id = %v, want req-1", parsed["request_id"]) + } + if parsed["message"] != "hello" { + t.Errorf("message = %v, want hello", parsed["message"]) + } +} + +func TestFromCtx_NoLoggerFallsBackToDisabled(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Fatalf("FromCtx panicked: %v", r) + } + }() + l := FromCtx(context.Background()) + l.Info().Msg("should not panic") +}