Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 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
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
9 changes: 7 additions & 2 deletions ctx_interface_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 26 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,29 @@ 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. Configure the default logger with `SetContextTemplate` and custom tags when the value is stored by middleware using package-private context keys.

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

log.SetContextTemplate(log.ContextConfig{
Format: "[${requestid}] ",
CustomTags: map[string]log.ContextTagFunc{
"requestid": func(output log.Buffer, ctx any, _ *log.ContextData, _ string) (int, error) {
return output.WriteString(requestid.FromContext(ctx))
},
},
})

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

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. For ordinary string keys on values with `Value` or `UserValue` lookup methods, use the built-in `${value:key}` tag.

## Logger

Expand Down
20 changes: 20 additions & 0 deletions docs/middleware/logger.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ Import the package:
```go
import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/log"
"github.com/gofiber/fiber/v3/middleware/logger"
"github.com/gofiber/fiber/v3/middleware/requestid"
)
```

Expand Down Expand Up @@ -52,6 +54,24 @@ app.Use(logger.New(logger.Config{
Format: "${pid} ${requestid} ${status} - ${method} ${path}\n",
}))

// Reuse the same tag approach for application logs emitted inside handlers.
// This configures Fiber's built-in default logger. Custom loggers registered
// with log.SetLogger should implement their own WithContext enrichment.
log.SetContextTemplate(log.ContextConfig{
Format: "[${requestid}] ",
CustomTags: map[string]log.ContextTagFunc{
"requestid": func(output log.Buffer, c any, _ *log.ContextData, _ string) (int, error) {
return output.WriteString(requestid.FromContext(c))
},
},
})

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

// Changing TimeZone & TimeFormat
app.Use(logger.New(logger.Config{
Format: "${pid} ${status} - ${method} ${path}\n",
Expand Down
23 changes: 23 additions & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -1587,6 +1587,29 @@ 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())

log.SetContextTemplate(log.ContextConfig{
Format: "[${requestid}] ",
CustomTags: map[string]log.ContextTagFunc{
"requestid": func(output log.Buffer, ctx any, _ *log.ContextData, _ string) (int, error) {
return output.WriteString(requestid.FromContext(ctx))
},
},
})

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("logger: template parameter missing")
Comment thread
ReneWerner87 marked this conversation as resolved.
Outdated
128 changes: 128 additions & 0 deletions internal/logtemplate/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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) {
templateB := utils.UnsafeBytes(format)
startTagB := utils.UnsafeBytes(startTag)
endTagB := utils.UnsafeBytes(endTag)
paramSeparatorB := utils.UnsafeBytes(paramSeparator)

fixedParts := make([][]byte, 0, bytes.Count(templateB, startTagB)+1)
funcChain := make([]Func[C, D], 0, bytes.Count(templateB, startTagB)+1)
Comment thread
ReneWerner87 marked this conversation as resolved.
Outdated

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)
}
Comment thread
ReneWerner87 marked this conversation as resolved.

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
}
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
}
41 changes: 41 additions & 0 deletions internal/logtemplate/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package logtemplate

import (
"testing"

"github.com/stretchr/testify/require"
"github.com/valyala/bytebufferpool"
)

type testData struct {
value string
}

func Test_Template_Execute(t *testing.T) {
t.Parallel()

tmpl, err := Build[string, testData]("a ${tag} ${param:name} z", map[string]Func[string, testData]{
"tag": func(output Buffer, ctx string, data *testData, _ string) (int, error) {
return output.WriteString(ctx + data.value)
},
"param:": func(output Buffer, _ string, _ *testData, extraParam string) (int, error) {
return output.WriteString(extraParam)
},
})
require.NoError(t, err)

buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)

err = tmpl.Execute(buf, "ctx-", &testData{value: "data"})
require.NoError(t, err)
require.Equal(t, "a ctx-data name z", buf.String())
}

func Test_Template_MissingParameterTag(t *testing.T) {
t.Parallel()

_, err := Build[string, testData]("${missing:name}", nil)
require.ErrorIs(t, err, ErrParameterMissing)
require.ErrorContains(t, err, "missing:name")
}
Loading
Loading