1
0
mirror of https://github.com/gofiber/fiber.git synced 2025-04-27 13:14:31 +00:00
Edvard e947e03ed2
🔥 feat(logger): Add predefined log formats ()
* 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 <835733+gaby@users.noreply.github.com>
2025-03-21 16:13:21 +01:00

1365 lines
33 KiB
Go

//nolint:depguard // Because we test logging :D
package logger
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"runtime"
"strconv"
"sync"
"testing"
"time"
"github.com/gofiber/fiber/v3"
fiberlog "github.com/gofiber/fiber/v3/log"
"github.com/gofiber/fiber/v3/middleware/requestid"
"github.com/stretchr/testify/require"
"github.com/valyala/bytebufferpool"
"github.com/valyala/fasthttp"
)
func benchmarkSetup(b *testing.B, app *fiber.App, uri string) {
b.Helper()
h := app.Handler()
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(fiber.MethodGet)
fctx.Request.SetRequestURI(uri)
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
h(fctx)
}
}
func benchmarkSetupParallel(b *testing.B, app *fiber.App, path string) {
b.Helper()
handler := app.Handler()
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
fctx := &fasthttp.RequestCtx{}
fctx.Request.Header.SetMethod(fiber.MethodGet)
fctx.Request.SetRequestURI(path)
for pb.Next() {
handler(fctx)
}
})
}
// go test -run Test_Logger
func Test_Logger(t *testing.T) {
t.Parallel()
app := fiber.New()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app.Use(New(Config{
Format: "${error}",
Stream: buf,
}))
app.Get("/", func(_ fiber.Ctx) error {
return errors.New("some random error")
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)
require.Equal(t, "some random error", buf.String())
}
// go test -run Test_Logger_locals
func Test_Logger_locals(t *testing.T) {
t.Parallel()
app := fiber.New()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app.Use(New(Config{
Format: "${locals:demo}",
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Locals("demo", "johndoe")
return c.SendStatus(fiber.StatusOK)
})
app.Get("/int", func(c fiber.Ctx) error {
c.Locals("demo", 55)
return c.SendStatus(fiber.StatusOK)
})
app.Get("/empty", 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)
require.Equal(t, "johndoe", buf.String())
buf.Reset()
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/int", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, "55", buf.String())
buf.Reset()
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/empty", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, "", buf.String())
}
// go test -run Test_Logger_Next
func Test_Logger_Next(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Use(New(Config{
Next: func(_ fiber.Ctx) bool {
return true
},
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
}
// go test -run Test_Logger_Done
func Test_Logger_Done(t *testing.T) {
t.Parallel()
buf := bytes.NewBuffer(nil)
app := fiber.New()
app.Use(New(Config{
Done: func(c fiber.Ctx, logString []byte) {
if c.Response().StatusCode() == fiber.StatusOK {
_, err := buf.Write(logString)
require.NoError(t, err)
}
},
})).Get("/logging", func(ctx fiber.Ctx) error {
return ctx.SendStatus(fiber.StatusOK)
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/logging", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
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()
app := fiber.New()
app.Use(New(Config{
TimeZone: "invalid",
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
}
// go test -run Test_Logger_Fiber_Logger
func Test_Logger_LoggerToWriter(t *testing.T) {
app := fiber.New()
buf := bytebufferpool.Get()
t.Cleanup(func() {
bytebufferpool.Put(buf)
})
logger := fiberlog.DefaultLogger()
stdlogger, ok := logger.Logger().(*log.Logger)
require.True(t, ok)
stdlogger.SetFlags(0)
logger.SetOutput(buf)
testCases := []struct {
levelStr string
level fiberlog.Level
}{
{
level: fiberlog.LevelTrace,
levelStr: "Trace",
},
{
level: fiberlog.LevelDebug,
levelStr: "Debug",
},
{
level: fiberlog.LevelInfo,
levelStr: "Info",
},
{
level: fiberlog.LevelWarn,
levelStr: "Warn",
},
{
level: fiberlog.LevelError,
levelStr: "Error",
},
}
for _, tc := range testCases {
level := strconv.Itoa(int(tc.level))
t.Run(level, func(t *testing.T) {
buf.Reset()
app.Use("/"+level, New(Config{
Format: "${error}",
Stream: LoggerToWriter(logger, tc.
level),
}))
app.Get("/"+level, func(_ fiber.Ctx) error {
return errors.New("some random error")
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/"+level, nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode)
require.Equal(t, "["+tc.levelStr+"] some random error\n", buf.String())
})
require.Panics(t, func() {
LoggerToWriter(logger, fiberlog.LevelPanic)
})
require.Panics(t, func() {
LoggerToWriter(logger, fiberlog.LevelFatal)
})
require.Panics(t, func() {
LoggerToWriter(nil, fiberlog.LevelFatal)
})
}
}
type fakeErrorOutput int
func (o *fakeErrorOutput) Write([]byte) (int, error) {
*o++
return 0, errors.New("fake output")
}
// go test -run Test_Logger_ErrorOutput_WithoutColor
func Test_Logger_ErrorOutput_WithoutColor(t *testing.T) {
t.Parallel()
o := new(fakeErrorOutput)
app := fiber.New()
app.Use(New(Config{
Stream: o,
DisableColors: true,
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
require.EqualValues(t, 2, *o)
}
// go test -run Test_Logger_ErrorOutput
func Test_Logger_ErrorOutput(t *testing.T) {
t.Parallel()
o := new(fakeErrorOutput)
app := fiber.New()
app.Use(New(Config{
Stream: o,
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
require.EqualValues(t, 2, *o)
}
// go test -run Test_Logger_All
func Test_Logger_All(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
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}",
Stream: buf,
}))
// Alias colors
colors := app.Config().ColorScheme
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("%dHost=example.comhttpHTTP/1.10.0.0.0example.com/?foo=bar/%s%s%s%s%s%s%s%s%sCannot GET /", os.Getpid(), colors.Black, colors.Red, colors.Green, colors.Yellow, colors.Blue, colors.Magenta, colors.Cyan, colors.White, colors.Reset)
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
} {
// windows does not support µs sleep precision
// https://github.com/golang/go/issues/29485
if runtime.GOOS == "windows" {
return []struct {
unit string
div time.Duration
}{
{unit: "ms", div: time.Millisecond},
{unit: "s", div: time.Second},
}
}
return []struct {
unit string
div time.Duration
}{
{unit: "µs", div: time.Microsecond},
{unit: "ms", div: time.Millisecond},
{unit: "s", div: time.Second},
}
}
// go test -run Test_Logger_WithLatency
func Test_Logger_WithLatency(t *testing.T) {
buff := bytebufferpool.Get()
defer bytebufferpool.Put(buff)
app := fiber.New()
logger := New(Config{
Stream: buff,
Format: "${latency}",
})
app.Use(logger)
// Define a list of time units to test
timeUnits := getLatencyTimeUnits()
// Initialize a new time unit
sleepDuration := 1 * time.Nanosecond
// Define a test route that sleeps
app.Get("/test", func(c fiber.Ctx) error {
time.Sleep(sleepDuration)
return c.SendStatus(fiber.StatusOK)
})
// Loop through each time unit and assert that the log output contains the expected latency value
for _, tu := range timeUnits {
// Update the sleep duration for the next iteration
sleepDuration = 1 * tu.div
// Create a new HTTP request to the test route
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{
Timeout: 3 * time.Second,
FailOnTimeout: true,
})
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
// Assert that the log output contains the expected latency value in the current time unit
require.True(t, bytes.HasSuffix(buff.Bytes(), []byte(tu.unit)), "Expected latency to be in %s, got %s", tu.unit, buff.String())
// Reset the buffer
buff.Reset()
}
}
// go test -run Test_Logger_WithLatency_DefaultFormat
func Test_Logger_WithLatency_DefaultFormat(t *testing.T) {
buff := bytebufferpool.Get()
defer bytebufferpool.Put(buff)
app := fiber.New()
logger := New(Config{
Stream: buff,
})
app.Use(logger)
// Define a list of time units to test
timeUnits := getLatencyTimeUnits()
// Initialize a new time unit
sleepDuration := 1 * time.Nanosecond
// Define a test route that sleeps
app.Get("/test", func(c fiber.Ctx) error {
time.Sleep(sleepDuration)
return c.SendStatus(fiber.StatusOK)
})
// Loop through each time unit and assert that the log output contains the expected latency value
for _, tu := range timeUnits {
// Update the sleep duration for the next iteration
sleepDuration = 1 * tu.div
// Create a new HTTP request to the test route
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), fiber.TestConfig{
Timeout: 2 * time.Second,
FailOnTimeout: true,
})
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
// Assert that the log output contains the expected latency value in the current time unit
// parse out the latency value from the log output
latency := bytes.Split(buff.Bytes(), []byte(" | "))[2]
// Assert that the latency value is in the current time unit
require.True(t, bytes.HasSuffix(latency, []byte(tu.unit)), "Expected latency to be in %s, got %s", tu.unit, latency)
// Reset the buffer
buff.Reset()
}
}
// go test -run Test_Query_Params
func Test_Query_Params(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: "${queryParams}",
Stream: buf,
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar&baz=moz", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
expected := "foo=bar&baz=moz"
require.Equal(t, expected, buf.String())
}
// go test -run Test_Response_Body
func Test_Response_Body(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: "${resBody}",
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Sample response body")
})
app.Post("/test", func(c fiber.Ctx) error {
return c.Send([]byte("Post in test"))
})
_, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
expectedGetResponse := "Sample response body"
require.Equal(t, expectedGetResponse, buf.String())
buf.Reset() // Reset buffer to test POST
_, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/test", nil))
expectedPostResponse := "Post in test"
require.NoError(t, err)
require.Equal(t, expectedPostResponse, buf.String())
}
// go test -run Test_Request_Body
func Test_Request_Body(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Stream: buf,
}))
app.Post("/", func(c fiber.Ctx) error {
c.Response().Header.SetContentLength(5)
return c.SendString("World")
})
// Create a POST request with a body
body := []byte("Hello")
req := httptest.NewRequest(fiber.MethodPost, "/", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/octet-stream")
_, err := app.Test(req)
require.NoError(t, err)
require.Equal(t, "5 5 200", buf.String())
}
// go test -run Test_Logger_AppendUint
func Test_Logger_AppendUint(t *testing.T) {
t.Parallel()
app := fiber.New()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("hello")
})
app.Get("/content", func(c fiber.Ctx) error {
c.Response().Header.SetContentLength(5)
return c.SendString("hello")
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, "-2 0 200", buf.String())
buf.Reset()
resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/content", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, "-2 5 200", buf.String())
}
// go test -run Test_Logger_Data_Race -race
func Test_Logger_Data_Race(t *testing.T) {
t.Parallel()
app := fiber.New()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app.Use(New(ConfigDefault))
app.Use(New(Config{
Format: "${time} | ${pid} | ${locals:requestid} | ${status} | ${latency} | ${method} | ${path}\n",
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("hello")
})
var (
resp1, resp2 *http.Response
err1, err2 error
)
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
resp1, err1 = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
wg.Done()
}()
resp2, err2 = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
wg.Wait()
require.NoError(t, err1)
require.Equal(t, fiber.StatusOK, resp1.StatusCode)
require.NoError(t, err2)
require.Equal(t, fiber.StatusOK, resp2.StatusCode)
}
// go test -run Test_Response_Header
func Test_Response_Header(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(requestid.New(requestid.Config{
Next: nil,
Header: fiber.HeaderXRequestID,
Generator: func() string { return "Hello fiber!" },
}))
app.Use(New(Config{
Format: "${respHeader:X-Request-ID}",
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!")
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, "Hello fiber!", buf.String())
}
// go test -run Test_Req_Header
func Test_Req_Header(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: "${reqHeader:test}",
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!")
})
headerReq := httptest.NewRequest(fiber.MethodGet, "/", nil)
headerReq.Header.Add("test", "Hello fiber!")
resp, err := app.Test(headerReq)
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, "Hello fiber!", buf.String())
}
// go test -run Test_ReqHeader_Header
func Test_ReqHeader_Header(t *testing.T) {
t.Parallel()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: "${reqHeader:test}",
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!")
})
reqHeaderReq := httptest.NewRequest(fiber.MethodGet, "/", nil)
reqHeaderReq.Header.Add("test", "Hello fiber!")
resp, err := app.Test(reqHeaderReq)
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, "Hello fiber!", buf.String())
}
// go test -run Test_CustomTags
func Test_CustomTags(t *testing.T) {
t.Parallel()
customTag := "it is a custom tag"
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app := fiber.New()
app.Use(New(Config{
Format: "${custom_tag}",
CustomTags: map[string]LogFunc{
"custom_tag": func(output Buffer, _ fiber.Ctx, _ *Data, _ string) (int, error) {
return output.WriteString(customTag)
},
},
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello fiber!")
})
reqHeaderReq := httptest.NewRequest(fiber.MethodGet, "/", nil)
reqHeaderReq.Header.Add("test", "Hello fiber!")
resp, err := app.Test(reqHeaderReq)
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
require.Equal(t, customTag, buf.String())
}
// go test -run Test_Logger_ByteSent_Streaming
func Test_Logger_ByteSent_Streaming(t *testing.T) {
t.Parallel()
app := fiber.New()
buf := bytebufferpool.Get()
defer bytebufferpool.Put(buf)
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Stream: buf,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("Connection", "keep-alive")
c.Set("Transfer-Encoding", "chunked")
c.RequestCtx().SetBodyStreamWriter(func(w *bufio.Writer) {
var i int
for {
i++
msg := fmt.Sprintf("%d - the time is %v", i, time.Now())
fmt.Fprintf(w, "data: Message: %s\n\n", msg)
err := w.Flush()
if err != nil {
break
}
if i == 10 {
break
}
}
})
return nil
})
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
// -2 means identity, -1 means chunked, 200 status
require.Equal(t, "-2 -1 200", buf.String())
}
type fakeOutput int
func (o *fakeOutput) Write(b []byte) (int, error) {
*o++
return len(b), nil
}
// go test -run Test_Logger_EnableColors
func Test_Logger_EnableColors(t *testing.T) {
t.Parallel()
o := new(fakeOutput)
app := fiber.New()
app.Use(New(Config{
Stream: o,
}))
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusNotFound, resp.StatusCode)
require.EqualValues(t, 1, *o)
}
// go test -v -run=^$ -bench=Benchmark_Logger$ -benchmem -count=4
func Benchmark_Logger(b *testing.B) {
b.Run("NoMiddleware", func(bb *testing.B) {
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetup(bb, app, "/")
})
b.Run("WithBytesAndStatus", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test")
return c.SendString("Hello, World!")
})
benchmarkSetup(bb, app, "/")
})
b.Run("DefaultFormat", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetup(bb, app, "/")
})
b.Run("DefaultFormatDisableColors", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Stream: io.Discard,
DisableColors: true,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetup(bb, app, "/")
})
b.Run("DefaultFormatWithFiberLog", func(bb *testing.B) {
app := fiber.New()
logger := fiberlog.DefaultLogger()
logger.SetOutput(io.Discard)
app.Use(New(Config{
Stream: LoggerToWriter(logger, fiberlog.LevelDebug),
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetup(bb, app, "/")
})
b.Run("WithTagParameter", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test")
return c.SendString("Hello, World!")
})
benchmarkSetup(bb, app, "/")
})
b.Run("WithLocals", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${locals:demo}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Locals("demo", "johndoe")
return c.SendStatus(fiber.StatusOK)
})
benchmarkSetup(bb, app, "/")
})
b.Run("WithLocalsInt", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${locals:demo}",
Stream: io.Discard,
}))
app.Get("/int", func(c fiber.Ctx) error {
c.Locals("demo", 55)
return c.SendStatus(fiber.StatusOK)
})
benchmarkSetup(bb, app, "/int")
})
b.Run("WithCustomDone", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Done: func(c fiber.Ctx, logString []byte) {
if c.Response().StatusCode() == fiber.StatusOK {
io.Discard.Write(logString) //nolint:errcheck // ignore error
}
},
Stream: io.Discard,
}))
app.Get("/logging", func(ctx fiber.Ctx) error {
return ctx.SendStatus(fiber.StatusOK)
})
benchmarkSetup(bb, app, "/logging")
})
b.Run("WithAllTags", func(bb *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}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetup(bb, app, "/")
})
b.Run("Streaming", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("Connection", "keep-alive")
c.Set("Transfer-Encoding", "chunked")
c.RequestCtx().SetBodyStreamWriter(func(w *bufio.Writer) {
var i int
for {
i++
msg := fmt.Sprintf("%d - the time is %v", i, time.Now())
fmt.Fprintf(w, "data: Message: %s\n\n", msg)
err := w.Flush()
if err != nil {
break
}
if i == 10 {
break
}
}
})
return nil
})
benchmarkSetup(bb, app, "/")
})
b.Run("WithBody", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${resBody}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Sample response body")
})
benchmarkSetup(bb, app, "/")
})
}
// go test -v -run=^$ -bench=Benchmark_Logger_Parallel$ -benchmem -count=4
func Benchmark_Logger_Parallel(b *testing.B) {
b.Run("NoMiddleware", func(bb *testing.B) {
app := fiber.New()
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetupParallel(bb, app, "/")
})
b.Run("WithBytesAndStatus", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test")
return c.SendString("Hello, World!")
})
benchmarkSetupParallel(bb, app, "/")
})
b.Run("DefaultFormat", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetupParallel(bb, app, "/")
})
b.Run("DefaultFormatWithFiberLog", func(bb *testing.B) {
app := fiber.New()
logger := fiberlog.DefaultLogger()
logger.SetOutput(io.Discard)
app.Use(New(Config{
Stream: LoggerToWriter(logger, fiberlog.LevelDebug),
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetupParallel(bb, app, "/")
})
b.Run("DefaultFormatDisableColors", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Stream: io.Discard,
DisableColors: true,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetupParallel(bb, app, "/")
})
b.Run("WithTagParameter", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("test", "test")
return c.SendString("Hello, World!")
})
benchmarkSetupParallel(bb, app, "/")
})
b.Run("WithLocals", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${locals:demo}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Locals("demo", "johndoe")
return c.SendStatus(fiber.StatusOK)
})
benchmarkSetupParallel(bb, app, "/")
})
b.Run("WithLocalsInt", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${locals:demo}",
Stream: io.Discard,
}))
app.Get("/int", func(c fiber.Ctx) error {
c.Locals("demo", 55)
return c.SendStatus(fiber.StatusOK)
})
benchmarkSetupParallel(bb, app, "/int")
})
b.Run("WithCustomDone", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Done: func(c fiber.Ctx, logString []byte) {
if c.Response().StatusCode() == fiber.StatusOK {
io.Discard.Write(logString) //nolint:errcheck // ignore error
}
},
Stream: io.Discard,
}))
app.Get("/logging", func(ctx fiber.Ctx) error {
return ctx.SendStatus(fiber.StatusOK)
})
benchmarkSetupParallel(bb, app, "/logging")
})
b.Run("WithAllTags", func(bb *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}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Hello, World!")
})
benchmarkSetupParallel(bb, app, "/")
})
b.Run("Streaming", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${bytesReceived} ${bytesSent} ${status}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
c.Set("Connection", "keep-alive")
c.Set("Transfer-Encoding", "chunked")
c.RequestCtx().SetBodyStreamWriter(func(w *bufio.Writer) {
var i int
for {
i++
msg := fmt.Sprintf("%d - the time is %v", i, time.Now())
fmt.Fprintf(w, "data: Message: %s\n\n", msg)
err := w.Flush()
if err != nil {
break
}
if i == 10 {
break
}
}
})
return nil
})
benchmarkSetupParallel(bb, app, "/")
})
b.Run("WithBody", func(bb *testing.B) {
app := fiber.New()
app.Use(New(Config{
Format: "${resBody}",
Stream: io.Discard,
}))
app.Get("/", func(c fiber.Ctx) error {
return c.SendString("Sample response body")
})
benchmarkSetupParallel(bb, app, "/")
})
}