Back to Fiber

Logger

docs/middleware/logger.md

3.5.020.4 KB
Original Source

Logger

Logger middleware for Fiber that logs HTTP requests and responses.

Signatures

go
func New(config ...Config) fiber.Handler
func RegisterTag(tag string, fn LogFunc) error
func MustRegisterTag(tag string, fn LogFunc)

Examples

Import the package:

go
import (
    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/middleware/logger"
    "github.com/gofiber/fiber/v3/middleware/requestid"
)

:::tip Registration order matters: only routes added after the logger are logged, so register it early. :::

Once your Fiber app is initialized, use the middleware like this:

go
// Initialize default config
app.Use(logger.New())

// Or extend your config for customization
// Log remote IP and port
app.Use(logger.New(logger.Config{
    Format: "[${ip}]:${port} ${status} - ${method} ${path}\n",
}))

// Logging Request ID
app.Use(requestid.New()) // Ensure requestid middleware is used before the logger
app.Use(logger.New(logger.Config{
    // requestid.New() registers ${requestid} automatically.
    Format: "${pid} ${requestid} ${status} - ${method} ${path}\n",
}))

// Changing TimeZone & TimeFormat
app.Use(logger.New(logger.Config{
    Format:     "${pid} ${status} - ${method} ${path}\n",
    TimeFormat: "02-Jan-2006",
    TimeZone:   "America/New_York",
}))

// Custom File Writer
accessLog, err := os.OpenFile("./access.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
    log.Fatalf("error opening access.log file: %v", err)
}
defer accessLog.Close()
app.Use(logger.New(logger.Config{
    Stream: accessLog,
}))

// Add Custom Tags
app.Use(logger.New(logger.Config{
    CustomTags: map[string]logger.LogFunc{
        "custom_tag": func(output logger.Buffer, c fiber.Ctx, data *logger.Data, extraParam string) (int, error) {
            return output.WriteString("it is a custom tag")
        },
    },
}))

// Callback after log is written
app.Use(logger.New(logger.Config{
    TimeFormat: time.RFC3339Nano,
    TimeZone:   "Asia/Shanghai",
    Done: func(c fiber.Ctx, logString []byte) {
        if c.Response().StatusCode() != fiber.StatusOK {
            reporter.SendToSlack(logString)
        }
    },
}))

// Disable colors when outputting to default format
app.Use(logger.New(logger.Config{
    DisableColors: true,
}))

// Force the use of colors
app.Use(logger.New(logger.Config{
    ForceColors: true,
}))

// Use predefined formats
app.Use(logger.New(logger.Config{
    Format: logger.CommonFormat,
}))

app.Use(logger.New(logger.Config{
    Format: logger.CombinedFormat,
}))

app.Use(logger.New(logger.Config{
    Format: logger.JSONFormat,
}))

app.Use(logger.New(logger.Config{
    Format: logger.ECSFormat,
}))

Logging Handler Errors

The ${error} tag reports a non-nil error returned by a downstream handler or middleware. When no error is returned, it renders -. A response status alone is not an error: return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "..."}) sets ${status} to 500, but ${error} remains -.

To include an error in the log, return it and let Fiber's ErrorHandler create the response. The logger invokes the configured error handler before rendering the log, so a custom handler can return JSON while ${error} keeps the original error message:

go
app := fiber.New(fiber.Config{
    ErrorHandler: func(c fiber.Ctx, err error) error {
        code := fiber.StatusInternalServerError
        if fiberErr, ok := err.(*fiber.Error); ok {
            code = fiberErr.Code
        }
        return c.Status(code).JSON(fiber.Map{"error": "request failed"})
    },
})

app.Use(logger.New(logger.Config{
    Format: "${status} ${method} ${path} ${error}\n",
}))

app.Get("/reports", func(_ fiber.Ctx) error {
    return fiber.NewError(fiber.StatusInternalServerError, "report generation failed")
})

Register the logger before routes and downstream middleware whose returned errors it should observe. Use ${status} when code writes a complete response directly; status codes by themselves do not populate ${error}.

Auto-Registered Tags

Some Fiber middleware registers logger middleware tags automatically. Register the producing middleware before logger.New() and then use the tag in Format.

go
app.Use(requestid.New())
app.Use(logger.New(logger.Config{
    Format: "${requestid} ${status} ${method} ${path}\n",
}))

The logger middleware resolves tags in this order:

  1. Built-in logger tags, such as ${method}, ${path}, and ${status}.
  2. Globally registered tags from Fiber middleware or logger.RegisterTag.
  3. Config.CustomTags, which override tags with the same name for that logger instance.

The following tags are registered by Fiber middleware when the middleware is initialized:

