From 4177ab4086a97648553f34bcff2ff81a137d31f3 Mon Sep 17 00:00:00 2001 From: vinicius Date: Fri, 7 Mar 2025 04:23:24 -0300 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=94=A5=20feat:=20Add=20support=20for?= =?UTF-8?q?=20context.Context=20in=20keyauth=20middleware=20(#3287)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(middleware): add support to context.Context in keyauth middleware pretty straightforward option to use context.Context instead of just fiber.Ctx, tests added accordingly. * fix(middleware): include import that was missing from previous commit * fix(middleware): include missing import * Replace logger with panic * Update keyauth_test.go * Update keyauth_test.go --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> --- middleware/keyauth/keyauth.go | 22 ++++++-- middleware/keyauth/keyauth_test.go | 82 +++++++++++++++++++++--------- 2 files changed, 75 insertions(+), 29 deletions(-) diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index e245ba42..54ecdbe5 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -2,6 +2,7 @@ package keyauth import ( + "context" "errors" "fmt" "net/url" @@ -59,7 +60,10 @@ func New(config ...Config) fiber.Handler { valid, err := cfg.Validator(c, key) if err == nil && valid { + // Store in both Locals and Context c.Locals(tokenKey, key) + ctx := context.WithValue(c.Context(), tokenKey, key) + c.SetContext(ctx) return cfg.SuccessHandler(c) } return cfg.ErrorHandler(c, err) @@ -68,12 +72,20 @@ func New(config ...Config) fiber.Handler { // TokenFromContext returns the bearer token from the request context. // returns an empty string if the token does not exist -func TokenFromContext(c fiber.Ctx) string { - token, ok := c.Locals(tokenKey).(string) - if !ok { - return "" +func TokenFromContext(c any) string { + switch ctx := c.(type) { + case context.Context: + if token, ok := ctx.Value(tokenKey).(string); ok { + return token + } + case fiber.Ctx: + if token, ok := ctx.Locals(tokenKey).(string); ok { + return token + } + default: + panic("unsupported context type, expected fiber.Ctx or context.Context") } - return token + return "" } // MultipleKeySourceLookup creates a CustomKeyLookup function that checks multiple sources until one is found diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index 72c9d3c1..27c4e5a0 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -503,33 +503,67 @@ func Test_TokenFromContext_None(t *testing.T) { } func Test_TokenFromContext(t *testing.T) { - app := fiber.New() - // Wire up keyauth middleware to set TokenFromContext now - app.Use(New(Config{ - KeyLookup: "header:Authorization", - AuthScheme: "Basic", - Validator: func(_ fiber.Ctx, key string) (bool, error) { - if key == CorrectKey { - return true, nil - } - return false, ErrMissingOrMalformedAPIKey - }, - })) - // Define a test handler that checks TokenFromContext - app.Get("/", func(c fiber.Ctx) error { - return c.SendString(TokenFromContext(c)) + // Test that TokenFromContext returns the correct token + t.Run("fiber.Ctx", func(t *testing.T) { + app := fiber.New() + app.Use(New(Config{ + KeyLookup: "header:Authorization", + AuthScheme: "Basic", + Validator: func(_ fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + app.Get("/", func(c fiber.Ctx) error { + return c.SendString(TokenFromContext(c)) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", "Basic "+CorrectKey) + res, err := app.Test(req) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, CorrectKey, string(body)) }) - req := httptest.NewRequest(fiber.MethodGet, "/", nil) - req.Header.Add("Authorization", "Basic "+CorrectKey) - // Send - res, err := app.Test(req) - require.NoError(t, err) + t.Run("context.Context", func(t *testing.T) { + app := fiber.New() + app.Use(New(Config{ + KeyLookup: "header:Authorization", + AuthScheme: "Basic", + Validator: func(_ fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + // Verify that TokenFromContext works with context.Context + app.Get("/", func(c fiber.Ctx) error { + ctx := c.Context() + token := TokenFromContext(ctx) + return c.SendString(token) + }) - // Read the response body into a string - body, err := io.ReadAll(res.Body) - require.NoError(t, err) - require.Equal(t, CorrectKey, string(body)) + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", "Basic "+CorrectKey) + res, err := app.Test(req) + require.NoError(t, err) + + body, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.Equal(t, CorrectKey, string(body)) + }) + + t.Run("invalid context type", func(t *testing.T) { + require.Panics(t, func() { + _ = TokenFromContext("invalid") + }) + }) } func Test_AuthSchemeToken(t *testing.T) { From 600ebd95ce741474724de93ea4ca920678f60e54 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Fri, 7 Mar 2025 22:33:22 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20replace=20?= =?UTF-8?q?isInCharset=20with=20bytes.IndexByte=20(#3342)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ctx.go | 2 +- path.go | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/ctx.go b/ctx.go index 07ffb1db..b2d13c0a 100644 --- a/ctx.go +++ b/ctx.go @@ -1349,7 +1349,7 @@ func (c *DefaultCtx) getLocationFromRoute(route Route, params Map) (string, erro for key, val := range params { isSame := key == segment.ParamName || (!c.app.config.CaseSensitive && utils.EqualFold(key, segment.ParamName)) - isGreedy := segment.IsGreedy && len(key) == 1 && isInCharset(key[0], greedyParameters) + isGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) != -1 if isSame || isGreedy { _, err := buf.WriteString(utils.ToString(val)) if err != nil { diff --git a/path.go b/path.go index fdd61e39..76236e40 100644 --- a/path.go +++ b/path.go @@ -7,6 +7,7 @@ package fiber import ( + "bytes" "regexp" "strconv" "strings" @@ -308,7 +309,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst parameterEndPosition = 0 case parameterEndPosition == -1: parameterEndPosition = len(pattern) - 1 - case !isInCharset(pattern[parameterEndPosition+1], parameterDelimiterChars): + case bytes.IndexByte(parameterDelimiterChars, pattern[parameterEndPosition+1]) == -1: parameterEndPosition++ } @@ -397,16 +398,6 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst return n, segment } -// isInCharset check is the given character in the charset list -func isInCharset(searchChar byte, charset []byte) bool { - for _, char := range charset { - if char == searchChar { - return true - } - } - return false -} - // findNextCharsetPosition search the next char position from the charset func findNextCharsetPosition(search string, charset []byte) int { nextPosition := -1 From 1b26cf6b5eefb75899cbe4b97fd0c048eded6591 Mon Sep 17 00:00:00 2001 From: Kashiwa <13825170+ksw2000@users.noreply.github.com> Date: Mon, 10 Mar 2025 16:04:04 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20optimize?= =?UTF-8?q?=20routeParser=20by=20using=20sync.Pool=20(#3343)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ Refactor: add routerParser pool ``` goos: linux goarch: amd64 pkg: github.com/gofiber/fiber/v3 cpu: AMD EPYC 9J14 96-Core Processor │ ori.txt │ pool.txt │ │ sec/op │ sec/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-16 173.9n ± 0% 159.3n ± 1% -8.37% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-16 163.9n ± 0% 150.9n ± 0% -7.90% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-16 165.4n ± 1% 150.6n ± 1% -8.95% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-16 174.9n ± 0% 160.6n ± 0% -8.15% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-16 520.2n ± 0% 438.1n ± 1% -15.78% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-16 521.8n ± 0% 436.8n ± 0% -16.29% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-16 630.0n ± 0% 525.0n ± 0% -16.67% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-16 633.3n ± 0% 526.4n ± 0% -16.89% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-16 627.8n ± 0% 527.5n ± 0% -15.97% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-16 602.1n ± 0% 501.9n ± 0% -16.65% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-16 604.9n ± 0% 504.3n ± 0% -16.62% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-16 616.7n ± 0% 512.8n ± 1% -16.86% (p=0.000 n=20) geomean 390.5n 336.5n -13.84% │ ori.txt │ pool.txt │ │ B/op │ B/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-16 152.0 ± 0% 144.0 ± 0% -5.26% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-16 144.0 ± 0% 136.0 ± 0% -5.56% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-16 144.0 ± 0% 136.0 ± 0% -5.56% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-16 160.0 ± 0% 152.0 ± 0% -5.00% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-16 440.0 ± 0% 368.0 ± 0% -16.36% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-16 440.0 ± 0% 368.0 ± 0% -16.36% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-16 536.0 ± 0% 432.0 ± 0% -19.40% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-16 536.0 ± 0% 432.0 ± 0% -19.40% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-16 536.0 ± 0% 432.0 ± 0% -19.40% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-16 528.0 ± 0% 424.0 ± 0% -19.70% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-16 528.0 ± 0% 424.0 ± 0% -19.70% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-16 528.0 ± 0% 424.0 ± 0% -19.70% (p=0.000 n=20) geomean 337.9 288.8 -14.52% │ ori.txt │ pool.txt │ │ allocs/op │ allocs/op vs base │ _RoutePatternMatch//api/v1/const_|_match_|_/api/v1/const-16 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1-16 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/-16 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=20) _RoutePatternMatch//api/v1/const_|_not_match_|_/api/v1/something-16 5.000 ± 0% 4.000 ± 0% -20.00% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_match_|_/api/abc/fixedEnd-16 13.000 ± 0% 9.000 ± 0% -30.77% (p=0.000 n=20) _RoutePatternMatch//api/:param/fixedEnd_|_not_match_|_/api/abc/def/fixedEnd-16 13.000 ± 0% 9.000 ± 0% -30.77% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_match_|_/api/v1/entity/1-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v2-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) _RoutePatternMatch//api/v1/:param/*_|_not_match_|_/api/v1/-16 14.000 ± 0% 9.000 ± 0% -35.71% (p=0.000 n=20) geomean 9.811 6.868 -29.99% ``` * 🩹 Fix: golangci-lint problem --- path.go | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/path.go b/path.go index 76236e40..3876943d 100644 --- a/path.go +++ b/path.go @@ -11,6 +11,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "unicode" @@ -26,6 +27,12 @@ type routeParser struct { plusCount int // number of plus parameters, used internally to give the plus parameter its number } +var routerParserPool = &sync.Pool{ + New: func() any { + return &routeParser{} + }, +} + // routeSegment holds the segment metadata type routeSegment struct { // const information @@ -163,7 +170,10 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { patternPretty = utils.TrimRight(patternPretty, '/') } - parser := parseRoute(string(patternPretty)) + parser, _ := routerParserPool.Get().(*routeParser) //nolint:errcheck // only contains routeParser + parser.reset() + parser.parseRoute(string(patternPretty)) + defer routerParserPool.Put(parser) if string(patternPretty) == "/" && path == "/" { return true @@ -184,10 +194,16 @@ func RoutePatternMatch(path, pattern string, cfg ...Config) bool { return string(patternPretty) == path } +func (parser *routeParser) reset() { + parser.segs = parser.segs[:0] + parser.params = parser.params[:0] + parser.wildCardCount = 0 + parser.plusCount = 0 +} + // parseRoute analyzes the route and divides it into segments for constant areas and parameters, // this information is needed later when assigning the requests to the declared routes -func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser { - parser := routeParser{} +func (parser *routeParser) parseRoute(pattern string, customConstraints ...CustomConstraint) { var n int var seg *routeSegment for len(pattern) > 0 { @@ -207,7 +223,13 @@ func parseRoute(pattern string, customConstraints ...CustomConstraint) routePars parser.segs[len(parser.segs)-1].IsLast = true } parser.segs = addParameterMetaInfo(parser.segs) +} +// parseRoute analyzes the route and divides it into segments for constant areas and parameters, +// this information is needed later when assigning the requests to the declared routes +func parseRoute(pattern string, customConstraints ...CustomConstraint) routeParser { + parser := routeParser{} + parser.parseRoute(pattern, customConstraints...) return parser } @@ -290,7 +312,7 @@ func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) ( } // analyseParameterPart find the parameter end and create the route segment -func (routeParser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (int, *routeSegment) { +func (parser *routeParser) analyseParameterPart(pattern string, customConstraints ...CustomConstraint) (int, *routeSegment) { isWildCard := pattern[0] == wildcardParam isPlusParam := pattern[0] == plusParam @@ -377,11 +399,11 @@ func (routeParser *routeParser) analyseParameterPart(pattern string, customConst // add access iterator to wildcard and plus if isWildCard { - routeParser.wildCardCount++ - paramName += strconv.Itoa(routeParser.wildCardCount) + parser.wildCardCount++ + paramName += strconv.Itoa(parser.wildCardCount) } else if isPlusParam { - routeParser.plusCount++ - paramName += strconv.Itoa(routeParser.plusCount) + parser.plusCount++ + paramName += strconv.Itoa(parser.plusCount) } segment := &routeSegment{ @@ -465,9 +487,9 @@ func splitNonEscaped(s, sep string) []string { } // getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions -func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint: revive // Accepting a bool param is fine here +func (parser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint: revive // Accepting a bool param is fine here var i, paramsIterator, partLen int - for _, segment := range routeParser.segs { + for _, segment := range parser.segs { partLen = len(detectionPath) // check const segment if !segment.IsParam { From c0599ee1d427d7dfa6241150109267b1d61a2cf4 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Mon, 10 Mar 2025 16:06:11 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=A5=20feat:=20Add=20Skip=20functio?= =?UTF-8?q?n=20to=20logger=20middleware=20(#3333)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔥 Feature(logger): Add Filter option to logger middleware * 📚 Doc(logger): Clarify Filter middleware description * 🚨 Test(logger): Enhance logger filter test with parallel subtests * 🔒 Test(logger): Add mutex to prevent race conditions in logger test * 🔥 Feature(logger): Add Filter option to logger middleware * 📚 Doc(logger): Clarify Filter middleware description * 🚨 Test(logger): Enhance logger filter test with parallel subtests * 🔒 Test(logger): Add mutex to prevent race conditions in logger test * 🚨 Test(logger): Refactor logger test to improve test isolation * Fix issue with unit-tests * Update middleware/logger/logger_test.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Apply logger filter as soon as possible * 📚 Doc: Add logger filter configuration example to whats_new.md * 📚 Doc: Update logger filter documentation in whats_new.md * 📚 Doc: Update logger filter documentation and examples * 🩹 Fix: improve what_new.md * Update logic for Filter() in Logger middleware. Add more unit-tests * Rename fields to match expressjs/morgan * Update middleware/logger/default_logger.go --------- Co-authored-by: Juan Calderon-Perez Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: RW --- docs/middleware/logger.md | 25 ++-- docs/whats_new.md | 25 ++++ middleware/logger/config.go | 21 ++- middleware/logger/default_logger.go | 14 +- middleware/logger/logger_test.go | 221 +++++++++++++++++++++++----- 5 files changed, 243 insertions(+), 63 deletions(-) diff --git a/docs/middleware/logger.md b/docs/middleware/logger.md index 07ff07c4..af16f384 100644 --- a/docs/middleware/logger.md +++ b/docs/middleware/logger.md @@ -55,13 +55,13 @@ app.Use(logger.New(logger.Config{ })) // Custom File Writer -file, err := os.OpenFile("./123.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) +accessLog, err := os.OpenFile("./access.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { - log.Fatalf("error opening file: %v", err) + log.Fatalf("error opening access.log file: %v", err) } -defer file.Close() +defer accessLog.Close() app.Use(logger.New(logger.Config{ - Output: file, + Stream: accessLog, })) // Add Custom Tags @@ -115,7 +115,7 @@ func main() { // Use the logger middleware with zerolog logger app.Use(logger.New(logger.Config{ - Output: logger.LoggerToWriter(zap, log.LevelDebug), + Stream: logger.LoggerToWriter(zap, log.LevelDebug), })) // Define a route @@ -129,7 +129,7 @@ func main() { ``` :::tip -Writing to os.File is goroutine-safe, but if you are using a custom Output that is not goroutine-safe, make sure to implement locking to properly serialize writes. +Writing to os.File is goroutine-safe, but if you are using a custom Stream that is not goroutine-safe, make sure to implement locking to properly serialize writes. ::: ## Config @@ -138,31 +138,30 @@ Writing to os.File is goroutine-safe, but if you are using a custom Output that | Property | Type | Description | Default | |:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------| -| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | -| Done | `func(fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Output, and pass the log string as parameter. | `nil` | +| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned 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` | | Format | `string` | Format defines the logging tags. | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` | | 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"` | | TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | -| Output | `io.Writer` | Output is a writer where logs are written. | `os.Stdout` | +| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` | | LoggerFunc | `func(c fiber.Ctx, data *Data, cfg Config) error` | Custom logger function for integration with logging libraries (Zerolog, Zap, Logrus, etc). Defaults to Fiber's default logger if not defined. | `see default_logger.go defaultLoggerInstance` | | DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | -| enableColors | `bool` | Internal field for enabling colors in the log output. (This is not a user-configurable field) | - | -| enableLatency | `bool` | Internal field for enabling latency measurement in logs. (This is not a user-configurable field) | - | -| timeZoneLocation | `*time.Location` | Internal field for the time zone location. (This is not a user-configurable field) | - | ## Default Config ```go var ConfigDefault = Config{ Next: nil, + Skip nil, Done: nil, Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n", TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, - Output: os.Stdout, + Stream: os.Stdout, DisableColors: false, LoggerFunc: defaultLoggerInstance, } diff --git a/docs/whats_new.md b/docs/whats_new.md index 5c3dd6ac..8528dc41 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -912,6 +912,31 @@ func main() { +The `Skip` is a function to determine if logging is skipped or written to `Stream`. + +
+Example Usage + +```go +app.Use(logger.New(logger.Config{ + Skip: func(c fiber.Ctx) bool { + // Skip logging HTTP 200 requests + return c.Response().StatusCode() == fiber.StatusOK + }, +})) +``` + +```go +app.Use(logger.New(logger.Config{ + Skip: func(c fiber.Ctx) bool { + // Only log errors, similar to an error.log + return c.Response().StatusCode() < 400 + }, +})) +``` + +
+ ### Filesystem We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. diff --git a/middleware/logger/config.go b/middleware/logger/config.go index 4826151e..2df814eb 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -10,16 +10,21 @@ import ( // Config defines the config for middleware. type Config struct { - // Output is a writer where logs are written + // Stream is a writer where logs are written // // Default: os.Stdout - Output io.Writer + Stream io.Writer // Next defines a function to skip this middleware when returned true. // // Optional. Default: nil Next func(c fiber.Ctx) bool + // Skip is a function to determine if logging is skipped or written to Stream. + // + // Optional. Default: nil + Skip func(c fiber.Ctx) bool + // Done is a function that is called after the log string for a request is written to Output, // and pass the log string as parameter. // @@ -98,12 +103,13 @@ type LogFunc func(output Buffer, c fiber.Ctx, data *Data, extraParam string) (in // ConfigDefault is the default config var ConfigDefault = Config{ Next: nil, + Skip: nil, Done: nil, Format: defaultFormat, TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, - Output: os.Stdout, + Stream: os.Stdout, BeforeHandlerFunc: beforeHandlerFunc, LoggerFunc: defaultLoggerInstance, enableColors: true, @@ -126,6 +132,9 @@ func configDefault(config ...Config) Config { if cfg.Next == nil { cfg.Next = ConfigDefault.Next } + if cfg.Skip == nil { + cfg.Skip = ConfigDefault.Skip + } if cfg.Done == nil { cfg.Done = ConfigDefault.Done } @@ -141,8 +150,8 @@ func configDefault(config ...Config) Config { if int(cfg.TimeInterval) <= 0 { cfg.TimeInterval = ConfigDefault.TimeInterval } - if cfg.Output == nil { - cfg.Output = ConfigDefault.Output + if cfg.Stream == nil { + cfg.Stream = ConfigDefault.Stream } if cfg.BeforeHandlerFunc == nil { @@ -154,7 +163,7 @@ func configDefault(config ...Config) Config { } // Enable colors if no custom format or output is given - if !cfg.DisableColors && cfg.Output == ConfigDefault.Output { + if !cfg.DisableColors && cfg.Stream == ConfigDefault.Stream { cfg.enableColors = true } diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index 369b2c85..e4de79bf 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -15,6 +15,12 @@ import ( // default logger for fiber func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { + // Check if Skip is defined and call it. + // Now, if Skip(c) == true, we SKIP logging: + if cfg.Skip != nil && cfg.Skip(c) { + return nil // Skip logging if Skip returns true + } + // Alias colors colors := c.App().Config().ColorScheme @@ -91,7 +97,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { } // Write buffer to output - writeLog(cfg.Output, buf.Bytes()) + writeLog(cfg.Stream, buf.Bytes()) if cfg.Done != nil { cfg.Done(c, buf.Bytes()) @@ -125,7 +131,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { buf.WriteString(err.Error()) } - writeLog(cfg.Output, buf.Bytes()) + writeLog(cfg.Stream, buf.Bytes()) if cfg.Done != nil { cfg.Done(c, buf.Bytes()) @@ -141,9 +147,9 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { func beforeHandlerFunc(cfg Config) { // If colors are enabled, check terminal compatibility if cfg.enableColors { - cfg.Output = colorable.NewColorableStdout() + cfg.Stream = colorable.NewColorableStdout() if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { - cfg.Output = colorable.NewNonColorable(os.Stdout) + cfg.Stream = colorable.NewNonColorable(os.Stdout) } } } diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index d459f22c..011a0ead 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -71,7 +71,7 @@ func Test_Logger(t *testing.T) { app.Use(New(Config{ Format: "${error}", - Output: buf, + Stream: buf, })) app.Get("/", func(_ fiber.Ctx) error { @@ -94,7 +94,7 @@ func Test_Logger_locals(t *testing.T) { app.Use(New(Config{ Format: "${locals:demo}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { @@ -171,6 +171,147 @@ func Test_Logger_Done(t *testing.T) { require.Positive(t, buf.Len(), 0) } +// Test_Logger_Filter tests the Filter functionality of the logger middleware. +// It verifies that logs are written or skipped based on the filter condition. +func Test_Logger_Filter(t *testing.T) { + t.Parallel() + + t.Run("Test Not Found", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Return true to skip logging for all requests != 404 + app.Use(New(Config{ + Skip: func(c fiber.Ctx) bool { + return c.Response().StatusCode() != fiber.StatusNotFound + }, + Stream: &logOutput, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/nonexistent", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + // Expect logs for the 404 request + require.Contains(t, logOutput.String(), "404") + }) + + t.Run("Test OK", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Return true to skip logging for all requests == 200 + app.Use(New(Config{ + Skip: func(c fiber.Ctx) bool { + return c.Response().StatusCode() == fiber.StatusOK + }, + Stream: &logOutput, + })) + + app.Get("/", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) + + // We skip logging for status == 200, so "200" should not appear + require.NotContains(t, logOutput.String(), "200") + }) + + t.Run("Always Skip", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Filter always returns true => skip all logs + app.Use(New(Config{ + Skip: func(_ fiber.Ctx) bool { + return true // always skip + }, + Stream: &logOutput, + })) + + app.Get("/something", func(c fiber.Ctx) error { + return c.Status(fiber.StatusTeapot).SendString("I'm a teapot") + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/something", nil)) + require.NoError(t, err) + + // Expect NO logs + require.Empty(t, logOutput.String()) + }) + + t.Run("Never Skip", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Filter always returns false => never skip logs + app.Use(New(Config{ + Skip: func(_ fiber.Ctx) bool { + return false // never skip + }, + Stream: &logOutput, + })) + + app.Get("/always", func(c fiber.Ctx) error { + return c.Status(fiber.StatusTeapot).SendString("Teapot again") + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/always", nil)) + require.NoError(t, err) + + // Expect some logging - check any substring + require.Contains(t, logOutput.String(), strconv.Itoa(fiber.StatusTeapot)) + }) + + t.Run("Skip /healthz", func(t *testing.T) { + t.Parallel() + app := fiber.New() + + logOutput := bytes.Buffer{} + + // Filter returns true (skip logs) if the request path is /healthz + app.Use(New(Config{ + Skip: func(c fiber.Ctx) bool { + return c.Path() == "/healthz" + }, + Stream: &logOutput, + })) + + // Normal route + app.Get("/", func(c fiber.Ctx) error { + return c.SendString("Hello World!") + }) + // Health route + app.Get("/healthz", func(c fiber.Ctx) error { + return c.SendString("OK") + }) + + // Request to "/" -> should be logged + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Contains(t, logOutput.String(), "200") + + // Reset output buffer + logOutput.Reset() + + // Request to "/healthz" -> should be skipped + _, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/healthz", nil)) + require.NoError(t, err) + require.Empty(t, logOutput.String()) + }) +} + // go test -run Test_Logger_ErrorTimeZone func Test_Logger_ErrorTimeZone(t *testing.T) { t.Parallel() @@ -234,7 +375,7 @@ func Test_Logger_LoggerToWriter(t *testing.T) { app.Use("/"+level, New(Config{ Format: "${error}", - Output: LoggerToWriter(logger, tc. + Stream: LoggerToWriter(logger, tc. level), })) @@ -276,7 +417,7 @@ func Test_Logger_ErrorOutput_WithoutColor(t *testing.T) { app := fiber.New() app.Use(New(Config{ - Output: o, + Stream: o, DisableColors: true, })) @@ -293,7 +434,7 @@ func Test_Logger_ErrorOutput(t *testing.T) { app := fiber.New() app.Use(New(Config{ - Output: o, + Stream: o, })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -312,7 +453,7 @@ func Test_Logger_All(t *testing.T) { app.Use(New(Config{ Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}", - Output: buf, + Stream: buf, })) // Alias colors @@ -358,7 +499,7 @@ func Test_Logger_WithLatency(t *testing.T) { app := fiber.New() logger := New(Config{ - Output: buff, + Stream: buff, Format: "${latency}", }) app.Use(logger) @@ -403,7 +544,7 @@ func Test_Logger_WithLatency_DefaultFormat(t *testing.T) { app := fiber.New() logger := New(Config{ - Output: buff, + Stream: buff, }) app.Use(logger) @@ -453,7 +594,7 @@ func Test_Query_Params(t *testing.T) { app.Use(New(Config{ Format: "${queryParams}", - Output: buf, + Stream: buf, })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar&baz=moz", nil)) @@ -474,7 +615,7 @@ func Test_Response_Body(t *testing.T) { app.Use(New(Config{ Format: "${resBody}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { @@ -508,7 +649,7 @@ func Test_Request_Body(t *testing.T) { app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: buf, + Stream: buf, })) app.Post("/", func(c fiber.Ctx) error { @@ -536,7 +677,7 @@ func Test_Logger_AppendUint(t *testing.T) { app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { @@ -611,7 +752,7 @@ func Test_Response_Header(t *testing.T) { })) app.Use(New(Config{ Format: "${respHeader:X-Request-ID}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") @@ -634,7 +775,7 @@ func Test_Req_Header(t *testing.T) { app.Use(New(Config{ Format: "${reqHeader:test}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") @@ -658,7 +799,7 @@ func Test_ReqHeader_Header(t *testing.T) { app.Use(New(Config{ Format: "${reqHeader:test}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") @@ -689,7 +830,7 @@ func Test_CustomTags(t *testing.T) { return output.WriteString(customTag) }, }, - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello fiber!") @@ -713,7 +854,7 @@ func Test_Logger_ByteSent_Streaming(t *testing.T) { app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: buf, + Stream: buf, })) app.Get("/", func(c fiber.Ctx) error { @@ -759,7 +900,7 @@ func Test_Logger_EnableColors(t *testing.T) { app := fiber.New() app.Use(New(Config{ - Output: o, + Stream: o, })) resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) @@ -782,7 +923,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "test") @@ -794,7 +935,7 @@ func Benchmark_Logger(b *testing.B) { b.Run("DefaultFormat", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -805,7 +946,7 @@ func Benchmark_Logger(b *testing.B) { b.Run("DefaultFormatDisableColors", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ - Output: io.Discard, + Stream: io.Discard, DisableColors: true, })) app.Get("/", func(c fiber.Ctx) error { @@ -819,7 +960,7 @@ func Benchmark_Logger(b *testing.B) { logger := fiberlog.DefaultLogger() logger.SetOutput(io.Discard) app.Use(New(Config{ - Output: LoggerToWriter(logger, fiberlog.LevelDebug), + Stream: LoggerToWriter(logger, fiberlog.LevelDebug), })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -831,7 +972,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "test") @@ -844,7 +985,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${locals:demo}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Locals("demo", "johndoe") @@ -857,7 +998,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${locals:demo}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/int", func(c fiber.Ctx) error { c.Locals("demo", 55) @@ -874,7 +1015,7 @@ func Benchmark_Logger(b *testing.B) { io.Discard.Write(logString) //nolint:errcheck // ignore error } }, - Output: io.Discard, + Stream: io.Discard, })) app.Get("/logging", func(ctx fiber.Ctx) error { return ctx.SendStatus(fiber.StatusOK) @@ -886,7 +1027,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -898,7 +1039,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("Connection", "keep-alive") @@ -927,7 +1068,7 @@ func Benchmark_Logger(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${resBody}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Sample response body") @@ -950,7 +1091,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "test") @@ -962,7 +1103,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { b.Run("DefaultFormat", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -975,7 +1116,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { logger := fiberlog.DefaultLogger() logger.SetOutput(io.Discard) app.Use(New(Config{ - Output: LoggerToWriter(logger, fiberlog.LevelDebug), + Stream: LoggerToWriter(logger, fiberlog.LevelDebug), })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -986,7 +1127,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { b.Run("DefaultFormatDisableColors", func(bb *testing.B) { app := fiber.New() app.Use(New(Config{ - Output: io.Discard, + Stream: io.Discard, DisableColors: true, })) app.Get("/", func(c fiber.Ctx) error { @@ -999,7 +1140,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("test", "test") @@ -1012,7 +1153,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${locals:demo}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Locals("demo", "johndoe") @@ -1025,7 +1166,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${locals:demo}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/int", func(c fiber.Ctx) error { c.Locals("demo", 55) @@ -1042,7 +1183,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { io.Discard.Write(logString) //nolint:errcheck // ignore error } }, - Output: io.Discard, + Stream: io.Discard, })) app.Get("/logging", func(ctx fiber.Ctx) error { return ctx.SendStatus(fiber.StatusOK) @@ -1054,7 +1195,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${pid}${reqHeaders}${referer}${scheme}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${reqHeader:test}${query:test}${form:test}${cookie:test}${non}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Hello, World!") @@ -1066,7 +1207,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${bytesReceived} ${bytesSent} ${status}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { c.Set("Connection", "keep-alive") @@ -1095,7 +1236,7 @@ func Benchmark_Logger_Parallel(b *testing.B) { app := fiber.New() app.Use(New(Config{ Format: "${resBody}", - Output: io.Discard, + Stream: io.Discard, })) app.Get("/", func(c fiber.Ctx) error { return c.SendString("Sample response body")