From 71579fdd6deefa70ae214965dac8d3fafea10687 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:50:41 +0000 Subject: [PATCH 01/21] Initial plan From 73ddbe9909b3c48d77738e78b8243058630030d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:59:40 +0000 Subject: [PATCH 02/21] feat: add context field extraction to log.WithContext via RegisterContextExtractor Add ContextExtractor type and RegisterContextExtractor function to the log package. The default logger's WithContext method now stores the context and extracts registered fields at log time, prepending them as key=value pairs. The requestid middleware auto-registers an extractor via init() so that log.WithContext(c).Info("msg") automatically includes request-id=. Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- log/default.go | 30 ++++++++-- log/default_test.go | 80 ++++++++++++++++++++++++++ log/log.go | 19 ++++++ middleware/requestid/requestid.go | 10 ++++ middleware/requestid/requestid_test.go | 29 ++++++++++ 5 files changed, 164 insertions(+), 4 deletions(-) diff --git a/log/default.go b/log/default.go index 971644b0b5f..aa2f0f6c880 100644 --- a/log/default.go +++ b/log/default.go @@ -17,6 +17,24 @@ type defaultLogger struct { stdlog *log.Logger level Level depth int + ctx context.Context //nolint:containedctx // stored for deferred field extraction +} + +// writeContextFields appends extracted context key-value pairs to buf. +// Each pair is written as "key=value " (trailing space included). +func (l *defaultLogger) writeContextFields(buf *bytebufferpool.ByteBuffer) { + if l.ctx == nil || len(contextExtractors) == 0 { + return + } + for _, extractor := range contextExtractors { + key, value, ok := extractor(l.ctx) + if ok { + buf.WriteString(key) + buf.WriteByte('=') + buf.WriteString(utils.ToString(value)) + buf.WriteByte(' ') + } + } } // privateLog logs a message at a given level log the default logger. @@ -28,6 +46,7 @@ func (l *defaultLogger) privateLog(lv Level, fmtArgs []any) { level := lv.toString() buf := bytebufferpool.Get() buf.WriteString(level) + l.writeContextFields(buf) fmt.Fprint(buf, fmtArgs...) _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error @@ -51,6 +70,7 @@ func (l *defaultLogger) privateLogf(lv Level, format string, fmtArgs []any) { level := lv.toString() buf := bytebufferpool.Get() buf.WriteString(level) + l.writeContextFields(buf) if len(fmtArgs) > 0 { _, _ = fmt.Fprintf(buf, format, fmtArgs...) @@ -78,8 +98,7 @@ func (l *defaultLogger) privateLogw(lv Level, format string, keysAndValues []any level := lv.toString() buf := bytebufferpool.Get() buf.WriteString(level) - - // Write format privateLog buffer + l.writeContextFields(buf) if format != "" { buf.WriteString(format) } @@ -220,12 +239,15 @@ func (l *defaultLogger) Panicw(msg string, keysAndValues ...any) { l.privateLogw(LevelPanic, msg, keysAndValues) } -// WithContext returns a logger that shares the underlying output but adjusts the call depth. -func (l *defaultLogger) WithContext(_ context.Context) CommonLogger { +// WithContext returns a logger that shares the underlying output but carries +// the provided context. Any registered ContextExtractor functions will be +// called at log time to prepend key-value fields extracted from the context. +func (l *defaultLogger) WithContext(ctx context.Context) CommonLogger { return &defaultLogger{ stdlog: l.stdlog, level: l.level, depth: l.depth - 1, + ctx: ctx, } } diff --git a/log/default_test.go b/log/default_test.go index 0cd2c60cead..369245d9598 100644 --- a/log/default_test.go +++ b/log/default_test.go @@ -118,6 +118,86 @@ func Test_CtxLogger(t *testing.T) { "[Panic] work panic\n", string(w.b)) } +type testContextKey struct{} + +func Test_WithContextExtractor(t *testing.T) { + // Save and restore global extractors + saved := contextExtractors + defer func() { contextExtractors = saved }() + contextExtractors = nil + + RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + if v, ok := ctx.Value(testContextKey{}).(string); ok && v != "" { + return "request-id", v, true + } + return "", nil, false + }) + + t.Run("Info with context field", func(t *testing.T) { + var buf bytes.Buffer + l := &defaultLogger{ + stdlog: log.New(&buf, "", 0), + level: LevelTrace, + depth: 4, + } + ctx := context.WithValue(context.Background(), testContextKey{}, "abc-123") + l.WithContext(ctx).Info("hello") + + require.Equal(t, "[Info] request-id=abc-123 hello\n", buf.String()) + }) + + t.Run("Infof with context field", func(t *testing.T) { + var buf bytes.Buffer + l := &defaultLogger{ + stdlog: log.New(&buf, "", 0), + level: LevelTrace, + depth: 4, + } + ctx := context.WithValue(context.Background(), testContextKey{}, "abc-123") + l.WithContext(ctx).Infof("hello %s", "world") + + require.Equal(t, "[Info] request-id=abc-123 hello world\n", buf.String()) + }) + + t.Run("Infow with context field", func(t *testing.T) { + var buf bytes.Buffer + l := &defaultLogger{ + stdlog: log.New(&buf, "", 0), + level: LevelTrace, + depth: 4, + } + ctx := context.WithValue(context.Background(), testContextKey{}, "abc-123") + l.WithContext(ctx).Infow("hello", "key", "value") + + require.Equal(t, "[Info] request-id=abc-123 hello key=value\n", buf.String()) + }) + + t.Run("no context field when value absent", func(t *testing.T) { + var buf bytes.Buffer + l := &defaultLogger{ + stdlog: log.New(&buf, "", 0), + level: LevelTrace, + depth: 4, + } + ctx := context.Background() + l.WithContext(ctx).Info("hello") + + require.Equal(t, "[Info] hello\n", buf.String()) + }) + + t.Run("no context field without WithContext", func(t *testing.T) { + var buf bytes.Buffer + l := &defaultLogger{ + stdlog: log.New(&buf, "", 0), + level: LevelTrace, + depth: 4, + } + l.Info("hello") + + require.Equal(t, "[Info] hello\n", buf.String()) + }) +} + func Test_LogfKeyAndValues(t *testing.T) { tests := []struct { name string diff --git a/log/log.go b/log/log.go index df21e0f971d..3941c12cc34 100644 --- a/log/log.go +++ b/log/log.go @@ -8,6 +8,25 @@ import ( "os" ) +// ContextExtractor extracts a key-value pair from the given context for +// inclusion in log output when using WithContext. +// It returns the log field name, its value, and whether extraction succeeded. +type ContextExtractor func(ctx context.Context) (string, any, bool) + +// contextExtractors holds all registered context field extractors. +// Registration is not concurrent-safe; call RegisterContextExtractor +// during program initialization only. +var contextExtractors []ContextExtractor + +// RegisterContextExtractor registers a function that extracts a key-value pair +// from context for inclusion in log output when using WithContext. +// +// Note that this function is not concurrent-safe and must be called during +// program initialization (e.g. in an init function), before any logging occurs. +func RegisterContextExtractor(extractor ContextExtractor) { + contextExtractors = append(contextExtractors, extractor) +} + // baseLogger defines the minimal logger functionality required by the package. // It allows storing any logger implementation regardless of its generic type. type baseLogger interface { diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index d998670a5c8..c62a445bf64 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -1,7 +1,10 @@ package requestid import ( + "context" + "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" "github.com/gofiber/utils/v2" ) @@ -14,6 +17,13 @@ const ( requestIDKey contextKey = iota ) +func init() { + log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + rid := FromContext(ctx) + return "request-id", rid, rid != "" + }) +} + // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index e6684d40d0a..cf2f98f77e5 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -1,11 +1,14 @@ package requestid import ( + "bytes" "net/http" "net/http/httptest" + "os" "testing" "github.com/gofiber/fiber/v3" + fiberlog "github.com/gofiber/fiber/v3/log" "github.com/stretchr/testify/require" ) @@ -233,3 +236,29 @@ func Test_RequestID_FromContext_Types(t *testing.T) { require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) } + +func Test_RequestID_LogWithContext(t *testing.T) { + reqID := "test-request-id-456" + + app := fiber.New() + app.Use(New(Config{ + Generator: func() string { + return reqID + }, + })) + + var logOutput bytes.Buffer + fiberlog.SetOutput(&logOutput) + defer fiberlog.SetOutput(os.Stderr) + + app.Get("/", func(c fiber.Ctx) error { + fiberlog.WithContext(c).Info("hello from handler") + return c.SendStatus(fiber.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Contains(t, logOutput.String(), "request-id="+reqID) + require.Contains(t, logOutput.String(), "hello from handler") +} From e862b9420eea3c49c742db69ba89af94e3b5a685 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:06:15 +0000 Subject: [PATCH 03/21] fix: apply betteralign struct field ordering for defaultLogger Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- log/default.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/log/default.go b/log/default.go index aa2f0f6c880..a47860be716 100644 --- a/log/default.go +++ b/log/default.go @@ -14,10 +14,10 @@ import ( var _ AllLogger[*log.Logger] = (*defaultLogger)(nil) type defaultLogger struct { + ctx context.Context //nolint:containedctx // stored for deferred field extraction stdlog *log.Logger level Level depth int - ctx context.Context //nolint:containedctx // stored for deferred field extraction } // writeContextFields appends extracted context key-value pairs to buf. From 2c4216ccddee728e8fa4d76b1e0a3a672e053a9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:07:12 +0000 Subject: [PATCH 04/21] docs: improve documentation for context extractors and requestid init Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- log/log.go | 7 +++++-- middleware/requestid/requestid.go | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/log/log.go b/log/log.go index 3941c12cc34..09c81fba634 100644 --- a/log/log.go +++ b/log/log.go @@ -14,8 +14,11 @@ import ( type ContextExtractor func(ctx context.Context) (string, any, bool) // contextExtractors holds all registered context field extractors. -// Registration is not concurrent-safe; call RegisterContextExtractor -// during program initialization only. +// +// This slice is read during logging and written during registration. +// All calls to RegisterContextExtractor must happen during program +// initialization (e.g. in init functions or before starting goroutines), +// before any logging occurs, to avoid data races. var contextExtractors []ContextExtractor // RegisterContextExtractor registers a function that extracts a key-value pair diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index c62a445bf64..84fb9f932b2 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -18,6 +18,9 @@ const ( ) func init() { + // Register a context extractor so that log.WithContext(c) automatically + // includes the request ID when the requestid middleware is in use. + // An empty request ID (no middleware or middleware skipped) is omitted. log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { rid := FromContext(ctx) return "request-id", rid, rid != "" From 45494497df036769d13ea39b0501a9f6cc2ef5d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:50:36 +0000 Subject: [PATCH 05/21] refactor: replace init() with sync.Once in New(), add basicauth extractor, update docs - Removed init() from requestid, register extractor via sync.Once in New() - Added context extractor for basicauth middleware (username field) - Added tests for WithContext with fiber.Ctx and context.Context types - Updated docs/api/log.md with context extractors documentation - Updated docs/whats_new.md with context-aware logging section Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- docs/api/log.md | 33 +++++++++++++++++++++++- docs/whats_new.md | 16 ++++++++++++ log/log.go | 7 +++--- middleware/basicauth/basicauth.go | 16 ++++++++++++ middleware/requestid/requestid.go | 23 ++++++++++------- middleware/requestid/requestid_test.go | 35 ++++++++++++++++++++++++-- 6 files changed, 115 insertions(+), 15 deletions(-) diff --git a/docs/api/log.md b/docs/api/log.md index b1457caf091..31ca6a30509 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -198,7 +198,38 @@ commonLogger := log.WithContext(ctx) commonLogger.Info("info") ``` -Context binding adds request-specific data for easier tracing. +Context binding adds request-specific data for easier tracing. The method accepts any value implementing `context.Context`, including `fiber.Ctx`, `*fasthttp.RequestCtx`, and standard `context.Context`. + +### Automatic Context Fields + +Middleware that stores values in the request context can register extractors so that `log.WithContext` automatically includes those values in every log entry. The `requestid` and `basicauth` middlewares register extractors when their `New()` constructor is called. + +```go +app.Use(requestid.New()) + +app.Get("/", func(c fiber.Ctx) error { + // Automatically includes request-id= in the log output + log.WithContext(c).Info("processing request") + return c.SendString("OK") +}) +``` + +### Custom Context Extractors + +Use `log.RegisterContextExtractor` to register your own extractors. Each extractor receives the bound context and returns a field name, value, and success flag: + +```go +log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + if traceID, ok := ctx.Value(traceIDKey).(string); ok && traceID != "" { + return "trace-id", traceID, true + } + return "", nil, false +}) +``` + +:::note +`RegisterContextExtractor` is not concurrent-safe and must be called during program initialization (e.g. in an `init` function or middleware constructor). +::: ## Logger diff --git a/docs/whats_new.md b/docs/whats_new.md index 838da9a35f4..6adf30f83e7 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -1214,6 +1214,22 @@ app.Use(logger.New(logger.Config{ })) ``` +### Context-Aware Logging + +`log.WithContext` now automatically includes context fields extracted by middleware. Middleware such as `requestid` and `basicauth` register extractors when their `New()` constructor is called. When you pass a `fiber.Ctx` (or any `context.Context`) to `log.WithContext`, registered fields are prepended to every log entry. + +```go +app.Use(requestid.New()) + +app.Get("/", func(c fiber.Ctx) error { + // Output: [Info] request-id=abc-123 processing request + log.WithContext(c).Info("processing request") + return c.SendString("OK") +}) +``` + +Custom extractors can be registered via `log.RegisterContextExtractor`. See [Log API docs](./api/log.md#custom-context-extractors) for details. + ## 📦 Storage Interface The storage interface has been updated to include new subset of methods with `WithContext` suffix. These methods allow you to pass a context to the storage operations, enabling better control over timeouts and cancellation if needed. This is particularly useful when storage implementations used outside of the Fiber core, such as in background jobs or long-running tasks. diff --git a/log/log.go b/log/log.go index 09c81fba634..2e807acc169 100644 --- a/log/log.go +++ b/log/log.go @@ -17,15 +17,16 @@ type ContextExtractor func(ctx context.Context) (string, any, bool) // // This slice is read during logging and written during registration. // All calls to RegisterContextExtractor must happen during program -// initialization (e.g. in init functions or before starting goroutines), -// before any logging occurs, to avoid data races. +// initialization (e.g. in init functions, or in middleware constructors +// using sync.Once), before any logging occurs, to avoid data races. var contextExtractors []ContextExtractor // RegisterContextExtractor registers a function that extracts a key-value pair // from context for inclusion in log output when using WithContext. // // Note that this function is not concurrent-safe and must be called during -// program initialization (e.g. in an init function), before any logging occurs. +// program initialization (e.g. in an init function or middleware constructor +// using sync.Once), before any logging occurs. func RegisterContextExtractor(extractor ContextExtractor) { contextExtractors = append(contextExtractors, extractor) } diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index e8878e0f9e7..36d2f17eef9 100644 --- a/middleware/basicauth/basicauth.go +++ b/middleware/basicauth/basicauth.go @@ -1,13 +1,16 @@ package basicauth import ( + "context" "encoding/base64" "errors" "strings" + "sync" "unicode" "unicode/utf8" "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" "github.com/gofiber/utils/v2" "golang.org/x/text/unicode/norm" ) @@ -23,11 +26,24 @@ const ( const basicScheme = "Basic" +// registerExtractor ensures the log context extractor for the authenticated +// username is registered exactly once. +var registerExtractor sync.Once + // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) + // Register a log context extractor so that log.WithContext(c) automatically + // includes the authenticated username when basicauth middleware is in use. + registerExtractor.Do(func() { + log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + username := UsernameFromContext(ctx) + return "username", username, username != "" + }) + }) + var cerr base64.CorruptInputError // Return new handler diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index 84fb9f932b2..77c630f990e 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -2,6 +2,7 @@ package requestid import ( "context" + "sync" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/log" @@ -17,21 +18,25 @@ const ( requestIDKey contextKey = iota ) -func init() { - // Register a context extractor so that log.WithContext(c) automatically - // includes the request ID when the requestid middleware is in use. - // An empty request ID (no middleware or middleware skipped) is omitted. - log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { - rid := FromContext(ctx) - return "request-id", rid, rid != "" - }) -} +// registerExtractor ensures the log context extractor for request IDs is +// registered exactly once, regardless of how many times New() is called. +var registerExtractor sync.Once // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) + // Register a log context extractor so that log.WithContext(c) automatically + // includes the request ID when the requestid middleware is in use. + // An empty request ID (no middleware or middleware skipped) is omitted. + registerExtractor.Do(func() { + log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + rid := FromContext(ctx) + return "request-id", rid, rid != "" + }) + }) + // Return new handler return func(c fiber.Ctx) error { // Don't execute middleware if Next returns true diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index cf2f98f77e5..b2e71602cd7 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -237,8 +237,10 @@ func Test_RequestID_FromContext_Types(t *testing.T) { require.Equal(t, fiber.StatusOK, resp.StatusCode) } -func Test_RequestID_LogWithContext(t *testing.T) { - reqID := "test-request-id-456" +// Test_RequestID_LogWithContext_FiberCtx verifies that log.WithContext(c) +// automatically includes the request ID when a fiber.Ctx is passed. +func Test_RequestID_LogWithContext_FiberCtx(t *testing.T) { + reqID := "test-request-id-fiber" app := fiber.New() app.Use(New(Config{ @@ -262,3 +264,32 @@ func Test_RequestID_LogWithContext(t *testing.T) { require.Contains(t, logOutput.String(), "request-id="+reqID) require.Contains(t, logOutput.String(), "hello from handler") } + +// Test_RequestID_LogWithContext_ContextContext verifies that log.WithContext +// works with a context.Context obtained via c.Context() when PassLocalsToContext +// is enabled. +func Test_RequestID_LogWithContext_ContextContext(t *testing.T) { + reqID := "test-request-id-context" + + app := fiber.New(fiber.Config{PassLocalsToContext: true}) + app.Use(New(Config{ + Generator: func() string { + return reqID + }, + })) + + var logOutput bytes.Buffer + fiberlog.SetOutput(&logOutput) + defer fiberlog.SetOutput(os.Stderr) + + app.Get("/", func(c fiber.Ctx) error { + fiberlog.WithContext(c.Context()).Info("hello via context.Context") + return c.SendStatus(fiber.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Contains(t, logOutput.String(), "request-id="+reqID) + require.Contains(t, logOutput.String(), "hello via context.Context") +} From ae206fbef49c3554e23619c4c0fdfb05d7b61e57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:22:29 +0000 Subject: [PATCH 06/21] feat: add log context extractors for csrf, keyauth, and session middlewares - csrf: registers csrf-token extractor via sync.Once in New() - keyauth: registers api-key extractor via sync.Once in New() - session: registers session-id extractor via sync.Once in NewWithStore() - Added Test_CSRF_LogWithContext, Test_KeyAuth_LogWithContext, Test_Session_LogWithContext - Updated docs/api/log.md and docs/whats_new.md to list all 5 middlewares Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- docs/api/log.md | 10 ++++++++- docs/whats_new.md | 2 +- middleware/csrf/csrf.go | 17 ++++++++++++++++ middleware/csrf/csrf_test.go | 23 +++++++++++++++++++++ middleware/keyauth/keyauth.go | 17 ++++++++++++++++ middleware/keyauth/keyauth_test.go | 29 +++++++++++++++++++++++++++ middleware/session/middleware.go | 19 ++++++++++++++++++ middleware/session/middleware_test.go | 25 +++++++++++++++++++++++ 8 files changed, 140 insertions(+), 2 deletions(-) diff --git a/docs/api/log.md b/docs/api/log.md index 31ca6a30509..9be57b17ace 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -202,7 +202,15 @@ Context binding adds request-specific data for easier tracing. The method accept ### Automatic Context Fields -Middleware that stores values in the request context can register extractors so that `log.WithContext` automatically includes those values in every log entry. The `requestid` and `basicauth` middlewares register extractors when their `New()` constructor is called. +Middleware that stores values in the request context can register extractors so that `log.WithContext` automatically includes those values in every log entry. The following middlewares register extractors when their `New()` constructor is called: + +| Middleware | Log Field | Description | +| ------------ | -------------- | ------------------------- | +| `requestid` | `request-id` | Request identifier | +| `basicauth` | `username` | Authenticated username | +| `keyauth` | `api-key` | API key token | +| `csrf` | `csrf-token` | CSRF token | +| `session` | `session-id` | Session identifier | ```go app.Use(requestid.New()) diff --git a/docs/whats_new.md b/docs/whats_new.md index 6adf30f83e7..b53a0a6360b 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -1216,7 +1216,7 @@ app.Use(logger.New(logger.Config{ ### Context-Aware Logging -`log.WithContext` now automatically includes context fields extracted by middleware. Middleware such as `requestid` and `basicauth` register extractors when their `New()` constructor is called. When you pass a `fiber.Ctx` (or any `context.Context`) to `log.WithContext`, registered fields are prepended to every log entry. +`log.WithContext` now automatically includes context fields extracted by middleware. Middleware such as `requestid`, `basicauth`, `keyauth`, `csrf`, and `session` register extractors when their `New()` constructor is called. When you pass a `fiber.Ctx` (or any `context.Context`) to `log.WithContext`, registered fields are prepended to every log entry. ```go app.Use(requestid.New()) diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index 559d05ea331..f1eca1defdc 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -1,11 +1,13 @@ package csrf import ( + "context" "errors" "fmt" "net/url" "slices" "strings" + "sync" "time" "github.com/gofiber/utils/v2" @@ -13,6 +15,7 @@ import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" + "github.com/gofiber/fiber/v3/log" ) var ( @@ -46,11 +49,25 @@ const ( handlerKey ) +// registerExtractor ensures the log context extractor for CSRF tokens is +// registered exactly once, regardless of how many times New() is called. +var registerExtractor sync.Once + // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) + // Register a log context extractor so that log.WithContext(c) automatically + // includes the CSRF token when the csrf middleware is in use. + // An empty token (no middleware or middleware skipped) is omitted. + registerExtractor.Do(func() { + log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + token := TokenFromContext(ctx) + return "csrf-token", token, token != "" + }) + }) + redactKeys := !cfg.DisableValueRedaction maskValue := func(value string) string { diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index 5637edae570..49ade35f114 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -1,17 +1,20 @@ package csrf import ( + "bytes" "context" "errors" "net" "net/http" "net/http/httptest" + "os" "strings" "testing" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" + fiberlog "github.com/gofiber/fiber/v3/log" "github.com/gofiber/fiber/v3/middleware/session" "github.com/gofiber/utils/v2" "github.com/stretchr/testify/require" @@ -2484,3 +2487,23 @@ func Test_CSRF_Extractors_ErrorTypes(t *testing.T) { }) } } + +func Test_CSRF_LogWithContext(t *testing.T) { + app := fiber.New() + app.Use(New()) + + var logOutput bytes.Buffer + fiberlog.SetOutput(&logOutput) + defer fiberlog.SetOutput(os.Stderr) + + app.Get("/", func(c fiber.Ctx) error { + fiberlog.WithContext(c).Info("csrf test") + return c.SendStatus(fiber.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Contains(t, logOutput.String(), "csrf-token=") + require.Contains(t, logOutput.String(), "csrf test") +} diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index 109c82d20c1..e167a24dc75 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -1,12 +1,15 @@ package keyauth import ( + "context" "errors" "fmt" "strings" + "sync" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" + "github.com/gofiber/fiber/v3/log" "github.com/gofiber/utils/v2" ) @@ -22,11 +25,25 @@ const ( // ErrMissingOrMalformedAPIKey is returned when the API key is missing or invalid. var ErrMissingOrMalformedAPIKey = errors.New("missing or invalid API Key") +// registerExtractor ensures the log context extractor for API keys is +// registered exactly once, regardless of how many times New() is called. +var registerExtractor sync.Once + // New creates a new middleware handler func New(config ...Config) fiber.Handler { // Init config cfg := configDefault(config...) + // Register a log context extractor so that log.WithContext(c) automatically + // includes the API key when the keyauth middleware is in use. + // An empty token (no middleware or middleware skipped) is omitted. + registerExtractor.Do(func() { + log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + token := TokenFromContext(ctx) + return "api-key", token, token != "" + }) + }) + // Determine the auth schemes from the extractor chain. authSchemes := getAuthSchemes(cfg.Extractor) diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index 20771e6615f..f52b42624a4 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -1,12 +1,14 @@ package keyauth import ( + "bytes" "context" "errors" "io" "net/http" "net/http/httptest" "net/url" + "os" "strings" "testing" @@ -15,6 +17,7 @@ import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" + fiberlog "github.com/gofiber/fiber/v3/log" ) const CorrectKey = "correct-token_123./~+" @@ -1130,3 +1133,29 @@ func Test_New_ErrorURIAbsolute(t *testing.T) { }) }) } + +func Test_KeyAuth_LogWithContext(t *testing.T) { + app := fiber.New() + app.Use(New(Config{ + Validator: func(_ fiber.Ctx, key string) (bool, error) { + return key == CorrectKey, nil + }, + })) + + var logOutput bytes.Buffer + fiberlog.SetOutput(&logOutput) + defer fiberlog.SetOutput(os.Stderr) + + app.Get("/", func(c fiber.Ctx) error { + fiberlog.WithContext(c).Info("keyauth test") + return c.SendStatus(fiber.StatusOK) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody) + req.Header.Set("Authorization", "Bearer "+CorrectKey) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Contains(t, logOutput.String(), "api-key="+CorrectKey) + require.Contains(t, logOutput.String(), "keyauth test") +} diff --git a/middleware/session/middleware.go b/middleware/session/middleware.go index 78c2ef05384..6f846945392 100644 --- a/middleware/session/middleware.go +++ b/middleware/session/middleware.go @@ -3,10 +3,12 @@ package session import ( + "context" "errors" "sync" "github.com/gofiber/fiber/v3" + "github.com/gofiber/fiber/v3/log" ) // Middleware holds session data and configuration. @@ -36,6 +38,10 @@ var ( return &Middleware{} }, } + + // registerExtractor ensures the log context extractor for session IDs is + // registered exactly once. + registerExtractor sync.Once ) // New initializes session middleware with optional configuration. @@ -81,6 +87,19 @@ func NewWithStore(config ...Config) (fiber.Handler, *Store) { cfg.Store = NewStore(cfg) } + // Register a log context extractor so that log.WithContext(c) automatically + // includes the session ID when the session middleware is in use. + registerExtractor.Do(func() { + log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + m := FromContext(ctx) + if m == nil || m.Session == nil { + return "", nil, false + } + id := m.Session.ID() + return "session-id", id, id != "" + }) + }) + handler := func(c fiber.Ctx) error { if cfg.Next != nil && cfg.Next(c) { return c.Next() diff --git a/middleware/session/middleware_test.go b/middleware/session/middleware_test.go index a48b6b1b49c..77133c5f1da 100644 --- a/middleware/session/middleware_test.go +++ b/middleware/session/middleware_test.go @@ -1,9 +1,11 @@ package session import ( + "bytes" "fmt" "net/http" "net/http/httptest" + "os" "sort" "strings" "sync" @@ -12,6 +14,7 @@ import ( "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/extractors" + fiberlog "github.com/gofiber/fiber/v3/log" "github.com/gofiber/utils/v2" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" @@ -590,3 +593,25 @@ func Test_Session_Middleware_Store(t *testing.T) { h(ctx) require.Equal(t, fiber.StatusOK, ctx.Response.StatusCode()) } + +func Test_Session_LogWithContext(t *testing.T) { + app := fiber.New() + app.Use(New()) + + var logOutput bytes.Buffer + fiberlog.SetOutput(&logOutput) + defer fiberlog.SetOutput(os.Stderr) + + app.Get("/", func(c fiber.Ctx) error { + sess := FromContext(c) + require.NotNil(t, sess) + fiberlog.WithContext(c).Info("session test") + return c.SendStatus(fiber.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Contains(t, logOutput.String(), "session-id=") + require.Contains(t, logOutput.String(), "session test") +} From a0d40547b15628d69aee93355d64cd34b0f4c15d Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:27:12 -0500 Subject: [PATCH 07/21] Update log/log.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- log/log.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/log/log.go b/log/log.go index 2e807acc169..0f2037e652d 100644 --- a/log/log.go +++ b/log/log.go @@ -16,19 +16,23 @@ type ContextExtractor func(ctx context.Context) (string, any, bool) // contextExtractors holds all registered context field extractors. // // This slice is read during logging and written during registration. -// All calls to RegisterContextExtractor must happen during program -// initialization (e.g. in init functions, or in middleware constructors -// using sync.Once), before any logging occurs, to avoid data races. +// Registrations use a copy-on-write strategy so that readers always see +// an immutable snapshot of the slice and never observe concurrent mutation +// of the underlying backing array. var contextExtractors []ContextExtractor // RegisterContextExtractor registers a function that extracts a key-value pair // from context for inclusion in log output when using WithContext. // -// Note that this function is not concurrent-safe and must be called during -// program initialization (e.g. in an init function or middleware constructor -// using sync.Once), before any logging occurs. +// This function is safe to call concurrently with logging: it uses a +// copy-on-write strategy so that existing readers continue to see their +// previous slice snapshot while new registrations are applied to a new slice. func RegisterContextExtractor(extractor ContextExtractor) { - contextExtractors = append(contextExtractors, extractor) + n := len(contextExtractors) + next := make([]ContextExtractor, n+1) + copy(next, contextExtractors) + next[n] = extractor + contextExtractors = next } // baseLogger defines the minimal logger functionality required by the package. From 041145b8ff763ad09cf7ffa8184978da10f9e45c Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 23 Feb 2026 10:27:30 -0500 Subject: [PATCH 08/21] Update log/log.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- log/log.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/log/log.go b/log/log.go index 0f2037e652d..73babf0b884 100644 --- a/log/log.go +++ b/log/log.go @@ -28,6 +28,9 @@ var contextExtractors []ContextExtractor // copy-on-write strategy so that existing readers continue to see their // previous slice snapshot while new registrations are applied to a new slice. func RegisterContextExtractor(extractor ContextExtractor) { + if extractor == nil { + panic("log: RegisterContextExtractor called with nil extractor") + } n := len(contextExtractors) next := make([]ContextExtractor, n+1) copy(next, contextExtractors) From 40bd5d0881734991305056da7df221b79e315b4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:29:35 +0000 Subject: [PATCH 09/21] security: redact sensitive values (api-key, csrf-token) in log context extractors - keyauth: log redacted api-key (first 4 chars + ****) - csrf: log [redacted] instead of actual token - Updated tests and docs to reflect redacted values Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- docs/api/log.md | 14 +++++++------- middleware/csrf/csrf.go | 7 +++++-- middleware/csrf/csrf_test.go | 2 +- middleware/keyauth/keyauth.go | 17 +++++++++++++++-- middleware/keyauth/keyauth_test.go | 2 +- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/docs/api/log.md b/docs/api/log.md index 9be57b17ace..0e26270fde2 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -204,13 +204,13 @@ Context binding adds request-specific data for easier tracing. The method accept Middleware that stores values in the request context can register extractors so that `log.WithContext` automatically includes those values in every log entry. The following middlewares register extractors when their `New()` constructor is called: -| Middleware | Log Field | Description | -| ------------ | -------------- | ------------------------- | -| `requestid` | `request-id` | Request identifier | -| `basicauth` | `username` | Authenticated username | -| `keyauth` | `api-key` | API key token | -| `csrf` | `csrf-token` | CSRF token | -| `session` | `session-id` | Session identifier | +| Middleware | Log Field | Description | +| ------------ | -------------- | -------------------------------- | +| `requestid` | `request-id` | Request identifier | +| `basicauth` | `username` | Authenticated username | +| `keyauth` | `api-key` | API key token (redacted) | +| `csrf` | `csrf-token` | CSRF token (redacted) | +| `session` | `session-id` | Session identifier | ```go app.Use(requestid.New()) diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index f1eca1defdc..4e3c0e0d1df 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -59,12 +59,15 @@ func New(config ...Config) fiber.Handler { cfg := configDefault(config...) // Register a log context extractor so that log.WithContext(c) automatically - // includes the CSRF token when the csrf middleware is in use. + // includes a redacted CSRF token when the csrf middleware is in use. // An empty token (no middleware or middleware skipped) is omitted. registerExtractor.Do(func() { log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { token := TokenFromContext(ctx) - return "csrf-token", token, token != "" + if token == "" { + return "", nil, false + } + return "csrf-token", redactedKey, true }) }) diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index 49ade35f114..7946666d7d5 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -2504,6 +2504,6 @@ func Test_CSRF_LogWithContext(t *testing.T) { resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) - require.Contains(t, logOutput.String(), "csrf-token=") + require.Contains(t, logOutput.String(), "csrf-token=[redacted]") require.Contains(t, logOutput.String(), "csrf test") } diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index e167a24dc75..bf8ef0c5bd8 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -35,12 +35,15 @@ func New(config ...Config) fiber.Handler { cfg := configDefault(config...) // Register a log context extractor so that log.WithContext(c) automatically - // includes the API key when the keyauth middleware is in use. + // includes a redacted API key when the keyauth middleware is in use. // An empty token (no middleware or middleware skipped) is omitted. registerExtractor.Do(func() { log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { token := TokenFromContext(ctx) - return "api-key", token, token != "" + if token == "" { + return "", nil, false + } + return "api-key", redactValue(token), true }) }) @@ -134,3 +137,13 @@ func getAuthSchemes(e extractors.Extractor) []string { } return schemes } + +// redactValue returns a masked version of a sensitive value for safe logging. +// It shows the first 4 characters followed by "****" for values longer than +// 8 characters, or "****" for shorter values. +func redactValue(s string) string { + if len(s) > 8 { + return s[:4] + "****" + } + return "****" +} diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index f52b42624a4..0842f67f2a1 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -1156,6 +1156,6 @@ func Test_KeyAuth_LogWithContext(t *testing.T) { resp, err := app.Test(req) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) - require.Contains(t, logOutput.String(), "api-key="+CorrectKey) + require.Contains(t, logOutput.String(), "api-key=corr****") require.Contains(t, logOutput.String(), "keyauth test") } From add61a5d12c82b436b5bb9d03a2c465da9d68e67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 00:31:57 +0000 Subject: [PATCH 10/21] fix: skip context extractors that return empty key to prevent malformed log output Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- log/default.go | 2 +- log/default_test.go | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/log/default.go b/log/default.go index a47860be716..49c0451164e 100644 --- a/log/default.go +++ b/log/default.go @@ -28,7 +28,7 @@ func (l *defaultLogger) writeContextFields(buf *bytebufferpool.ByteBuffer) { } for _, extractor := range contextExtractors { key, value, ok := extractor(l.ctx) - if ok { + if ok && key != "" { buf.WriteString(key) buf.WriteByte('=') buf.WriteString(utils.ToString(value)) diff --git a/log/default_test.go b/log/default_test.go index 369245d9598..1af1b246fb4 100644 --- a/log/default_test.go +++ b/log/default_test.go @@ -196,6 +196,28 @@ func Test_WithContextExtractor(t *testing.T) { require.Equal(t, "[Info] hello\n", buf.String()) }) + + t.Run("empty key extractor is skipped", func(t *testing.T) { + // Save and restore extractors for this subtest + savedInner := contextExtractors + defer func() { contextExtractors = savedInner }() + + // Add an extractor that returns ok=true but key="" + RegisterContextExtractor(func(_ context.Context) (string, any, bool) { + return "", "should-not-appear", true + }) + + var buf bytes.Buffer + l := &defaultLogger{ + stdlog: log.New(&buf, "", 0), + level: LevelTrace, + depth: 4, + } + ctx := context.WithValue(context.Background(), testContextKey{}, "abc-123") + l.WithContext(ctx).Info("hello") + + require.Equal(t, "[Info] request-id=abc-123 hello\n", buf.String()) + }) } func Test_LogfKeyAndValues(t *testing.T) { From 1b7896769014372b608422bec56d0e516fc5fc92 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 03:27:36 +0000 Subject: [PATCH 11/21] fix: make RegisterContextExtractor concurrent-safe with RWMutex, redact session IDs - Add sync.RWMutex to guard contextExtractors for concurrent reads/writes - Add loadContextExtractors() helper for safe snapshot reads in writeContextFields - Use copy-on-write in RegisterContextExtractor to keep existing snapshots stable - Redact session IDs in session middleware (first 4 chars + ****) to prevent session hijacking if logs are exposed - Add redactSessionID() helper and tests for it - Update docs/api/log.md note to accurately describe thread-safety - Update docs table to show session-id as redacted - Fix test save/restore patterns to use the mutex correctly Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- docs/api/log.md | 4 ++-- log/default.go | 5 +++-- log/default_test.go | 22 +++++++++++++++---- log/log.go | 31 ++++++++++++++++++++------- middleware/session/middleware.go | 19 ++++++++++++++-- middleware/session/middleware_test.go | 29 +++++++++++++++++++++++++ 6 files changed, 92 insertions(+), 18 deletions(-) diff --git a/docs/api/log.md b/docs/api/log.md index 0e26270fde2..96097326870 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -210,7 +210,7 @@ Middleware that stores values in the request context can register extractors so | `basicauth` | `username` | Authenticated username | | `keyauth` | `api-key` | API key token (redacted) | | `csrf` | `csrf-token` | CSRF token (redacted) | -| `session` | `session-id` | Session identifier | +| `session` | `session-id` | Session identifier (redacted) | ```go app.Use(requestid.New()) @@ -236,7 +236,7 @@ log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { ``` :::note -`RegisterContextExtractor` is not concurrent-safe and must be called during program initialization (e.g. in an `init` function or middleware constructor). +`RegisterContextExtractor` may be called at any time, including while your application is handling requests and emitting logs. Registrations are safe to perform concurrently with logging. In practice, register extractors during program initialization (e.g. in an `init` function or middleware constructor) so that they are in place before requests are processed. ::: ## Logger diff --git a/log/default.go b/log/default.go index 49c0451164e..25fa02a3ccf 100644 --- a/log/default.go +++ b/log/default.go @@ -23,10 +23,11 @@ type defaultLogger struct { // writeContextFields appends extracted context key-value pairs to buf. // Each pair is written as "key=value " (trailing space included). func (l *defaultLogger) writeContextFields(buf *bytebufferpool.ByteBuffer) { - if l.ctx == nil || len(contextExtractors) == 0 { + if l.ctx == nil { return } - for _, extractor := range contextExtractors { + extractors := loadContextExtractors() + for _, extractor := range extractors { key, value, ok := extractor(l.ctx) if ok && key != "" { buf.WriteString(key) diff --git a/log/default_test.go b/log/default_test.go index 1af1b246fb4..d4d4f9900ab 100644 --- a/log/default_test.go +++ b/log/default_test.go @@ -121,10 +121,17 @@ func Test_CtxLogger(t *testing.T) { type testContextKey struct{} func Test_WithContextExtractor(t *testing.T) { - // Save and restore global extractors + // Save and restore global extractors using the mutex for correctness. + contextExtractorsMu.Lock() saved := contextExtractors - defer func() { contextExtractors = saved }() contextExtractors = nil + contextExtractorsMu.Unlock() + + defer func() { + contextExtractorsMu.Lock() + contextExtractors = saved + contextExtractorsMu.Unlock() + }() RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { if v, ok := ctx.Value(testContextKey{}).(string); ok && v != "" { @@ -198,9 +205,16 @@ func Test_WithContextExtractor(t *testing.T) { }) t.Run("empty key extractor is skipped", func(t *testing.T) { - // Save and restore extractors for this subtest + // Save and restore extractors for this subtest using the mutex. + contextExtractorsMu.Lock() savedInner := contextExtractors - defer func() { contextExtractors = savedInner }() + contextExtractorsMu.Unlock() + + defer func() { + contextExtractorsMu.Lock() + contextExtractors = savedInner + contextExtractorsMu.Unlock() + }() // Add an extractor that returns ok=true but key="" RegisterContextExtractor(func(_ context.Context) (string, any, bool) { diff --git a/log/log.go b/log/log.go index 73babf0b884..cb667da8525 100644 --- a/log/log.go +++ b/log/log.go @@ -6,6 +6,7 @@ import ( "io" "log" "os" + "sync" ) // ContextExtractor extracts a key-value pair from the given context for @@ -13,29 +14,43 @@ import ( // It returns the log field name, its value, and whether extraction succeeded. type ContextExtractor func(ctx context.Context) (string, any, bool) +// contextExtractorsMu guards contextExtractors for concurrent registration +// and snapshot reads. +var contextExtractorsMu sync.RWMutex + // contextExtractors holds all registered context field extractors. -// -// This slice is read during logging and written during registration. -// Registrations use a copy-on-write strategy so that readers always see -// an immutable snapshot of the slice and never observe concurrent mutation -// of the underlying backing array. +// Use loadContextExtractors to obtain a safe snapshot for iteration. var contextExtractors []ContextExtractor +// loadContextExtractors returns an immutable snapshot of the registered +// extractors. The returned slice must not be modified. +func loadContextExtractors() []ContextExtractor { + contextExtractorsMu.RLock() + snapshot := contextExtractors + contextExtractorsMu.RUnlock() + return snapshot +} + // RegisterContextExtractor registers a function that extracts a key-value pair // from context for inclusion in log output when using WithContext. // -// This function is safe to call concurrently with logging: it uses a -// copy-on-write strategy so that existing readers continue to see their -// previous slice snapshot while new registrations are applied to a new slice. +// This function is safe to call concurrently with logging and with other +// registrations. All calls to RegisterContextExtractor should happen during +// program initialization (e.g. in an init function or middleware constructor) +// so that extractors are in place before requests are processed. func RegisterContextExtractor(extractor ContextExtractor) { if extractor == nil { panic("log: RegisterContextExtractor called with nil extractor") } + contextExtractorsMu.Lock() + // Copy-on-write: always allocate a new backing array so snapshots taken + // by concurrent readers remain stable. n := len(contextExtractors) next := make([]ContextExtractor, n+1) copy(next, contextExtractors) next[n] = extractor contextExtractors = next + contextExtractorsMu.Unlock() } // baseLogger defines the minimal logger functionality required by the package. diff --git a/middleware/session/middleware.go b/middleware/session/middleware.go index 6f846945392..cdff830532d 100644 --- a/middleware/session/middleware.go +++ b/middleware/session/middleware.go @@ -88,7 +88,9 @@ func NewWithStore(config ...Config) (fiber.Handler, *Store) { } // Register a log context extractor so that log.WithContext(c) automatically - // includes the session ID when the session middleware is in use. + // includes a redacted session ID when the session middleware is in use. + // Session IDs are bearer secrets, so only the first 4 characters are logged + // to enable correlation without exposing the full token. registerExtractor.Do(func() { log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { m := FromContext(ctx) @@ -96,7 +98,10 @@ func NewWithStore(config ...Config) (fiber.Handler, *Store) { return "", nil, false } id := m.Session.ID() - return "session-id", id, id != "" + if id == "" { + return "", nil, false + } + return "session-id", redactSessionID(id), true }) }) @@ -203,6 +208,16 @@ func FromContext(ctx any) *Middleware { return nil } +// redactSessionID returns a masked version of a session ID for safe logging. +// Session IDs are bearer secrets; only the first 4 characters are retained so +// that log entries can still be correlated without exposing the full token. +func redactSessionID(id string) string { + if len(id) > 8 { + return id[:4] + "****" + } + return "****" +} + // Set sets a key-value pair in the session. // // Parameters: diff --git a/middleware/session/middleware_test.go b/middleware/session/middleware_test.go index 77133c5f1da..eed043f0468 100644 --- a/middleware/session/middleware_test.go +++ b/middleware/session/middleware_test.go @@ -602,9 +602,12 @@ func Test_Session_LogWithContext(t *testing.T) { fiberlog.SetOutput(&logOutput) defer fiberlog.SetOutput(os.Stderr) + var capturedID string + app.Get("/", func(c fiber.Ctx) error { sess := FromContext(c) require.NotNil(t, sess) + capturedID = sess.Session.ID() fiberlog.WithContext(c).Info("session test") return c.SendStatus(fiber.StatusOK) }) @@ -612,6 +615,32 @@ func Test_Session_LogWithContext(t *testing.T) { resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody)) require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) + // Session ID must appear in log but be redacted (only first 4 chars + ****) require.Contains(t, logOutput.String(), "session-id=") require.Contains(t, logOutput.String(), "session test") + require.NotContains(t, logOutput.String(), capturedID, "full session ID must not appear in log") +} + +func Test_redactSessionID(t *testing.T) { + t.Parallel() + + t.Run("long ID is redacted", func(t *testing.T) { + t.Parallel() + require.Equal(t, "abcd****", redactSessionID("abcdefghij")) + }) + + t.Run("short ID is fully redacted", func(t *testing.T) { + t.Parallel() + require.Equal(t, "****", redactSessionID("short")) + }) + + t.Run("exactly 8 chars is fully redacted", func(t *testing.T) { + t.Parallel() + require.Equal(t, "****", redactSessionID("12345678")) + }) + + t.Run("empty string is fully redacted", func(t *testing.T) { + t.Parallel() + require.Equal(t, "****", redactSessionID("")) + }) } From e5f0b4366b8cac504cdf6b64b6642d14bf2e9679 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:45:30 +0000 Subject: [PATCH 12/21] docs: fix redactSessionID comment to accurately describe >8 vs <=8 behavior Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- middleware/session/middleware.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/middleware/session/middleware.go b/middleware/session/middleware.go index cdff830532d..94f07ce1ff5 100644 --- a/middleware/session/middleware.go +++ b/middleware/session/middleware.go @@ -209,8 +209,10 @@ func FromContext(ctx any) *Middleware { } // redactSessionID returns a masked version of a session ID for safe logging. -// Session IDs are bearer secrets; only the first 4 characters are retained so -// that log entries can still be correlated without exposing the full token. +// Session IDs are bearer secrets; for IDs longer than 8 characters, only the +// first 4 characters are retained and the remainder is masked so that log +// entries can still be correlated without exposing the full token. Shorter +// IDs are fully redacted. func redactSessionID(id string) string { if len(id) > 8 { return id[:4] + "****" From 57fd7aaa97f3a8cdf431d2896b00c74faa740636 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:05:43 +0000 Subject: [PATCH 13/21] fix: CSRF extractor respects DisableValueRedaction; clarify WithContext context types in docs - Move redactKeys computation before registerExtractor.Do() so the closure captures the correct redaction setting from the first New() call. When DisableValueRedaction=true, the real token is emitted; otherwise the constant [redacted] placeholder is used. - Update docs/api/log.md to remove the incorrect claim that *fasthttp.RequestCtx works directly; clarify that c.Context() (the underlying *fasthttp.RequestCtx) only finds middleware values when PassLocalsToContext is enabled Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- docs/api/log.md | 2 +- middleware/csrf/csrf.go | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/api/log.md b/docs/api/log.md index 96097326870..65eb73b35cc 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -198,7 +198,7 @@ commonLogger := log.WithContext(ctx) commonLogger.Info("info") ``` -Context binding adds request-specific data for easier tracing. The method accepts any value implementing `context.Context`, including `fiber.Ctx`, `*fasthttp.RequestCtx`, and standard `context.Context`. +Context binding adds request-specific data for easier tracing. The method accepts any value implementing `context.Context`, including `fiber.Ctx` and standard `context.Context`. To use a `*fasthttp.RequestCtx` (obtained via `c.Context()`), enable `PassLocalsToContext` in the app config so that context values are propagated through the standard context chain. ### Automatic Context Fields diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index 4e3c0e0d1df..f9b9f657fb7 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -58,21 +58,25 @@ func New(config ...Config) fiber.Handler { // Set default config cfg := configDefault(config...) + redactKeys := !cfg.DisableValueRedaction + // Register a log context extractor so that log.WithContext(c) automatically - // includes a redacted CSRF token when the csrf middleware is in use. - // An empty token (no middleware or middleware skipped) is omitted. + // includes the CSRF token when the csrf middleware is in use. + // Redaction is controlled by the DisableValueRedaction option from the first + // call to New(). An empty token (no middleware or middleware skipped) is omitted. registerExtractor.Do(func() { log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { token := TokenFromContext(ctx) if token == "" { return "", nil, false } - return "csrf-token", redactedKey, true + if redactKeys { + return "csrf-token", redactedKey, true + } + return "csrf-token", token, true }) }) - redactKeys := !cfg.DisableValueRedaction - maskValue := func(value string) string { if redactKeys { return redactedKey From be7f6ffd2cbb0562285d57e8b972cbf1924f2a60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 01:21:26 +0000 Subject: [PATCH 14/21] docs: clarify WithContext accepted context types in log.md Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- docs/api/log.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/log.md b/docs/api/log.md index 65eb73b35cc..5acb31ea0ec 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -198,7 +198,7 @@ commonLogger := log.WithContext(ctx) commonLogger.Info("info") ``` -Context binding adds request-specific data for easier tracing. The method accepts any value implementing `context.Context`, including `fiber.Ctx` and standard `context.Context`. To use a `*fasthttp.RequestCtx` (obtained via `c.Context()`), enable `PassLocalsToContext` in the app config so that context values are propagated through the standard context chain. +Context binding adds request-specific data for easier tracing. The method accepts any value implementing `context.Context`, including `fiber.Ctx` (which itself implements `context.Context`) and standard `context.Context` instances such as `c.Context()`. When using `c.Context()`, enable `PassLocalsToContext` in the app config so that values stored in `fiber.Ctx.Locals` are propagated through the standard context chain. ### Automatic Context Fields From 13b97d752e62dfc579758108ec22898fb75ce43d Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:51:45 -0400 Subject: [PATCH 15/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- middleware/session/middleware_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/middleware/session/middleware_test.go b/middleware/session/middleware_test.go index eed043f0468..db3c7c31362 100644 --- a/middleware/session/middleware_test.go +++ b/middleware/session/middleware_test.go @@ -616,9 +616,11 @@ func Test_Session_LogWithContext(t *testing.T) { require.NoError(t, err) require.Equal(t, fiber.StatusOK, resp.StatusCode) // Session ID must appear in log but be redacted (only first 4 chars + ****) - require.Contains(t, logOutput.String(), "session-id=") - require.Contains(t, logOutput.String(), "session test") - require.NotContains(t, logOutput.String(), capturedID, "full session ID must not appear in log") + logStr := logOutput.String() + expectedRedacted := redactSessionID(capturedID) + require.Contains(t, logStr, "session-id="+expectedRedacted, "redacted session ID must appear in log") + require.Contains(t, logStr, "session test") + require.NotContains(t, logStr, capturedID, "full session ID must not appear in log") } func Test_redactSessionID(t *testing.T) { From c419d2a954ecd3258e34cec9ff4535d0434d76f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:17:29 +0000 Subject: [PATCH 16/21] fix: always redact CSRF tokens in log extractor, remove sync.Once config capture footgun The previous implementation captured DisableValueRedaction from the first New() call via sync.Once closure, which would silently apply that redaction setting to all subsequent csrf middleware instances in the same process. If the first call used DisableValueRedaction=true, full CSRF tokens would leak into logs for every instance created afterward. CSRF tokens are bearer secrets and must always be redacted in log output regardless of DisableValueRedaction. That option controls other masking behavior (error responses, debug output) but not log output. Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- middleware/csrf/csrf.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index f9b9f657fb7..fdd9567fd39 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -61,19 +61,17 @@ func New(config ...Config) fiber.Handler { redactKeys := !cfg.DisableValueRedaction // Register a log context extractor so that log.WithContext(c) automatically - // includes the CSRF token when the csrf middleware is in use. - // Redaction is controlled by the DisableValueRedaction option from the first - // call to New(). An empty token (no middleware or middleware skipped) is omitted. + // includes a redacted CSRF token when the csrf middleware is in use. + // CSRF tokens are always redacted in log output regardless of DisableValueRedaction, + // because they are bearer secrets and must never appear in plain text in logs. + // An empty token (no middleware or middleware skipped) is omitted. registerExtractor.Do(func() { log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { token := TokenFromContext(ctx) if token == "" { return "", nil, false } - if redactKeys { - return "csrf-token", redactedKey, true - } - return "csrf-token", token, true + return "csrf-token", redactedKey, true }) }) From 34d3cb877dd1d8e81f8d2962b69b3c83aa2a0928 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:24:12 +0000 Subject: [PATCH 17/21] test: add missing Test_BasicAuth_LogWithContext for complete coverage Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- middleware/basicauth/basicauth_test.go | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go index dc5a0ab8173..0fe6f814e7e 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/middleware/basicauth/basicauth_test.go @@ -1,6 +1,7 @@ package basicauth import ( + "bytes" "crypto/sha256" "crypto/sha512" "encoding/base64" @@ -9,10 +10,12 @@ import ( "io" "net/http" "net/http/httptest" + "os" "strings" "testing" "github.com/gofiber/fiber/v3" + fiberlog "github.com/gofiber/fiber/v3/log" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" "golang.org/x/crypto/bcrypt" @@ -630,3 +633,34 @@ func Test_BasicAuth_HashVariants_Invalid(t *testing.T) { require.Equal(t, fiber.StatusUnauthorized, resp.StatusCode) } } + +func Test_BasicAuth_LogWithContext(t *testing.T) { + t.Parallel() + + hashedJohn := sha256Hash("doe") + app := fiber.New() + app.Use(New(Config{ + Users: map[string]string{ + "john": hashedJohn, + }, + })) + + var logOutput bytes.Buffer + fiberlog.SetOutput(&logOutput) + defer fiberlog.SetOutput(os.Stderr) + + app.Get("/", func(c fiber.Ctx) error { + fiberlog.WithContext(c).Info("basicauth test") + return c.SendStatus(fiber.StatusOK) + }) + + creds := base64.StdEncoding.EncodeToString([]byte("john:doe")) + req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody) + req.Header.Set(fiber.HeaderAuthorization, "Basic "+creds) + + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + require.Contains(t, logOutput.String(), "username=john") + require.Contains(t, logOutput.String(), "basicauth test") +} From 7695b26463ad4727e857a5ffc963fd1b7e1c6d0c Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Fri, 13 Mar 2026 19:38:04 -0400 Subject: [PATCH 18/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- middleware/basicauth/basicauth_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go index 0fe6f814e7e..aa785ff9d1a 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/middleware/basicauth/basicauth_test.go @@ -635,7 +635,6 @@ func Test_BasicAuth_HashVariants_Invalid(t *testing.T) { } func Test_BasicAuth_LogWithContext(t *testing.T) { - t.Parallel() hashedJohn := sha256Hash("doe") app := fiber.New() From 3f9816540d6ed65d5dc345a1c3fcf043810fd619 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Mar 2026 23:44:21 +0000 Subject: [PATCH 19/21] fix: remove blank line after function signature in basicauth_test.go (gofumpt) Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- middleware/basicauth/basicauth_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go index aa785ff9d1a..a41819b27f1 100644 --- a/middleware/basicauth/basicauth_test.go +++ b/middleware/basicauth/basicauth_test.go @@ -635,7 +635,6 @@ func Test_BasicAuth_HashVariants_Invalid(t *testing.T) { } func Test_BasicAuth_LogWithContext(t *testing.T) { - hashedJohn := sha256Hash("doe") app := fiber.New() app.Use(New(Config{ From cdcd49b97021ce589ef4fc403b1f00e69a9aaf78 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Mar 2026 04:38:47 +0000 Subject: [PATCH 20/21] feat: make log.WithContext accept any instead of context.Context for consistency with fiber.ValueFromContext Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- docs/api/log.md | 7 ++++--- docs/whats_new.md | 4 +++- log/default.go | 6 +++--- log/default_test.go | 10 ++++++---- log/log.go | 9 +++++---- middleware/basicauth/basicauth.go | 3 +-- middleware/csrf/csrf.go | 3 +-- middleware/keyauth/keyauth.go | 3 +-- middleware/requestid/requestid.go | 3 +-- middleware/session/middleware.go | 3 +-- 10 files changed, 26 insertions(+), 25 deletions(-) diff --git a/docs/api/log.md b/docs/api/log.md index 5acb31ea0ec..cebd47e8c0b 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -198,7 +198,7 @@ commonLogger := log.WithContext(ctx) commonLogger.Info("info") ``` -Context binding adds request-specific data for easier tracing. The method accepts any value implementing `context.Context`, including `fiber.Ctx` (which itself implements `context.Context`) and standard `context.Context` instances such as `c.Context()`. When using `c.Context()`, enable `PassLocalsToContext` in the app config so that values stored in `fiber.Ctx.Locals` are propagated through the standard context chain. +Context binding adds request-specific data for easier tracing. The method accepts `fiber.Ctx`, `*fasthttp.RequestCtx`, or `context.Context`. When using standard `context.Context` instances (such as `c.Context()`), enable `PassLocalsToContext` in the app config so that values stored in `fiber.Ctx.Locals` are propagated through the context chain. ### Automatic Context Fields @@ -227,8 +227,9 @@ app.Get("/", func(c fiber.Ctx) error { Use `log.RegisterContextExtractor` to register your own extractors. Each extractor receives the bound context and returns a field name, value, and success flag: ```go -log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { - if traceID, ok := ctx.Value(traceIDKey).(string); ok && traceID != "" { +log.RegisterContextExtractor(func(ctx any) (string, any, bool) { + // Use fiber.ValueFromContext to extract from any supported context type + if traceID, ok := fiber.ValueFromContext[string](ctx, traceIDKey); ok && traceID != "" { return "trace-id", traceID, true } return "", nil, false diff --git a/docs/whats_new.md b/docs/whats_new.md index 7e2ea996b7e..a22d67a3a9a 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -1217,7 +1217,9 @@ app.Use(logger.New(logger.Config{ ### Context-Aware Logging -`log.WithContext` now automatically includes context fields extracted by middleware. Middleware such as `requestid`, `basicauth`, `keyauth`, `csrf`, and `session` register extractors when their `New()` constructor is called. When you pass a `fiber.Ctx` (or any `context.Context`) to `log.WithContext`, registered fields are prepended to every log entry. +`log.WithContext` now automatically includes context fields extracted by middleware. The method accepts `fiber.Ctx`, `*fasthttp.RequestCtx`, or `context.Context`, making it flexible and consistent with Fiber's context handling throughout the framework. + +Middleware such as `requestid`, `basicauth`, `keyauth`, `csrf`, and `session` register extractors when their `New()` constructor is called. When you pass a context to `log.WithContext`, registered fields are prepended to every log entry. ```go app.Use(requestid.New()) diff --git a/log/default.go b/log/default.go index 25fa02a3ccf..d7723d3c32c 100644 --- a/log/default.go +++ b/log/default.go @@ -1,7 +1,6 @@ package log import ( - "context" "fmt" "io" "log" @@ -14,7 +13,7 @@ import ( var _ AllLogger[*log.Logger] = (*defaultLogger)(nil) type defaultLogger struct { - ctx context.Context //nolint:containedctx // stored for deferred field extraction + ctx any stdlog *log.Logger level Level depth int @@ -243,7 +242,8 @@ func (l *defaultLogger) Panicw(msg string, keysAndValues ...any) { // WithContext returns a logger that shares the underlying output but carries // the provided context. Any registered ContextExtractor functions will be // called at log time to prepend key-value fields extracted from the context. -func (l *defaultLogger) WithContext(ctx context.Context) CommonLogger { +// The ctx parameter can be fiber.Ctx, *fasthttp.RequestCtx, or context.Context. +func (l *defaultLogger) WithContext(ctx any) CommonLogger { return &defaultLogger{ stdlog: l.stdlog, level: l.level, diff --git a/log/default_test.go b/log/default_test.go index d4d4f9900ab..dbfec7cb714 100644 --- a/log/default_test.go +++ b/log/default_test.go @@ -133,9 +133,11 @@ func Test_WithContextExtractor(t *testing.T) { contextExtractorsMu.Unlock() }() - RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { - if v, ok := ctx.Value(testContextKey{}).(string); ok && v != "" { - return "request-id", v, true + RegisterContextExtractor(func(ctx any) (string, any, bool) { + if ctxTyped, ok := ctx.(context.Context); ok { + if v, ok := ctxTyped.Value(testContextKey{}).(string); ok && v != "" { + return "request-id", v, true + } } return "", nil, false }) @@ -217,7 +219,7 @@ func Test_WithContextExtractor(t *testing.T) { }() // Add an extractor that returns ok=true but key="" - RegisterContextExtractor(func(_ context.Context) (string, any, bool) { + RegisterContextExtractor(func(_ any) (string, any, bool) { return "", "should-not-appear", true }) diff --git a/log/log.go b/log/log.go index cb667da8525..7dd9a7be46d 100644 --- a/log/log.go +++ b/log/log.go @@ -1,7 +1,6 @@ package log import ( - "context" "fmt" "io" "log" @@ -12,7 +11,8 @@ import ( // ContextExtractor extracts a key-value pair from the given context for // inclusion in log output when using WithContext. // It returns the log field name, its value, and whether extraction succeeded. -type ContextExtractor func(ctx context.Context) (string, any, bool) +// The ctx parameter can be fiber.Ctx, *fasthttp.RequestCtx, or context.Context. +type ContextExtractor func(ctx any) (string, any, bool) // contextExtractorsMu guards contextExtractors for concurrent registration // and snapshot reads. @@ -59,7 +59,7 @@ type baseLogger interface { CommonLogger SetLevel(Level) SetOutput(io.Writer) - WithContext(ctx context.Context) CommonLogger + WithContext(ctx any) CommonLogger } var logger baseLogger = &defaultLogger{ @@ -129,7 +129,8 @@ type AllLogger[T any] interface { ConfigurableLogger[T] // WithContext returns a new logger with the given context. - WithContext(ctx context.Context) CommonLogger + // The ctx parameter can be fiber.Ctx, *fasthttp.RequestCtx, or context.Context. + WithContext(ctx any) CommonLogger } // Level defines the priority of a log message. diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go index 36d2f17eef9..e3748f66791 100644 --- a/middleware/basicauth/basicauth.go +++ b/middleware/basicauth/basicauth.go @@ -1,7 +1,6 @@ package basicauth import ( - "context" "encoding/base64" "errors" "strings" @@ -38,7 +37,7 @@ func New(config ...Config) fiber.Handler { // Register a log context extractor so that log.WithContext(c) automatically // includes the authenticated username when basicauth middleware is in use. registerExtractor.Do(func() { - log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + log.RegisterContextExtractor(func(ctx any) (string, any, bool) { username := UsernameFromContext(ctx) return "username", username, username != "" }) diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go index fdd9567fd39..55e527cc054 100644 --- a/middleware/csrf/csrf.go +++ b/middleware/csrf/csrf.go @@ -1,7 +1,6 @@ package csrf import ( - "context" "errors" "fmt" "net/url" @@ -66,7 +65,7 @@ func New(config ...Config) fiber.Handler { // because they are bearer secrets and must never appear in plain text in logs. // An empty token (no middleware or middleware skipped) is omitted. registerExtractor.Do(func() { - log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + log.RegisterContextExtractor(func(ctx any) (string, any, bool) { token := TokenFromContext(ctx) if token == "" { return "", nil, false diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index bf8ef0c5bd8..4eadd50e756 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -1,7 +1,6 @@ package keyauth import ( - "context" "errors" "fmt" "strings" @@ -38,7 +37,7 @@ func New(config ...Config) fiber.Handler { // includes a redacted API key when the keyauth middleware is in use. // An empty token (no middleware or middleware skipped) is omitted. registerExtractor.Do(func() { - log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + log.RegisterContextExtractor(func(ctx any) (string, any, bool) { token := TokenFromContext(ctx) if token == "" { return "", nil, false diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index 77c630f990e..d73e98ad9c5 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -1,7 +1,6 @@ package requestid import ( - "context" "sync" "github.com/gofiber/fiber/v3" @@ -31,7 +30,7 @@ func New(config ...Config) fiber.Handler { // includes the request ID when the requestid middleware is in use. // An empty request ID (no middleware or middleware skipped) is omitted. registerExtractor.Do(func() { - log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + log.RegisterContextExtractor(func(ctx any) (string, any, bool) { rid := FromContext(ctx) return "request-id", rid, rid != "" }) diff --git a/middleware/session/middleware.go b/middleware/session/middleware.go index 94f07ce1ff5..1fb7dca875a 100644 --- a/middleware/session/middleware.go +++ b/middleware/session/middleware.go @@ -3,7 +3,6 @@ package session import ( - "context" "errors" "sync" @@ -92,7 +91,7 @@ func NewWithStore(config ...Config) (fiber.Handler, *Store) { // Session IDs are bearer secrets, so only the first 4 characters are logged // to enable correlation without exposing the full token. registerExtractor.Do(func() { - log.RegisterContextExtractor(func(ctx context.Context) (string, any, bool) { + log.RegisterContextExtractor(func(ctx any) (string, any, bool) { m := FromContext(ctx) if m == nil || m.Session == nil { return "", nil, false From b3fc68a23ad87b01fd3d433b6d3b84ce9c749bff Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:53:39 +0000 Subject: [PATCH 21/21] docs: add output examples and format compatibility notes to log.WithContext - Added concrete output examples showing how context fields appear in logs - Added example with multiple middleware to show how fields are combined - Added compatibility note explaining key=value format and JSON/structured logging support - Clarified that context fields are always included when using WithContext - Fixed markdown linting issues (language specification and blank lines) Co-authored-by: gaby <835733+gaby@users.noreply.github.com> --- docs/api/log.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/api/log.md b/docs/api/log.md index cebd47e8c0b..c015f833455 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -222,6 +222,43 @@ app.Get("/", func(c fiber.Ctx) error { }) ``` +**Example output:** + +```text +2026/03/17 12:00:00.123456 main.go:15: [Info] request-id=abc-123 processing request +``` + +The context fields (`request-id=abc-123`) are automatically prepended to the log message. You can use multiple middlewares and all their fields will be included: + +```go +app.Use(requestid.New()) +app.Use(basicauth.New(basicauth.Config{ + Users: map[string]string{"admin": "password"}, +})) + +app.Get("/", func(c fiber.Ctx) error { + log.WithContext(c).Info("user action") + return c.SendString("OK") +}) +``` + +**Example output:** + +```text +2026/03/17 12:00:00.123456 main.go:20: [Info] request-id=abc-123 username=admin user action +``` + +:::note +**Context fields and logger compatibility:** + +- Context fields are prepended to the log message in `key=value` format +- The fields are extracted once per log call and added before the message +- Works with any logger that implements the `AllLogger` interface +- For JSON or structured logging, use the `Logw` methods (e.g., `log.WithContext(c).Infow("message", "key", "value")`) which preserve field structure +- Context fields are always included when using `log.WithContext()`, regardless of how many times you call the logger in a handler + +::: + ### Custom Context Extractors Use `log.RegisterContextExtractor` to register your own extractors. Each extractor receives the bound context and returns a field name, value, and success flag: