Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
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.

30 changes: 28 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,31 @@ 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())

if err := 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))
},
},
}); err != nil {
log.Fatal(err)
}

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
22 changes: 22 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,26 @@ 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.
if err := 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))
},
},
}); 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")
})

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

if err := 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))
},
},
}); 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")
129 changes: 129 additions & 0 deletions internal/logtemplate/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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)

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