Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bba8c4c
feat: reuse logger template rendering for contextual logs
ReneWerner87 Apr 27, 2026
03fc04e
refactor: update logger context handling to accept any type
ReneWerner87 Apr 27, 2026
8323c21
refactor: enhance context template error handling and improve logger …
ReneWerner87 Apr 27, 2026
0e3b43c
refactor: update logger context handling to use retainedContext for i…
ReneWerner87 Apr 29, 2026
43f712f
test: document serial context logger tests
ReneWerner87 Apr 29, 2026
c3aa623
test: cover nil context logger caller
ReneWerner87 Apr 29, 2026
1b5e637
test: tighten context logger coverage
ReneWerner87 Apr 30, 2026
2c3cffa
docs: keep logger middleware docs focused
ReneWerner87 Apr 30, 2026
64fee00
feat: add configurable log context formats
ReneWerner87 Apr 30, 2026
94ac462
feat: auto register logger middleware tags
ReneWerner87 Apr 30, 2026
696dcbc
Merge branch 'main' into codex/reusable-log-template
ReneWerner87 Apr 30, 2026
d509f58
fix: address logger template review feedback
ReneWerner87 Apr 30, 2026
765daa1
fix: harden context tag configuration
ReneWerner87 May 2, 2026
8ec293b
docs: fix logger custom tags default
ReneWerner87 May 2, 2026
2452141
test: expand log template coverage
ReneWerner87 May 2, 2026
c71abdb
refactor: harden reusable log template per multi-perspective review
ReneWerner87 May 2, 2026
ec89b48
style: satisfy modernize + golangci-lint v2 on review-driven changes
ReneWerner87 May 2, 2026
7cb3c86
fix(test): make ConcurrentRegistration order-independent + drop ${non}
ReneWerner87 May 2, 2026
132fcd9
fix(test): initialize default logger in Test_WithContextRenderError
ReneWerner87 May 2, 2026
bca1a01
fix: address PR review feedback (Copilot + CodeRabbit)
ReneWerner87 May 2, 2026
5321528
fix: preserve logger buffer compatibility
ReneWerner87 May 3, 2026
15ff5e5
fix: address logger template review comments
ReneWerner87 May 3, 2026
c353216
fix: address logger template review feedback
ReneWerner87 May 4, 2026
4239c1d
Merge branch 'main' into codex/reusable-log-template
ReneWerner87 May 4, 2026
436e357
docs(logger): align JSONFormat row with the actual constant
ReneWerner87 May 4, 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
61 changes: 59 additions & 2 deletions docs/api/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ type ConfigurableLogger[T any] interface {
type AllLogger[T any] interface {
CommonLogger
ConfigurableLogger[T]
WithLogger

// WithContext returns a new logger with the given context.
WithContext(ctx any) CommonLogger
}
```

Expand Down Expand Up @@ -198,7 +200,62 @@ commonLogger := log.WithContext(ctx)
commonLogger.Info("info")
```

Context binding adds request-specific data for easier tracing.
Context binding can render request-specific data for easier tracing. The default context format is `log.DefaultFormat`, which is empty, so `log.WithContext(ctx)` does not add fields until you configure a format.

Use `log.Format` to configure Fiber's built-in default logger. Custom loggers registered with `SetLogger` keep full control over their own `WithContext` behavior and should implement equivalent enrichment themselves when needed.

```go
app.Use(requestid.New())

if err := log.Format(log.RequestIDFormat); err != nil {
log.Fatal(err)
}

app.Get("/", func(c fiber.Ctx) error {
log.WithContext(c).Info("start")
return c.SendString("Hello, World!")
})
```

Middleware that stores request values can register log context tags automatically when the middleware is initialized. For example, after `requestid.New()` has been used, `${requestid}` and `${request-id}` can be used directly in `log.Format` without defining custom tags.

Use `log.WithContext(c)` inside handlers when you want tags to read values stored by Fiber middleware. Passing `c.Context()` only exposes values propagated into the standard request context.

### Context Formats

| Format Constant | Format String | Description |
| :-- | :-- | :-- |
| `DefaultFormat` | `""` | Disables contextual fields. |
| `RequestIDFormat` | `"[${requestid}] "` | Prepends the request ID when the requestid middleware is used. |
| `KeyValueFormat` | `"request-id=${requestid} username=${username} api-key=${api-key} csrf-token=${csrf-token} session-id=${session-id} "` | Prepends common middleware context values as key/value fields. Sensitive values are redacted by the registering middleware. |

### Context Tags

| Tag | Source |
| :-- | :-- |
| `${requestid}` / `${request-id}` | `requestid` middleware |
| `${username}` | `basicauth` middleware |
| `${api-key}` | `keyauth` middleware, redacted |
| `${csrf-token}` | `csrf` middleware, redacted |
| `${session-id}` | `session` middleware, redacted |
| `${value:key}` | Any bound value with `Value(key)` or `UserValue(key)` lookup methods |

### Custom Context Tags

Register custom tags with `log.RegisterContextTag`, then reference them from `log.Format`.

```go
log.MustRegisterContextTag("tenant", func(output log.Buffer, ctx any, _ *log.ContextData, _ string) (int, error) {
c, ok := ctx.(fiber.Ctx)
if !ok {
return 0, nil
}
tenant, _ := c.Locals("tenant").(string)
Comment thread
ReneWerner87 marked this conversation as resolved.
Outdated
return output.WriteString(tenant)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})

log.MustFormat("[${tenant}] ")
```

## Logger

Expand Down
97 changes: 89 additions & 8 deletions docs/middleware/logger.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Logger middleware for [Fiber](https://github.com/gofiber/fiber) that logs HTTP r

```go
func New(config ...Config) fiber.Handler
func RegisterTag(tag string, fn LogFunc) error
func MustRegisterTag(tag string, fn LogFunc)
```

## Examples
Expand All @@ -20,6 +22,7 @@ Import the package:
import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/logger"
"github.com/gofiber/fiber/v3/middleware/requestid"
)
```

Expand All @@ -42,13 +45,7 @@ app.Use(logger.New(logger.Config{
// Logging Request ID
app.Use(requestid.New()) // Ensure requestid middleware is used before the logger
app.Use(logger.New(logger.Config{
CustomTags: map[string]logger.LogFunc{
"requestid": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) {
return output.WriteString(requestid.FromContext(c))
},
},
// For more options, see the Config section
// Use the custom tag ${requestid} as defined above.
// requestid.New() registers ${requestid} automatically.
Format: "${pid} ${requestid} ${status} - ${method} ${path}\n",
}))

Expand Down Expand Up @@ -117,6 +114,90 @@ app.Use(logger.New(logger.Config{
}))
```

### Auto-Registered Tags

Some Fiber middleware registers logger middleware tags automatically. Register the producing middleware before `logger.New()` and then use the tag in `Format`.

```go
app.Use(requestid.New())
app.Use(logger.New(logger.Config{
Format: "${requestid} ${status} ${method} ${path}\n",
}))
```

The logger middleware resolves tags in this order:

1. Built-in logger tags, such as `${method}`, `${path}`, and `${status}`.
2. Globally registered tags from Fiber middleware or `logger.RegisterTag`.
3. `Config.CustomTags`, which override tags with the same name for that logger instance.

The following tags are registered by Fiber middleware when the middleware is initialized:

| Tag | Registered by | Value |
| :-- | :------------ | :---- |
| `${requestid}` / `${request-id}` | `requestid.New()` | Request ID stored by the requestid middleware. |
| `${username}` | `basicauth.New()` | Authenticated username stored by the basicauth middleware. |
| `${api-key}` | `keyauth.New()` | Redacted API key stored by the keyauth middleware. |
| `${csrf-token}` | `csrf.New()` | Redacted marker when the csrf middleware stores a token. |
| `${session-id}` | `session.New()` or `session.NewWithStore()` | Redacted session ID stored by the session middleware. |

:::note
Auto-registered tags are access-log tags for `middleware/logger`. Application logs from the `log` package use their own context tag registry.
:::
Comment thread
ReneWerner87 marked this conversation as resolved.

### Register Tags from Custom Middleware

Third-party middleware can expose logger tags with `logger.RegisterTag` or `logger.MustRegisterTag`. Use `sync.Once` so the tag is registered once even when the middleware is initialized multiple times.

```go
package tenantmw

import (
"sync"

"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/logger"
)

var registerLoggerTagsOnce sync.Once

func New() fiber.Handler {
registerLoggerTagsOnce.Do(func() {
logger.MustRegisterTag("tenant", func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
tenant, _ := c.Locals("tenant").(string)
return output.WriteString(tenant)
})
})

return func(c fiber.Ctx) error {
c.Locals("tenant", "acme")
Comment thread
ReneWerner87 marked this conversation as resolved.
Outdated
return c.Next()
}
}
```

Use the registered tag in the logger format after installing the middleware:

```go
app.Use(tenantmw.New())
app.Use(logger.New(logger.Config{
Format: "${tenant} ${status} ${method} ${path}\n",
}))
```

Use `Config.CustomTags` when one logger instance needs a local override without changing the global tag registration:

```go
app.Use(logger.New(logger.Config{
Format: "${tenant} ${status} ${method} ${path}\n",
CustomTags: map[string]logger.LogFunc{
"tenant": func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
return output.WriteString("override")
},
},
}))
```

### Use Logger Middleware with Other Loggers

To combine the logger middleware with loggers like Zerolog, Zap, or Logrus, use the `LoggerToWriter` helper to adapt them to an `io.Writer`.
Expand Down Expand Up @@ -166,7 +247,7 @@ Writing to `os.File` is goroutine-safe, but custom streams may require locking t
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when it returns true. | `nil` |
| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` |
| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Stream, and pass the log string as parameter. | `nil` |
| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` |
| CustomTags | `map[string]LogFunc` | Defines custom tag actions for this logger instance. These tags override built-in and globally registered tags with the same name. | `map[string]LogFunc` |
| `Format` | `string` | Defines the logging tags. See more in [Predefined Formats](#predefined-formats), or create your own using [Tags](#constants). | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` (same as `DefaultFormat`) |
| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` |
| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` |
Expand Down
19 changes: 19 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,25 @@ app.Use(logger.New(logger.Config{

Both approaches ensure your logger can access these values while respecting Go's context practices.

The same template/tag mechanism is also available for application logs emitted through `log.WithContext`. This keeps request logging and handler logging consistent without hard-coding middleware-specific values into the `log` package:

```go
app.Use(requestid.New())

// The requestid middleware automatically registers the ${requestid} tag.
if err := log.Format("[${requestid}] "); err != nil {
log.Fatal(err)
}

app.Get("/", func(c fiber.Ctx) error {
// Pass c so middleware values stored on Fiber's request context can be read.
log.WithContext(c).Info("handling request")
return c.SendString("OK")
})
```
Comment thread
ReneWerner87 marked this conversation as resolved.

`SetContextTemplate` configures Fiber's built-in default logger. Custom loggers registered with `log.SetLogger` keep control over their own `WithContext` behavior.

The `Skip` is a function to determine if logging is skipped or written to `Stream`.

<details>
Expand Down
8 changes: 8 additions & 0 deletions internal/logtemplate/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package logtemplate

import (
"errors"
)

// ErrParameterMissing indicates that a template parameter was referenced but not provided.
var ErrParameterMissing = errors.New("logtemplate: template parameter missing")
131 changes: 131 additions & 0 deletions internal/logtemplate/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package logtemplate

import (
"bytes"
"fmt"
"io"

"github.com/gofiber/utils/v2"
)

const (
startTag = "${"
endTag = "}"
paramSeparator = ":"
)

// Buffer abstracts the buffer operations used when rendering log templates.
type Buffer interface {
Len() int
ReadFrom(r io.Reader) (int64, error)
WriteTo(w io.Writer) (int64, error)
Bytes() []byte
Write(p []byte) (int, error)
WriteByte(c byte) error
WriteString(s string) (int, error)
Set(p []byte)
SetString(s string)
String() string
}

// Func renders one dynamic template tag.
type Func[C, D any] func(output Buffer, ctx C, data *D, extraParam string) (int, error)

// Template is a precompiled log template.
type Template[C, D any] struct {
fixedParts [][]byte
funcChain []Func[C, D]
}

// Build parses format once and returns a reusable template.
func Build[C, D any](format string, tagFunctions map[string]Func[C, D]) (*Template[C, D], error) {
Comment thread
ReneWerner87 marked this conversation as resolved.
templateB := utils.UnsafeBytes(format)
startTagB := utils.UnsafeBytes(startTag)
endTagB := utils.UnsafeBytes(endTag)
paramSeparatorB := utils.UnsafeBytes(paramSeparator)

chainCapacity := 2*bytes.Count(templateB, startTagB) + 1
fixedParts := make([][]byte, 0, chainCapacity)
funcChain := make([]Func[C, D], 0, chainCapacity)

for {
before, after, found := bytes.Cut(templateB, startTagB)
if !found {
break
}

funcChain = append(funcChain, nil)
fixedParts = append(fixedParts, before)

templateB = after
before, after, found = bytes.Cut(templateB, endTagB)
if !found {
funcChain = append(funcChain, nil)
fixedParts = append(fixedParts, startTagB)
break
}

tag, param, foundParam := bytes.Cut(before, paramSeparatorB)
if foundParam {
fn, ok := tagFunctions[utils.UnsafeString(tag)+paramSeparator]
if !ok {
return nil, fmt.Errorf("%w: %q", ErrParameterMissing, utils.UnsafeString(before))
}
funcChain = append(funcChain, fn)
fixedParts = append(fixedParts, param)
} else if fn, ok := tagFunctions[utils.UnsafeString(before)]; ok {
funcChain = append(funcChain, fn)
fixedParts = append(fixedParts, nil)
} else {
return nil, fmt.Errorf("%w: %q", ErrParameterMissing, utils.UnsafeString(before))
}

templateB = after
}

funcChain = append(funcChain, nil)
fixedParts = append(fixedParts, templateB)

return &Template[C, D]{
fixedParts: fixedParts,
funcChain: funcChain,
}, nil
}

// Chains returns the fixed template parts and functions used by Execute.
func (t *Template[C, D]) Chains() ([][]byte, []Func[C, D]) {
if t == nil {
return nil, nil
}
return t.fixedParts, t.funcChain
}

// Execute renders the template into output.
func (t *Template[C, D]) Execute(output Buffer, ctx C, data *D) error {
if t == nil {
return nil
}
return ExecuteChains(output, ctx, data, t.fixedParts, t.funcChain)
}

// ExecuteChains renders precompiled template chains into output.
func ExecuteChains[C, D any](output Buffer, ctx C, data *D, fixedParts [][]byte, funcChain []Func[C, D]) error {
for i, fn := range funcChain {
switch {
case fn == nil:
if _, err := output.Write(fixedParts[i]); err != nil {
return err
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
case fixedParts[i] == nil:
if _, err := fn(output, ctx, data, ""); err != nil {
return err
}
default:
if _, err := fn(output, ctx, data, utils.UnsafeString(fixedParts[i])); err != nil {
return err
}
}
}

return nil
}
Loading
Loading