TagRegistered byValue
${requestid} / ${request-id}requestid.New()Request ID stored by the requestid middleware.
${username}basicauth.New()Authenticated username stored by the basicauth middleware.
${api-key}keyauth.New()Redacted API key stored by the keyauth middleware.
${csrf-token}csrf.New()Redacted marker when the csrf middleware stores a token.
${session-id}session.New() or session.NewWithStore()Redacted session ID stored by the session middleware.

:::note Auto-registered tags are access-log tags for middleware/logger. The same names are also registered for application logs in the log package via logger.RegisterContextTag — see api/log#context-tags for details on log.WithContext enrichment. :::

Register Tags from Custom Middleware

Third-party middleware can expose logger tags with logger.RegisterTag or logger.MustRegisterTag. Use sync.Once so the tag is registered once even when the middleware is initialized multiple times.

A tag you register replaces the built-in renderer, so it does not inherit the control-character scrubbing the built-in tags apply. Wrap request-derived values in logger.SanitizeValue.

go
package tenantmw

import (
    "sync"

    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/middleware/logger"
)

type tenantContextKey struct{}

var tenantKey tenantContextKey

var registerLoggerTagsOnce sync.Once

func New() fiber.Handler {
    registerLoggerTagsOnce.Do(func() {
        logger.MustRegisterTag("tenant", func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
            tenant, _ := fiber.ValueFromContext[string](c, tenantKey)
            return output.WriteString(tenant)
        })
    })

    return func(c fiber.Ctx) error {
        fiber.StoreInContext(c, tenantKey, "acme")
        return c.Next()
    }
}

Use the registered tag in the logger format after installing the middleware:

go
app.Use(tenantmw.New())
app.Use(logger.New(logger.Config{
    Format: "${tenant} ${status} ${method} ${path}\n",
}))

Use Config.CustomTags when one logger instance needs a local override without changing the global tag registration. These also bypass the built-in control-character scrubbing — wrap request-derived values in logger.SanitizeValue:

go
app.Use(logger.New(logger.Config{
    Format: "${tenant} ${status} ${method} ${path}\n",
    CustomTags: map[string]logger.LogFunc{
        "tenant": func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
            return output.WriteString("override")
        },
    },
}))

Use Logger Middleware with Other Loggers

To combine the logger middleware with loggers like Zerolog, Zap, or Logrus, use the LoggerToWriter helper to adapt them to an io.Writer.

go
package main

import (
    "github.com/gofiber/contrib/fiberzap/v2"
    "github.com/gofiber/fiber/v3"
    "github.com/gofiber/fiber/v3/log"
    "github.com/gofiber/fiber/v3/middleware/logger"
)

func main() {
    // Create a new Fiber instance
    app := fiber.New()

    // Create a new zap logger which is compatible with Fiber AllLogger interface
    zap := fiberzap.NewLogger(fiberzap.LoggerConfig{
        ExtraKeys: []string{"request_id"},
    })

    // Use the logger middleware with the zap logger
    app.Use(logger.New(logger.Config{
        Stream: logger.LoggerToWriter(zap, log.LevelDebug),
    }))

    // Define a route
    app.Get("/", func(c fiber.Ctx) error {
        return c.SendString("Hello, World!")
    })

    // Start server on http://localhost:3000
    app.Listen(":3000")
}

:::tip Writing to os.File is goroutine-safe, but custom streams may require locking to serialize writes. :::

Config

