Skip to content

Commit 94ac462

Browse files
committed
feat: auto register logger middleware tags
1 parent 64fee00 commit 94ac462

10 files changed

Lines changed: 256 additions & 15 deletions

File tree

docs/middleware/logger.md

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Logger middleware for [Fiber](https://github.com/gofiber/fiber) that logs HTTP r
1010

1111
```go
1212
func New(config ...Config) fiber.Handler
13+
func RegisterTag(tag string, fn LogFunc) error
14+
func MustRegisterTag(tag string, fn LogFunc)
1315
```
1416

1517
## Examples
@@ -43,13 +45,7 @@ app.Use(logger.New(logger.Config{
4345
// Logging Request ID
4446
app.Use(requestid.New()) // Ensure requestid middleware is used before the logger
4547
app.Use(logger.New(logger.Config{
46-
CustomTags: map[string]logger.LogFunc{
47-
"requestid": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) {
48-
return output.WriteString(requestid.FromContext(c))
49-
},
50-
},
51-
// For more options, see the Config section
52-
// Use the custom tag ${requestid} as defined above.
48+
// requestid.New() registers ${requestid} automatically.
5349
Format: "${pid} ${requestid} ${status} - ${method} ${path}\n",
5450
}))
5551

@@ -118,6 +114,90 @@ app.Use(logger.New(logger.Config{
118114
}))
119115
```
120116

117+
### Auto-Registered Tags
118+
119+
Some Fiber middleware registers logger middleware tags automatically. Register the producing middleware before `logger.New()` and then use the tag in `Format`.
120+
121+
```go
122+
app.Use(requestid.New())
123+
app.Use(logger.New(logger.Config{
124+
Format: "${requestid} ${status} ${method} ${path}\n",
125+
}))
126+
```
127+
128+
The logger middleware resolves tags in this order:
129+
130+
1. Built-in logger tags, such as `${method}`, `${path}`, and `${status}`.
131+
2. Globally registered tags from Fiber middleware or `logger.RegisterTag`.
132+
3. `Config.CustomTags`, which override tags with the same name for that logger instance.
133+
134+
The following tags are registered by Fiber middleware when the middleware is initialized:
135+
136+
| Tag | Registered by | Value |
137+
| :-- | :------------ | :---- |
138+
| `${requestid}` | `requestid.New()` | Request ID stored by the requestid middleware. |
139+
| `${username}` | `basicauth.New()` | Authenticated username stored by the basicauth middleware. |
140+
| `${api-key}` | `keyauth.New()` | Redacted API key stored by the keyauth middleware. |
141+
| `${csrf-token}` | `csrf.New()` | Redacted marker when the csrf middleware stores a token. |
142+
| `${session-id}` | `session.New()` or `session.NewWithStore()` | Redacted session ID stored by the session middleware. |
143+
144+
:::note
145+
Auto-registered tags are access-log tags for `middleware/logger`. Application logs from the `log` package use their own context tag registry.
146+
:::
147+
148+
### Register Tags from Custom Middleware
149+
150+
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.
151+
152+
```go
153+
package tenantmw
154+
155+
import (
156+
"sync"
157+
158+
"github.com/gofiber/fiber/v3"
159+
"github.com/gofiber/fiber/v3/middleware/logger"
160+
)
161+
162+
var registerLoggerTagsOnce sync.Once
163+
164+
func New() fiber.Handler {
165+
registerLoggerTagsOnce.Do(func() {
166+
logger.MustRegisterTag("tenant", func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
167+
tenant, _ := c.Locals("tenant").(string)
168+
return output.WriteString(tenant)
169+
})
170+
})
171+
172+
return func(c fiber.Ctx) error {
173+
c.Locals("tenant", "acme")
174+
return c.Next()
175+
}
176+
}
177+
```
178+
179+
Use the registered tag in the logger format after installing the middleware:
180+
181+
```go
182+
app.Use(tenantmw.New())
183+
app.Use(logger.New(logger.Config{
184+
Format: "${tenant} ${status} ${method} ${path}\n",
185+
}))
186+
```
187+
188+
Use `Config.CustomTags` when one logger instance needs a local override without changing the global tag registration:
189+
190+
```go
191+
app.Use(logger.New(logger.Config{
192+
Format: "${tenant} ${status} ${method} ${path}\n",
193+
CustomTags: map[string]logger.LogFunc{
194+
"tenant": func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
195+
return output.WriteString("override")
196+
},
197+
},
198+
}))
199+
```
200+
121201
### Use Logger Middleware with Other Loggers
122202

123203
To combine the logger middleware with loggers like Zerolog, Zap, or Logrus, use the `LoggerToWriter` helper to adapt them to an `io.Writer`.
@@ -167,7 +247,7 @@ Writing to `os.File` is goroutine-safe, but custom streams may require locking t
167247
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when it returns true. | `nil` |
168248
| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` |
169249
| 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` |
170-
| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` |
250+
| 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` |
171251
| `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`) |
172252
| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` |
173253
| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` |

middleware/basicauth/basicauth.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/gofiber/fiber/v3"
1212
fiberlog "github.com/gofiber/fiber/v3/log"
13+
"github.com/gofiber/fiber/v3/middleware/logger"
1314
"github.com/gofiber/utils/v2"
1415
"golang.org/x/text/unicode/norm"
1516
)
@@ -120,6 +121,9 @@ func registerLogContextTags() {
120121
fiberlog.MustRegisterContextTag("username", func(output fiberlog.Buffer, ctx any, _ *fiberlog.ContextData, _ string) (int, error) {
121122
return output.WriteString(UsernameFromContext(ctx))
122123
})
124+
logger.MustRegisterTag("username", func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
125+
return output.WriteString(UsernameFromContext(c))
126+
})
123127
}
124128

125129
func containsCTL(s string) bool {

middleware/csrf/csrf.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/gofiber/fiber/v3"
1616
"github.com/gofiber/fiber/v3/extractors"
1717
fiberlog "github.com/gofiber/fiber/v3/log"
18+
"github.com/gofiber/fiber/v3/middleware/logger"
1819
)
1920

2021
var (
@@ -234,6 +235,12 @@ func registerLogContextTags() {
234235
}
235236
return output.WriteString(redactedKey)
236237
})
238+
logger.MustRegisterTag("csrf-token", func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
239+
if TokenFromContext(c) == "" {
240+
return 0, nil
241+
}
242+
return output.WriteString(redactedKey)
243+
})
237244
}
238245

239246
// TokenFromContext returns the token found in the context.

middleware/keyauth/keyauth.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/gofiber/fiber/v3"
1010
"github.com/gofiber/fiber/v3/extractors"
1111
fiberlog "github.com/gofiber/fiber/v3/log"
12+
"github.com/gofiber/fiber/v3/middleware/logger"
1213
"github.com/gofiber/utils/v2"
1314
)
1415

@@ -107,6 +108,13 @@ func registerLogContextTags() {
107108
}
108109
return output.WriteString(redactContextValue(token))
109110
})
111+
logger.MustRegisterTag("api-key", func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
112+
token := TokenFromContext(c)
113+
if token == "" {
114+
return 0, nil
115+
}
116+
return output.WriteString(redactContextValue(token))
117+
})
110118
}
111119

112120
func redactContextValue(value string) string {

middleware/logger/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ type Config struct {
3232
// Optional. Default: nil
3333
Done func(c fiber.Ctx, logString []byte)
3434

35-
// tagFunctions defines the custom tag action
35+
// CustomTags defines per-middleware tag functions.
36+
// CustomTags override built-in and globally registered tags with the same name.
3637
//
3738
// Optional. Default: map[string]LogFunc
3839
CustomTags map[string]LogFunc

middleware/logger/logger_test.go

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424

2525
"github.com/gofiber/fiber/v3"
2626
fiberlog "github.com/gofiber/fiber/v3/log"
27-
"github.com/gofiber/fiber/v3/middleware/requestid"
2827
)
2928

3029
const (
@@ -884,11 +883,10 @@ func Test_Response_Header(t *testing.T) {
884883

885884
app := fiber.New()
886885

887-
app.Use(requestid.New(requestid.Config{
888-
Next: nil,
889-
Header: fiber.HeaderXRequestID,
890-
Generator: func() string { return "Hello fiber!" },
891-
}))
886+
app.Use(func(c fiber.Ctx) error {
887+
c.Response().Header.Set(fiber.HeaderXRequestID, "Hello fiber!")
888+
return c.Next()
889+
})
892890
app.Use(New(Config{
893891
Format: "${respHeader:X-Request-ID}",
894892
Stream: buf,
@@ -904,6 +902,74 @@ func Test_Response_Header(t *testing.T) {
904902
require.Equal(t, "Hello fiber!", buf.String())
905903
}
906904

905+
func Test_Logger_RegisteredTag(t *testing.T) {
906+
t.Parallel()
907+
908+
const tag = "registered-test-tag"
909+
910+
require.NoError(t, RegisterTag(tag, func(output Buffer, _ fiber.Ctx, _ *Data, _ string) (int, error) {
911+
return output.WriteString("registered")
912+
}))
913+
914+
buf := bytebufferpool.Get()
915+
defer bytebufferpool.Put(buf)
916+
917+
app := fiber.New()
918+
app.Use(New(Config{
919+
Format: "${" + tag + "}",
920+
Stream: buf,
921+
}))
922+
app.Get("/", func(c fiber.Ctx) error {
923+
return c.SendStatus(fiber.StatusOK)
924+
})
925+
926+
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
927+
require.NoError(t, err)
928+
require.Equal(t, fiber.StatusOK, resp.StatusCode)
929+
require.Equal(t, "registered", buf.String())
930+
}
931+
932+
func Test_Logger_CustomTagOverridesRegisteredTag(t *testing.T) {
933+
t.Parallel()
934+
935+
const tag = "registered-override-test-tag"
936+
937+
require.NoError(t, RegisterTag(tag, func(output Buffer, _ fiber.Ctx, _ *Data, _ string) (int, error) {
938+
return output.WriteString("registered")
939+
}))
940+
941+
buf := bytebufferpool.Get()
942+
defer bytebufferpool.Put(buf)
943+
944+
app := fiber.New()
945+
app.Use(New(Config{
946+
Format: "${" + tag + "}",
947+
CustomTags: map[string]LogFunc{
948+
tag: func(output Buffer, _ fiber.Ctx, _ *Data, _ string) (int, error) {
949+
return output.WriteString("override")
950+
},
951+
},
952+
Stream: buf,
953+
}))
954+
app.Get("/", func(c fiber.Ctx) error {
955+
return c.SendStatus(fiber.StatusOK)
956+
})
957+
958+
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", http.NoBody))
959+
require.NoError(t, err)
960+
require.Equal(t, fiber.StatusOK, resp.StatusCode)
961+
require.Equal(t, "override", buf.String())
962+
}
963+
964+
func Test_Logger_RegisterTagRejectsInvalidInput(t *testing.T) {
965+
t.Parallel()
966+
967+
require.ErrorIs(t, RegisterTag("", func(output Buffer, _ fiber.Ctx, _ *Data, _ string) (int, error) {
968+
return output.WriteString("ignored")
969+
}), errTagInvalid)
970+
require.ErrorIs(t, RegisterTag("missing", nil), errTagInvalid)
971+
}
972+
907973
// go test -run Test_Req_Header
908974
func Test_Req_Header(t *testing.T) {
909975
t.Parallel()

middleware/logger/tags.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package logger
22

33
import (
4+
"errors"
45
"fmt"
56
"maps"
67
"strings"
8+
"sync"
79

810
"github.com/gofiber/fiber/v3"
911
)
@@ -50,6 +52,38 @@ const (
5052
TagReset = "reset"
5153
)
5254

55+
var errTagInvalid = errors.New("logger: tag name and function are required")
56+
57+
var registeredTags = struct {
58+
m map[string]LogFunc
59+
sync.RWMutex
60+
}{
61+
m: make(map[string]LogFunc),
62+
}
63+
64+
// RegisterTag registers a global logger middleware tag.
65+
// Registered tags are available to logger middleware instances created after
66+
// registration and can be overridden per instance with Config.CustomTags.
67+
// Re-registering a tag replaces the existing tag function.
68+
func RegisterTag(tag string, fn LogFunc) error {
69+
if tag == "" || fn == nil {
70+
return errTagInvalid
71+
}
72+
73+
registeredTags.Lock()
74+
defer registeredTags.Unlock()
75+
76+
registeredTags.m[tag] = fn
77+
return nil
78+
}
79+
80+
// MustRegisterTag registers a global logger middleware tag and panics on failure.
81+
func MustRegisterTag(tag string, fn LogFunc) {
82+
if err := RegisterTag(tag, fn); err != nil {
83+
panic(err)
84+
}
85+
}
86+
5387
// createTagMap function merged the default with the custom tags
5488
func createTagMap(cfg *Config) map[string]LogFunc {
5589
// Set default tags
@@ -204,6 +238,10 @@ func createTagMap(cfg *Config) map[string]LogFunc {
204238
return output.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert,errcheck // We always store a string in here
205239
},
206240
}
241+
registeredTags.RLock()
242+
maps.Copy(tagFunctions, registeredTags.m)
243+
registeredTags.RUnlock()
244+
207245
// merge with custom tags from user
208246
maps.Copy(tagFunctions, cfg.CustomTags)
209247

middleware/requestid/requestid.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/gofiber/fiber/v3"
77
fiberlog "github.com/gofiber/fiber/v3/log"
8+
"github.com/gofiber/fiber/v3/middleware/logger"
89
"github.com/gofiber/utils/v2"
910
)
1011

@@ -51,6 +52,9 @@ func registerLogContextTags() {
5152
}
5253
fiberlog.MustRegisterContextTag("requestid", tag)
5354
fiberlog.MustRegisterContextTag("request-id", tag)
55+
logger.MustRegisterTag("requestid", func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
56+
return output.WriteString(FromContext(c))
57+
})
5458
}
5559

5660
// sanitizeRequestID returns the provided request ID when it is valid, otherwise

0 commit comments

Comments
 (0)