diff --git a/client/request.go b/client/request.go index 80a03b09..daa7e60b 100644 --- a/client/request.go +++ b/client/request.go @@ -298,11 +298,12 @@ func (r *Request) Cookie(key string) string { // Use maps.Collect() to gather them into a map if needed. func (r *Request) Cookies() iter.Seq2[string, string] { return func(yield func(string, string) bool) { - r.cookies.VisitAll(func(key, val string) { - if !yield(key, val) { + for k, v := range *r.cookies { + res := yield(k, v) + if !res { return } - }) + } } } @@ -343,11 +344,11 @@ func (r *Request) PathParam(key string) string { // Use maps.Collect() to gather them into a map if needed. func (r *Request) PathParams() iter.Seq2[string, string] { return func(yield func(string, string) bool) { - r.path.VisitAll(func(key, val string) { - if !yield(key, val) { + for k, v := range *r.path { + if !yield(k, v) { return } - }) + } } } diff --git a/client/request_test.go b/client/request_test.go index c13dbbd8..73bae29d 100644 --- a/client/request_test.go +++ b/client/request_test.go @@ -1,3 +1,4 @@ +//nolint:goconst // Much easier to just ignore memory leaks in tests package client import ( @@ -451,6 +452,14 @@ func Test_Request_Cookies(t *testing.T) { require.Equal(t, "bar", cookies["foo"]) require.Equal(t, "foo", cookies["bar"]) + require.NotPanics(t, func() { + for _, v := range req.Cookies() { + if v == "bar" { + break + } + } + }) + require.Len(t, cookies, 2) } @@ -564,6 +573,14 @@ func Test_Request_PathParams(t *testing.T) { require.Equal(t, "foo", pathParams["bar"]) require.Len(t, pathParams, 2) + + require.NotPanics(t, func() { + for _, v := range req.PathParams() { + if v == "bar" { + break + } + } + }) } func Benchmark_Request_PathParams(b *testing.B) { @@ -1579,7 +1596,7 @@ func Test_SetValWithStruct(t *testing.T) { require.True(t, func() bool { for _, v := range p.PeekMulti("TSlice") { - if string(v) == "bar" { //nolint:goconst // test + if string(v) == "bar" { return true } } diff --git a/docs/middleware/logger.md b/docs/middleware/logger.md index af16f384..c4c60cbc 100644 --- a/docs/middleware/logger.md +++ b/docs/middleware/logger.md @@ -79,7 +79,7 @@ app.Use(logger.New(logger.Config{ TimeZone: "Asia/Shanghai", Done: func(c fiber.Ctx, logString []byte) { if c.Response().StatusCode() != fiber.StatusOK { - reporter.SendToSlack(logString) + reporter.SendToSlack(logString) } }, })) @@ -88,6 +88,23 @@ app.Use(logger.New(logger.Config{ app.Use(logger.New(logger.Config{ DisableColors: true, })) + +// Use predefined formats +app.Use(logger.New(logger.Config{ + Format: logger.FormatCommon, +})) + +app.Use(logger.New(logger.Config{ + Format: logger.FormatCombined, +})) + +app.Use(logger.New(logger.Config{ + Format: logger.FormatJSON, +})) + +app.Use(logger.New(logger.Config{ + Format: logger.FormatECS, +})) ``` ### Use Logger Middleware with Other Loggers @@ -136,37 +153,50 @@ Writing to os.File is goroutine-safe, but if you are using a custom Stream that ### Config -| Property | Type | Description | Default | -|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------| -| 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` | -| 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` | +| Property | Type | Description | Default | +| :------------ | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- | +| 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` | 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"` | +| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | +| 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` | ## 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, - Stream: os.Stdout, - DisableColors: false, - LoggerFunc: defaultLoggerInstance, + Next: nil, + Skip: nil, + Done: nil, + Format: DefaultFormat, + TimeFormat: "15:04:05", + TimeZone: "Local", + TimeInterval: 500 * time.Millisecond, + Stream: os.Stdout, + BeforeHandlerFunc: beforeHandlerFunc, + LoggerFunc: defaultLoggerInstance, + enableColors: true, } ``` +## Predefined Formats + +Logger provides predefined formats that you can use by name or directly by specifying the format string. + +| **Format Constant** | **Format String** | **Description** | +|---------------------|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------| +| `DefaultFormat` | `"[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n"` | Fiber's default logger format. | +| `CommonFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent}\n"` | Common Log Format (CLF) used in web server logs. | +| `CombinedFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent} "${referer}" "${ua}"\n"` | CLF format plus the `referer` and `user agent` fields. | +| `JSONFormat` | `"{time: ${time}, ip: ${ip}, method: ${method}, url: ${url}, status: ${status}, bytesSent: ${bytesSent}}\n"` | JSON format for structured logging. | +| `ECSFormat` | `"{\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}\n"` | Elastic Common Schema (ECS) format for structured logging. | + ## Constants ```go diff --git a/docs/whats_new.md b/docs/whats_new.md index 8528dc41..bc569d3e 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -937,6 +937,22 @@ app.Use(logger.New(logger.Config{ +#### Predefined Formats + +Logger provides predefined formats that you can use by name or directly by specifying the format string. +
+ +Example Usage + +```go +app.Use(logger.New(logger.Config{ + Format: logger.FormatCombined, +})) +``` + +See more in [Logger](./middleware/logger.md#predefined-formats) +
+ ### Filesystem We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware. diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go index 090082f4..7f586cd9 100644 --- a/middleware/csrf/csrf_test.go +++ b/middleware/csrf/csrf_test.go @@ -1331,56 +1331,65 @@ func Test_CSRF_Cookie_Injection_Exploit(t *testing.T) { } // TODO: use this test case and make the unsafe header value bug from https://github.com/gofiber/fiber/issues/2045 reproducible and permanently fixed/tested by this testcase -// func Test_CSRF_UnsafeHeaderValue(t *testing.T) { -// t.Parallel() -// app := fiber.New() +func Test_CSRF_UnsafeHeaderValue(t *testing.T) { + t.SkipNow() + t.Parallel() + app := fiber.New() -// app.Use(New()) -// app.Get("/", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) -// app.Get("/test", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) -// app.Post("/", func(c fiber.Ctx) error { -// return c.SendStatus(fiber.StatusOK) -// }) + app.Use(New()) + app.Get("/", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + app.Get("/test", func(c fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + app.Post("/", 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) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// var token string -// for _, c := range resp.Cookies() { -// if c.Name != ConfigDefault.CookieName { -// continue -// } -// token = c.Value -// break -// } + var token string + for _, c := range resp.Cookies() { + if c.Name != ConfigDefault.CookieName { + continue + } + token = c.Value + break + } -// fmt.Println("token", token) + t.Log("token", token) -// getReq := httptest.NewRequest(fiber.MethodGet, "/", nil) -// getReq.Header.Set(HeaderName, token) -// resp, err = app.Test(getReq) + getReq := httptest.NewRequest(fiber.MethodGet, "/", nil) + getReq.Header.Set(HeaderName, token) + resp, err = app.Test(getReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// getReq = httptest.NewRequest(fiber.MethodGet, "/test", nil) -// getReq.Header.Set("X-Requested-With", "XMLHttpRequest") -// getReq.Header.Set(fiber.HeaderCacheControl, "no") -// getReq.Header.Set(HeaderName, token) + getReq = httptest.NewRequest(fiber.MethodGet, "/test", nil) + getReq.Header.Set("X-Requested-With", "XMLHttpRequest") + getReq.Header.Set(fiber.HeaderCacheControl, "no") + getReq.Header.Set(HeaderName, token) -// resp, err = app.Test(getReq) + resp, err = app.Test(getReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// getReq.Header.Set(fiber.HeaderAccept, "*/*") -// getReq.Header.Del(HeaderName) -// resp, err = app.Test(getReq) + getReq.Header.Set(fiber.HeaderAccept, "*/*") + getReq.Header.Del(HeaderName) + resp, err = app.Test(getReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) -// postReq := httptest.NewRequest(fiber.MethodPost, "/", nil) -// postReq.Header.Set("X-Requested-With", "XMLHttpRequest") -// postReq.Header.Set(HeaderName, token) -// resp, err = app.Test(postReq) -// } + postReq := httptest.NewRequest(fiber.MethodPost, "/", nil) + postReq.Header.Set("X-Requested-With", "XMLHttpRequest") + postReq.Header.Set(HeaderName, token) + resp, err = app.Test(postReq) + require.NoError(t, err) + require.Equal(t, fiber.StatusOK, resp.StatusCode) +} // go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_Check -benchmem -count=4 func Benchmark_Middleware_CSRF_Check(b *testing.B) { diff --git a/middleware/logger/config.go b/middleware/logger/config.go index 2df814eb..d543acfa 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -50,9 +50,23 @@ type Config struct { timeZoneLocation *time.Location - // Format defines the logging tags + // Format defines the logging format for the middleware. // - // Optional. Default: [${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error} + // You can customize the log output by defining a format string with placeholders + // such as: ${time}, ${ip}, ${status}, ${method}, ${path}, ${latency}, ${error}, etc. + // The full list of available placeholders can be found in 'tags.go' or at + // 'https://docs.gofiber.io/api/middleware/logger/#constants'. + // + // Fiber provides predefined logging formats that can be used directly: + // + // - DefaultFormat → Uses the default log format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}" + // - CommonFormat → Uses the Apache Common Log Format (CLF): "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\n" + // - CombinedFormat → Uses the Apache Combined Log Format: "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\n" + // - JSONFormat → Uses the JSON log format: "{\"time\":\"${time}\",\"ip\":\"${ip}\",\"method\":\"${method}\",\"url\":\"${url}\",\"status\":${status},\"bytesSent\":${bytesSent}}\n" + // - ECSFormat → Uses the Elastic Common Schema (ECS) log format: {\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}" + // If both `Format` and `CustomFormat` are provided, the `CustomFormat` will be used, and the `Format` field will be ignored. + // If no format is specified, the default format is used: + // "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}" Format string // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html @@ -105,7 +119,7 @@ var ConfigDefault = Config{ Next: nil, Skip: nil, Done: nil, - Format: defaultFormat, + Format: DefaultFormat, TimeFormat: "15:04:05", TimeZone: "Local", TimeInterval: 500 * time.Millisecond, @@ -115,9 +129,6 @@ var ConfigDefault = Config{ enableColors: true, } -// default logging format for Fiber's default logger -var defaultFormat = "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n" - // Helper function to set default values func configDefault(config ...Config) Config { // Return default config if nothing provided diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index c70a3e0e..a2cbfa1f 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -28,7 +28,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { buf := bytebufferpool.Get() // Default output when no custom Format or io.Writer is given - if cfg.Format == defaultFormat { + if cfg.Format == DefaultFormat { // Format error if exist formatErr := "" if cfg.enableColors { diff --git a/middleware/logger/format.go b/middleware/logger/format.go new file mode 100644 index 00000000..901c2409 --- /dev/null +++ b/middleware/logger/format.go @@ -0,0 +1,14 @@ +package logger + +const ( + // Fiber's default logger + DefaultFormat = "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n" + // Apache Common Log Format (CLF) + CommonFormat = "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\n" + // Apache Combined Log Format + CombinedFormat = "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\n" + // JSON log formats + JSONFormat = "{\"time\":\"${time}\",\"ip\":\"${ip}\",\"method\":\"${method}\",\"url\":\"${url}\",\"status\":${status},\"bytesSent\":${bytesSent}}\n" + // Elastic Common Schema (ECS) Log Format + ECSFormat = "{\"@timestamp\":\"${time}\",\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":\"${ip}\"},\"http\":{\"request\":{\"method\":\"${method}\",\"url\":\"${url}\",\"protocol\":\"${protocol}\"},\"response\":{\"status_code\":${status},\"body\":{\"bytes\":${bytesSent}}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":\"${method} ${url} responded with ${status}\"}\n" +) diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index 793c16c2..7d4befc9 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -40,7 +40,6 @@ func New(config ...Config) fiber.Handler { } }() } - // Set PID once pid := strconv.Itoa(os.Getpid()) diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index eb1b6a44..edce174c 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -467,6 +467,124 @@ func Test_Logger_All(t *testing.T) { require.Equal(t, expected, buf.String()) } +func Test_Logger_CLF_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: CommonFormat, + Stream: buf, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf("0.0.0.0 - - [%s] \"%s %s %s\" %d %d\n", + time.Now().Format("15:04:05"), + fiber.MethodGet, "/?foo=bar", "HTTP/1.1", + fiber.StatusNotFound, + 0) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + +func Test_Logger_Combined_CLF_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: CombinedFormat, + Stream: buf, + })) + const expectedUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36" + const expectedReferer = "http://example.com" + req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil) + req.Header.Set("Referer", expectedReferer) + req.Header.Set("User-Agent", expectedUA) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf("0.0.0.0 - - [%s] %q %d %d %q %q\n", + time.Now().Format("15:04:05"), + fmt.Sprintf("%s %s %s", fiber.MethodGet, "/?foo=bar", "HTTP/1.1"), + fiber.StatusNotFound, + 0, + expectedReferer, + expectedUA) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + +func Test_Logger_Json_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: JSONFormat, + Stream: buf, + })) + + req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf( + "{\"time\":%q,\"ip\":%q,\"method\":%q,\"url\":%q,\"status\":%d,\"bytesSent\":%d}\n", + time.Now().Format("15:04:05"), + "0.0.0.0", + fiber.MethodGet, + "/?foo=bar", + fiber.StatusNotFound, + 0, + ) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + +func Test_Logger_ECS_Format(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + + app.Use(New(Config{ + Format: ECSFormat, + Stream: buf, + })) + + req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil) + resp, err := app.Test(req) + require.NoError(t, err) + require.Equal(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf( + "{\"@timestamp\":%q,\"ecs\":{\"version\":\"1.6.0\"},\"client\":{\"ip\":%q},\"http\":{\"request\":{\"method\":%q,\"url\":%q,\"protocol\":%q},\"response\":{\"status_code\":%d,\"body\":{\"bytes\":%d}}},\"log\":{\"level\":\"INFO\",\"logger\":\"fiber\"},\"message\":%q}\n", + time.Now().Format("15:04:05"), + "0.0.0.0", + fiber.MethodGet, + "/?foo=bar", + "HTTP/1.1", + fiber.StatusNotFound, + 0, + fmt.Sprintf("%s %s responded with %d", fiber.MethodGet, "/?foo=bar", fiber.StatusNotFound), + ) + logResponse := buf.String() + require.Equal(t, expected, logResponse) +} + func getLatencyTimeUnits() []struct { unit string div time.Duration