Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
71579fd
Initial plan
Copilot Feb 23, 2026
73ddbe9
feat: add context field extraction to log.WithContext via RegisterCon…
Copilot Feb 23, 2026
e862b94
fix: apply betteralign struct field ordering for defaultLogger
Copilot Feb 23, 2026
2c4216c
docs: improve documentation for context extractors and requestid init
Copilot Feb 23, 2026
4549449
refactor: replace init() with sync.Once in New(), add basicauth extra…
Copilot Feb 23, 2026
ae206fb
feat: add log context extractors for csrf, keyauth, and session middl…
Copilot Feb 23, 2026
a0d4054
Update log/log.go
gaby Feb 23, 2026
041145b
Update log/log.go
gaby Feb 23, 2026
40bd5d0
security: redact sensitive values (api-key, csrf-token) in log contex…
Copilot Feb 23, 2026
add61a5
fix: skip context extractors that return empty key to prevent malform…
Copilot Feb 24, 2026
8234a50
Merge branch 'main' into copilot/fix-request-id-logging
gaby Mar 12, 2026
1b78967
fix: make RegisterContextExtractor concurrent-safe with RWMutex, reda…
Copilot Mar 12, 2026
e5f0b43
docs: fix redactSessionID comment to accurately describe >8 vs <=8 be…
Copilot Mar 12, 2026
57fd7aa
fix: CSRF extractor respects DisableValueRedaction; clarify WithConte…
Copilot Mar 12, 2026
be7f6ff
docs: clarify WithContext accepted context types in log.md
Copilot Mar 13, 2026
13b97d7
Potential fix for pull request finding
gaby Mar 13, 2026
c419d2a
fix: always redact CSRF tokens in log extractor, remove sync.Once con…
Copilot Mar 13, 2026
34d3cb8
test: add missing Test_BasicAuth_LogWithContext for complete coverage
Claude Mar 13, 2026
060dd32
Merge branch 'main' into copilot/fix-request-id-logging
gaby Mar 13, 2026
7695b26
Potential fix for pull request finding
gaby Mar 13, 2026
3f98165
fix: remove blank line after function signature in basicauth_test.go …
Copilot Mar 13, 2026
cdcd49b
feat: make log.WithContext accept any instead of context.Context for …
Claude Mar 14, 2026
3da399c
Merge branch 'main' into copilot/fix-request-id-logging
gaby Mar 17, 2026
b3fc68a
docs: add output examples and format compatibility notes to log.WithC…
Claude Mar 17, 2026
e359e50
Merge branch 'main' into copilot/fix-request-id-logging
gaby Mar 29, 2026
5c3dddd
fix: address PR review comments - API signature, log injection, and docs
Claude Mar 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion docs/api/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<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

Expand Down
16 changes: 16 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 26 additions & 4 deletions log/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,29 @@ 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
}

// 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.
// when the level is fatal, it will exit the program.
func (l *defaultLogger) privateLog(lv Level, fmtArgs []any) {
Expand All @@ -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
Expand All @@ -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...)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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,
}
}

Expand Down
80 changes: 80 additions & 0 deletions log/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ 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.
//
// 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.
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.
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 {
Expand Down
16 changes: 16 additions & 0 deletions middleware/basicauth/basicauth.go
Original file line number Diff line number Diff line change
@@ -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"
)
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions middleware/requestid/requestid.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package requestid

import (
"context"
"sync"

"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/log"
"github.com/gofiber/utils/v2"
)

Expand All @@ -14,11 +18,25 @@ const (
requestIDKey contextKey = iota
)

// 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
Expand Down
Loading
Loading