Skip to content

Commit e947e03

Browse files
edvardsantagaby
andauthored
🔥 feat(logger): Add predefined log formats (#3359)
* feat(logger): Add predefined log formats This commit introduces predefined log formats for the logger middleware, enhancing its flexibility and ease of use. Users can now specify formats like "common", "combined", and "json" in addition to the default format. Changes: - Added a `format.go` file to store predefined log format constants. - Updated `config.go` to include documentation for the `Format` configuration option, explaining the available placeholders and predefined formats. - Modified `logger.go` to utilize the predefined formats based on the `Format` configuration. - Added a new test case `Test_Logger_CLF` in `logger_test.go` to verify the "common" log format. * feat(logger): Use predefined formats and fix default format This commit updates the logger middleware to utilize the predefined log formats introduced in a previous commit. It also fixes the default format to use the `FormatDefault` constant. Changes: - Updated `config.go` to use `FormatDefault` constant for the default format. - Updated `default_logger.go` to use `FormatDefault` constant for the default format. - Added new test cases in `logger_test.go` to verify the "common", "combined" and "json" log formats. - Updated `format.go` to add newline character to the end of the default format. * feat(logger): Document and exemplify predefined formats * fix(logger): Improve test assertions based on golangci-lint * docs(logger): Improve documentation and formatting logger.md based on markdownlint-cli2 * docs(logger): Improve documentation based on markdownlint-cli2 * fix(logger): Improve combined and JSON format tests * feat(logger): Add ECS log format * feat(logger): Add CustomFormat option This commit introduces a `CustomFormat` option to the `Config` struct, allowing users to specify a predefined format (like "common", "combined", "json", or "ecs") * feat(logger): Add ECS log format to examples and config * docs(logger): Update examples in whats_new.md * feat(logger): Remove CustomFormat option and renamed Format consts - Removed `CustomFormat` field from `Config`. - Removed `LoggerConfig` map. - Rename predefined formats constants. * docs(logger): Update documentation and examples after format refactor --------- Co-authored-by: Juan Calderon-Perez <[email protected]>
1 parent f6ac929 commit e947e03

File tree

7 files changed

+220
-32
lines changed

7 files changed

+220
-32
lines changed

docs/middleware/logger.md

+54-24
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ app.Use(logger.New(logger.Config{
7979
TimeZone: "Asia/Shanghai",
8080
Done: func(c fiber.Ctx, logString []byte) {
8181
if c.Response().StatusCode() != fiber.StatusOK {
82-
reporter.SendToSlack(logString)
82+
reporter.SendToSlack(logString)
8383
}
8484
},
8585
}))
@@ -88,6 +88,23 @@ app.Use(logger.New(logger.Config{
8888
app.Use(logger.New(logger.Config{
8989
DisableColors: true,
9090
}))
91+
92+
// Use predefined formats
93+
app.Use(logger.New(logger.Config{
94+
Format: logger.FormatCommon,
95+
}))
96+
97+
app.Use(logger.New(logger.Config{
98+
Format: logger.FormatCombined,
99+
}))
100+
101+
app.Use(logger.New(logger.Config{
102+
Format: logger.FormatJSON,
103+
}))
104+
105+
app.Use(logger.New(logger.Config{
106+
Format: logger.FormatECS,
107+
}))
91108
```
92109

93110
### 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
136153

137154
### Config
138155

139-
| Property | Type | Description | Default |
140-
|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------|
141-
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
142-
| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` |
143-
| 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` |
144-
| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` |
145-
| Format | `string` | Format defines the logging tags. | `[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n` |
146-
| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` |
147-
| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` |
148-
| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` |
149-
| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` |
150-
| 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` |
151-
| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` |
156+
| Property | Type | Description | Default |
157+
| :------------ | :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- |
158+
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
159+
| Skip | `func(fiber.Ctx) bool` | Skip is a function to determine if logging is skipped or written to Stream. | `nil` |
160+
| 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` |
161+
| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` |
162+
| `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`) |
163+
| TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` |
164+
| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` |
165+
| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` |
166+
| Stream | `io.Writer` | Stream is a writer where logs are written. | `os.Stdout` |
167+
| 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` |
168+
| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` |
152169

153170
## Default Config
154171

155172
```go
156173
var ConfigDefault = Config{
157-
Next: nil,
158-
Skip nil,
159-
Done: nil,
160-
Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n",
161-
TimeFormat: "15:04:05",
162-
TimeZone: "Local",
163-
TimeInterval: 500 * time.Millisecond,
164-
Stream: os.Stdout,
165-
DisableColors: false,
166-
LoggerFunc: defaultLoggerInstance,
174+
Next: nil,
175+
Skip: nil,
176+
Done: nil,
177+
Format: DefaultFormat,
178+
TimeFormat: "15:04:05",
179+
TimeZone: "Local",
180+
TimeInterval: 500 * time.Millisecond,
181+
Stream: os.Stdout,
182+
BeforeHandlerFunc: beforeHandlerFunc,
183+
LoggerFunc: defaultLoggerInstance,
184+
enableColors: true,
167185
}
168186
```
169187

188+
## Predefined Formats
189+
190+
Logger provides predefined formats that you can use by name or directly by specifying the format string.
191+
192+
| **Format Constant** | **Format String** | **Description** |
193+
|---------------------|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
194+
| `DefaultFormat` | `"[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n"` | Fiber's default logger format. |
195+
| `CommonFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent}\n"` | Common Log Format (CLF) used in web server logs. |
196+
| `CombinedFormat` | `"${ip} - - [${time}] "${method} ${url} ${protocol}" ${status} ${bytesSent} "${referer}" "${ua}"\n"` | CLF format plus the `referer` and `user agent` fields. |
197+
| `JSONFormat` | `"{time: ${time}, ip: ${ip}, method: ${method}, url: ${url}, status: ${status}, bytesSent: ${bytesSent}}\n"` | JSON format for structured logging. |
198+
| `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. |
199+
170200
## Constants
171201

172202
```go

docs/whats_new.md

+16
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,22 @@ app.Use(logger.New(logger.Config{
937937

938938
</details>
939939

940+
#### Predefined Formats
941+
942+
Logger provides predefined formats that you can use by name or directly by specifying the format string.
943+
<details>
944+
945+
<summary>Example Usage</summary>
946+
947+
```go
948+
app.Use(logger.New(logger.Config{
949+
Format: logger.FormatCombined,
950+
}))
951+
```
952+
953+
See more in [Logger](./middleware/logger.md#predefined-formats)
954+
</details>
955+
940956
### Filesystem
941957

942958
We've decided to remove filesystem middleware to clear up the confusion between static and filesystem middleware.

middleware/logger/config.go

+17-6
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,23 @@ type Config struct {
5050

5151
timeZoneLocation *time.Location
5252

53-
// Format defines the logging tags
53+
// Format defines the logging format for the middleware.
5454
//
55-
// Optional. Default: [${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}
55+
// You can customize the log output by defining a format string with placeholders
56+
// such as: ${time}, ${ip}, ${status}, ${method}, ${path}, ${latency}, ${error}, etc.
57+
// The full list of available placeholders can be found in 'tags.go' or at
58+
// 'https://docs.gofiber.io/api/middleware/logger/#constants'.
59+
//
60+
// Fiber provides predefined logging formats that can be used directly:
61+
//
62+
// - DefaultFormat → Uses the default log format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}"
63+
// - CommonFormat → Uses the Apache Common Log Format (CLF): "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\n"
64+
// - CombinedFormat → Uses the Apache Combined Log Format: "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\n"
65+
// - JSONFormat → Uses the JSON log format: "{\"time\":\"${time}\",\"ip\":\"${ip}\",\"method\":\"${method}\",\"url\":\"${url}\",\"status\":${status},\"bytesSent\":${bytesSent}}\n"
66+
// - 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}\"}"
67+
// If both `Format` and `CustomFormat` are provided, the `CustomFormat` will be used, and the `Format` field will be ignored.
68+
// If no format is specified, the default format is used:
69+
// "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}"
5670
Format string
5771

5872
// TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html
@@ -105,7 +119,7 @@ var ConfigDefault = Config{
105119
Next: nil,
106120
Skip: nil,
107121
Done: nil,
108-
Format: defaultFormat,
122+
Format: DefaultFormat,
109123
TimeFormat: "15:04:05",
110124
TimeZone: "Local",
111125
TimeInterval: 500 * time.Millisecond,
@@ -115,9 +129,6 @@ var ConfigDefault = Config{
115129
enableColors: true,
116130
}
117131

118-
// default logging format for Fiber's default logger
119-
var defaultFormat = "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n"
120-
121132
// Helper function to set default values
122133
func configDefault(config ...Config) Config {
123134
// Return default config if nothing provided

middleware/logger/default_logger.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error {
2828
buf := bytebufferpool.Get()
2929

3030
// Default output when no custom Format or io.Writer is given
31-
if cfg.Format == defaultFormat {
31+
if cfg.Format == DefaultFormat {
3232
// Format error if exist
3333
formatErr := ""
3434
if cfg.enableColors {

middleware/logger/format.go

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package logger
2+
3+
const (
4+
// Fiber's default logger
5+
DefaultFormat = "[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n"
6+
// Apache Common Log Format (CLF)
7+
CommonFormat = "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent}\n"
8+
// Apache Combined Log Format
9+
CombinedFormat = "${ip} - - [${time}] \"${method} ${url} ${protocol}\" ${status} ${bytesSent} \"${referer}\" \"${ua}\"\n"
10+
// JSON log formats
11+
JSONFormat = "{\"time\":\"${time}\",\"ip\":\"${ip}\",\"method\":\"${method}\",\"url\":\"${url}\",\"status\":${status},\"bytesSent\":${bytesSent}}\n"
12+
// Elastic Common Schema (ECS) Log Format
13+
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"
14+
)

middleware/logger/logger.go

-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ func New(config ...Config) fiber.Handler {
4040
}
4141
}()
4242
}
43-
4443
// Set PID once
4544
pid := strconv.Itoa(os.Getpid())
4645

middleware/logger/logger_test.go

+118
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,124 @@ func Test_Logger_All(t *testing.T) {
467467
require.Equal(t, expected, buf.String())
468468
}
469469

470+
func Test_Logger_CLF_Format(t *testing.T) {
471+
t.Parallel()
472+
buf := bytebufferpool.Get()
473+
defer bytebufferpool.Put(buf)
474+
475+
app := fiber.New()
476+
477+
app.Use(New(Config{
478+
Format: CommonFormat,
479+
Stream: buf,
480+
}))
481+
482+
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil))
483+
require.NoError(t, err)
484+
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
485+
486+
expected := fmt.Sprintf("0.0.0.0 - - [%s] \"%s %s %s\" %d %d\n",
487+
time.Now().Format("15:04:05"),
488+
fiber.MethodGet, "/?foo=bar", "HTTP/1.1",
489+
fiber.StatusNotFound,
490+
0)
491+
logResponse := buf.String()
492+
require.Equal(t, expected, logResponse)
493+
}
494+
495+
func Test_Logger_Combined_CLF_Format(t *testing.T) {
496+
t.Parallel()
497+
buf := bytebufferpool.Get()
498+
defer bytebufferpool.Put(buf)
499+
500+
app := fiber.New()
501+
502+
app.Use(New(Config{
503+
Format: CombinedFormat,
504+
Stream: buf,
505+
}))
506+
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"
507+
const expectedReferer = "http://example.com"
508+
req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)
509+
req.Header.Set("Referer", expectedReferer)
510+
req.Header.Set("User-Agent", expectedUA)
511+
resp, err := app.Test(req)
512+
require.NoError(t, err)
513+
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
514+
515+
expected := fmt.Sprintf("0.0.0.0 - - [%s] %q %d %d %q %q\n",
516+
time.Now().Format("15:04:05"),
517+
fmt.Sprintf("%s %s %s", fiber.MethodGet, "/?foo=bar", "HTTP/1.1"),
518+
fiber.StatusNotFound,
519+
0,
520+
expectedReferer,
521+
expectedUA)
522+
logResponse := buf.String()
523+
require.Equal(t, expected, logResponse)
524+
}
525+
526+
func Test_Logger_Json_Format(t *testing.T) {
527+
t.Parallel()
528+
buf := bytebufferpool.Get()
529+
defer bytebufferpool.Put(buf)
530+
531+
app := fiber.New()
532+
533+
app.Use(New(Config{
534+
Format: JSONFormat,
535+
Stream: buf,
536+
}))
537+
538+
req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)
539+
resp, err := app.Test(req)
540+
require.NoError(t, err)
541+
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
542+
543+
expected := fmt.Sprintf(
544+
"{\"time\":%q,\"ip\":%q,\"method\":%q,\"url\":%q,\"status\":%d,\"bytesSent\":%d}\n",
545+
time.Now().Format("15:04:05"),
546+
"0.0.0.0",
547+
fiber.MethodGet,
548+
"/?foo=bar",
549+
fiber.StatusNotFound,
550+
0,
551+
)
552+
logResponse := buf.String()
553+
require.Equal(t, expected, logResponse)
554+
}
555+
556+
func Test_Logger_ECS_Format(t *testing.T) {
557+
t.Parallel()
558+
buf := bytebufferpool.Get()
559+
defer bytebufferpool.Put(buf)
560+
561+
app := fiber.New()
562+
563+
app.Use(New(Config{
564+
Format: ECSFormat,
565+
Stream: buf,
566+
}))
567+
568+
req := httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)
569+
resp, err := app.Test(req)
570+
require.NoError(t, err)
571+
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
572+
573+
expected := fmt.Sprintf(
574+
"{\"@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",
575+
time.Now().Format("15:04:05"),
576+
"0.0.0.0",
577+
fiber.MethodGet,
578+
"/?foo=bar",
579+
"HTTP/1.1",
580+
fiber.StatusNotFound,
581+
0,
582+
fmt.Sprintf("%s %s responded with %d", fiber.MethodGet, "/?foo=bar", fiber.StatusNotFound),
583+
)
584+
logResponse := buf.String()
585+
require.Equal(t, expected, logResponse)
586+
}
587+
470588
func getLatencyTimeUnits() []struct {
471589
unit string
472590
div time.Duration

0 commit comments

Comments
 (0)