Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 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
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
42 changes: 41 additions & 1 deletion docs/api/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,47 @@ 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 `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

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are values that are written internally, I have two questions:

  1. Are these values always written, even if I use the logger multiple times in a handler? If so, this feature should be made configurable in some way.
  2. Questions about formatting will likely arise, so this should be considered right from the start of the feature
    to ensure that it is possible to format this output.
    Since everything is output in JSON format in the microservices world, it would be good if these values could also directly conform to this schema.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. You mean to make the registration optional for each middleware?

Right now it registers in the middleware, but that doesn't do anything unless the user is using fiberlog.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added compatibility note in b3fc68a addressing both points:

  1. Always written: Yes, context fields are automatically included with every log.WithContext(c) call in a handler. This is intentional for request correlation. If you need to log without context fields, use the regular log.Info() methods instead of log.WithContext(c).Info().

  2. Formatting: The default format is key=value pairs prepended to messages. For JSON/structured logging, the note now recommends using the Logw methods (e.g., log.WithContext(c).Infow("message", "key", "value")) which preserve field structure and work better with JSON formatters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I use three middleware components from the core, for example, requestId, csrf, and session

and then call the log method multiple times in my business logic handler, as a consumer I might not want to see the IDs of these three core middleware components in the output every time


| 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 (redacted) |

```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 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
})
```

:::note
`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

Expand Down
18 changes: 18 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,24 @@ app.Use(logger.New(logger.Config{
}))
```

### Context-Aware Logging

`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())

app.Get("/", func(c fiber.Ctx) error {
// Output: [Info] request-id=abc-123 processing request
log.WithContext(c).Info("processing request")
Comment on lines +1220 to +1229
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
33 changes: 28 additions & 5 deletions log/default.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package log

import (
"context"
"fmt"
"io"
"log"
Expand All @@ -14,11 +13,30 @@ import (
var _ AllLogger[*log.Logger] = (*defaultLogger)(nil)

type defaultLogger struct {
ctx any
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 {
return
}
extractors := loadContextExtractors()
for _, extractor := range extractors {
key, value, ok := extractor(l.ctx)
if ok && key != "" {
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,16 @@ 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.
// 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,
depth: l.depth - 1,
ctx: ctx,
}
}

Expand Down
118 changes: 118 additions & 0 deletions log/default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,124 @@ 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 using the mutex for correctness.
contextExtractorsMu.Lock()
saved := contextExtractors
contextExtractors = nil
contextExtractorsMu.Unlock()

defer func() {
contextExtractorsMu.Lock()
contextExtractors = saved
contextExtractorsMu.Unlock()
}()

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
})

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())
})

t.Run("empty key extractor is skipped", func(t *testing.T) {
// Save and restore extractors for this subtest using the mutex.
contextExtractorsMu.Lock()
savedInner := contextExtractors
contextExtractorsMu.Unlock()

defer func() {
contextExtractorsMu.Lock()
contextExtractors = savedInner
contextExtractorsMu.Unlock()
}()

// Add an extractor that returns ok=true but key=""
RegisterContextExtractor(func(_ any) (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) {
tests := []struct {
name string
Expand Down
52 changes: 49 additions & 3 deletions log/log.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,65 @@
package log

import (
"context"
"fmt"
"io"
"log"
"os"
"sync"
)

// 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.
// 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.
var contextExtractorsMu sync.RWMutex

// contextExtractors holds all registered context field extractors.
// 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 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.
// It allows storing any logger implementation regardless of its generic type.
type baseLogger interface {
CommonLogger
SetLevel(Level)
SetOutput(io.Writer)
WithContext(ctx context.Context) CommonLogger
WithContext(ctx any) CommonLogger
}

var logger baseLogger = &defaultLogger{
Expand Down Expand Up @@ -84,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.
Expand Down
15 changes: 15 additions & 0 deletions middleware/basicauth/basicauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"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 +25,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 any) (string, any, bool) {
username := UsernameFromContext(ctx)
return "username", username, username != ""
})
})

var cerr base64.CorruptInputError

// Return new handler
Expand Down
Loading
Loading