PropertyTypeDescriptionDefault
Nextfunc(fiber.Ctx) boolNext defines a function to skip this middleware when it returns true.nil
Skipfunc(fiber.Ctx) boolSkip is a function to determine if logging is skipped or written to Stream.nil
Donefunc(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
CustomTagsmap[string]LogFuncDefines custom tag actions for this logger instance. These tags override built-in and globally registered tags with the same name.nil
FormatstringDefines the logging tags. See more in Predefined Formats, or create your own using Tags.[${time}] ${ip} ${status} - ${latency} ${method} ${path} ${error}\n (same as DefaultFormat)
TimeFormatstringTimeFormat defines the time format for log timestamps.15:04:05
TimeZonestringTimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc"Local"
TimeIntervaltime.DurationTimeInterval is the delay before the timestamp is updated.500 * time.Millisecond
Streamio.WriterStream is a writer where logs are written.os.Stdout
TimeDone<-chan struct{}TimeDone stops the background timestamp updater when it is closed.nil
BeforeHandlerFuncfunc(*Config)BeforeHandlerFunc runs once before the handler is built, letting you customize colors, template, etc.beforeHandlerFunc
LoggerFuncfunc(c fiber.Ctx, data *Data, cfg *Config) errorCustom 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
DisableColorsboolDisableColors defines if the logs output should be colorized.false
ForceColorsboolForceColors defines if the logs output should be colorized even when the output is not a terminal.false

Default Config

go
var ConfigDefault = Config{
    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 ConstantFormat StringDescription
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.

:::tip ${bytesSent} returns the value of the Content-Length response header. If the header is missing or the response is streaming (e.g., chunked encoding), the value will be -1. Fiber does not calculate the actual response body size for performance reasons. :::

The ${ips} tag

${ips} logs the chain the framework parsed, Ctx.IPs(), joined with ,. Reading X-Forwarded-For here separately meant reading it a second way, and under DisableHeaderNormalizing a lower-case x-forwarded-for: logged an empty chain while Ctx.IPs() went on returning it.

It is not the list any trust decision is made from. Ctx.IPs() parses X-Forwarded-For unconditionally, while Ctx.IP() consults TrustProxy and reads ProxyHeader, which need not be X-Forwarded-For at all — so ${ips} and the address Fiber acted on can name different hosts.

Because the entries are split and trimmed rather than echoed as sent, repeated X-Forwarded-For header lines and a single comma-joined one log identically, which is what RFC 9110 §5.2 says they are.

Treat a logged chain as attacker-controlled, trusted peer or not. A proxy you trust appends the address it saw to whatever the client already put in X-Forwarded-For, and Ctx.IPs() returns every element without the right-to-left walk Ctx.IP() uses, so the entries to the left of the ones your own infrastructure added are still the client's to choose. Use ${ip}, which is the peer address, when you need one you can rely on.

Control-Character Sanitization

Values that come from the request are scrubbed before they reach the log stream: every ASCII control byte (C0 and DEL) is replaced with a space, and horizontal tab is preserved. Without this, a percent-decoded query parameter, form field, or request body containing \r\n could forge additional access-log lines and corrupt an audit trail.

Scrubbing covers the default format as well as these tags:

${path} ${url} ${ua} ${referer} ${ip} ${ips} ${host} ${scheme} ${route} ${body} ${resBody} ${reqHeaders} ${queryParams} ${error} ${reqHeader:} ${respHeader:} ${query:} ${form:} ${cookie:} ${locals:}

Tags whose values the framework controls — ${status}, ${method}, ${protocol}, ${port}, ${latency}, ${pid}, ${time}, ${bytesSent}, ${bytesReceived} and the color tags — are written unchanged. ${method} and ${protocol} come from the request line, which fasthttp rejects outright if it holds a control byte.

Only ASCII controls are replaced. Bytes at or above 0x80 pass through untouched, so C1 controls (U+0080–U+009F, including NEL U+0085, which some log pipelines treat as a line break) survive scrubbing. Handle those yourself if your values can carry them.

:::caution Three paths bypass the built-in scrubbing, because each one replaces the renderer rather than wrapping it:

  • Config.CustomTags
  • RegisterTag / MustRegisterTag
  • Config.LoggerFunc, which replaces the rendering pipeline wholesale

Anything request-derived that you write from one of these needs scrubbing. Use logger.SanitizeValue, which applies exactly what the built-in tags apply:

go
logger.MustRegisterTag("tenant", func(output logger.Buffer, c fiber.Ctx, _ *logger.Data, _ string) (int, error) {
    return output.WriteString(logger.SanitizeValue(c.Get("X-Tenant-ID")))
})

RegisterContextTag is not on that list: it wraps your extractor rather than being one, so what the extractor returns is scrubbed on the way out — in both the access-log renderer and the log package one. Fiber's own context tags — ${username}, ${api-key}, ${csrf-token}, ${requestid}, ${session-id} — are registered through it, and the middleware behind each one validates or redacts at the source as well. :::

Constants

go
// Logger variables
const (
    TagPid               = "pid"
    TagTime              = "time"
    TagReferer           = "referer"
    TagProtocol          = "protocol"
    TagScheme            = "scheme"
    TagPort              = "port"
    TagIP                = "ip"
    TagIPs               = "ips"
    TagHost              = "host"
    TagMethod            = "method"
    TagPath              = "path"
    TagURL               = "url"
    TagUA                = "ua"
    TagLatency           = "latency"
    TagStatus            = "status"         // response status
    TagResBody           = "resBody"        // response body
    TagReqHeaders        = "reqHeaders"
    TagQueryStringParams = "queryParams"    // request query parameters
    TagBody              = "body"           // request body
    TagBytesSent         = "bytesSent"
    TagBytesReceived     = "bytesReceived"
    TagRoute             = "route"
    TagError             = "error"
    TagReqHeader         = "reqHeader:"     // request header
    TagRespHeader        = "respHeader:"    // response header
    TagQuery             = "query:"         // request query
    TagForm              = "form:"          // request form
    TagCookie            = "cookie:"        // request cookie
    TagLocals            = "locals:"
    // colors
    TagBlack             = "black"
    TagRed               = "red"
    TagGreen             = "green"
    TagYellow            = "yellow"
    TagBlue              = "blue"
    TagMagenta           = "magenta"
    TagCyan              = "cyan"
    TagWhite             = "white"
    TagReset             = "reset"
)