fiber/middleware/logger/logger.go
Fenny fcfb063f0b ✏ remove arrow
Co-Authored-By: Yuil Tripathee <28005756+YuilTripathee@users.noreply.github.com>
2020-09-29 21:41:47 +02:00

363 lines
9.0 KiB
Go

package logger
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/internal/bytebufferpool"
"github.com/gofiber/fiber/v2/internal/colorable"
"github.com/gofiber/fiber/v2/internal/fasttemplate"
"github.com/gofiber/fiber/v2/internal/isatty"
"github.com/valyala/fasthttp"
)
// Config defines the config for middleware.
type Config struct {
// Next defines a function to skip this middleware when returned true.
//
// Optional. Default: nil
Next func(c *fiber.Ctx) bool
// Format defines the logging tags
//
// Optional. Default: [${time}] ${status} - ${latency} ${method} ${path}\n
Format string
// TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html
//
// Optional. Default: 15:04:05
TimeFormat string
// TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc
//
// Optional. Default: "Local"
TimeZone string
// Output is a writter where logs are written
//
// Default: os.Stderr
Output io.Writer
enableColors bool
enableLatency bool
timeZoneLocation *time.Location
}
// ConfigDefault is the default config
var ConfigDefault = Config{
Next: nil,
Format: "[${time}] ${status} - ${latency} ${method} ${path}\n",
TimeFormat: "15:04:05",
TimeZone: "Local",
Output: os.Stderr,
}
// Logger variables
const (
TagPid = "pid"
TagTime = "time"
TagReferer = "referer"
TagProtocol = "protocol"
TagIP = "ip"
TagIPs = "ips"
TagHost = "host"
TagMethod = "method"
TagPath = "path"
TagURL = "url"
TagUA = "ua"
TagLatency = "latency"
TagStatus = "status"
TagBody = "body"
TagBytesSent = "bytesSent"
TagBytesReceived = "bytesReceived"
TagRoute = "route"
TagError = "error"
TagHeader = "header:"
TagQuery = "query:"
TagForm = "form:"
TagCookie = "cookie:"
TagBlack = "black"
TagRed = "red"
TagGreen = "green"
TagYellow = "yellow"
TagBlue = "blue"
TagMagenta = "magenta"
TagCyan = "cyan"
TagWhite = "white"
TagReset = "reset"
)
// Color values
const (
cBlack = "\u001b[90m"
cRed = "\u001b[91m"
cGreen = "\u001b[92m"
cYellow = "\u001b[93m"
cBlue = "\u001b[94m"
cMagenta = "\u001b[95m"
cCyan = "\u001b[96m"
cWhite = "\u001b[97m"
cReset = "\u001b[0m"
)
// New creates a new middleware handler
func New(config ...Config) fiber.Handler {
// Set default config
cfg := ConfigDefault
// Override config if provided
if len(config) > 0 {
cfg = config[0]
// Enable colors if no custom format or output is given
if cfg.Format == "" && cfg.Output == nil {
cfg.enableColors = true
}
// Set default values
if cfg.Next == nil {
cfg.Next = ConfigDefault.Next
}
if cfg.Format == "" {
cfg.Format = ConfigDefault.Format
}
if cfg.TimeZone == "" {
cfg.TimeZone = ConfigDefault.TimeZone
}
if cfg.TimeFormat == "" {
cfg.TimeFormat = ConfigDefault.TimeFormat
}
if cfg.Output == nil {
cfg.Output = ConfigDefault.Output
}
} else {
cfg.enableColors = true
}
// Get timezone location
tz, err := time.LoadLocation(cfg.TimeZone)
if err != nil || tz == nil {
cfg.timeZoneLocation = time.Local
} else {
cfg.timeZoneLocation = tz
}
// Check if format contains latency
cfg.enableLatency = strings.Contains(cfg.Format, "${latency}")
// Create template parser
tmpl := fasttemplate.New(cfg.Format, "${", "}")
// Create correct timeformat
var timestamp atomic.Value
timestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat))
// Update date/time every 750 milliseconds in a separate go routine
if strings.Contains(cfg.Format, "${time}") {
go func() {
for {
time.Sleep(750 * time.Millisecond)
timestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat))
}
}()
}
// Set PID once
pid := strconv.Itoa(os.Getpid())
// Set variables
var (
start, stop time.Time
once sync.Once
errHandler fiber.ErrorHandler
)
// If colors are enabled, check terminal compatibility
if cfg.enableColors {
cfg.Output = colorable.NewColorableStderr()
if os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(os.Stderr.Fd()) && !isatty.IsCygwinTerminal(os.Stderr.Fd())) {
cfg.Output = colorable.NewNonColorable(os.Stderr)
}
}
var errPadding = 15
var errPaddingStr = strconv.Itoa(errPadding)
// Return new handler
return func(c *fiber.Ctx) (err error) {
// Don't execute middleware if Next returns true
if cfg.Next != nil && cfg.Next(c) {
return c.Next()
}
// Set error handler once
once.Do(func() {
errHandler = c.App().Config().ErrorHandler
stack := c.App().Stack()
for m := range stack {
for r := range stack[m] {
if len(stack[m][r].Path) > errPadding {
errPadding = len(stack[m][r].Path)
errPaddingStr = strconv.Itoa(errPadding)
}
}
}
})
// Set latency start time
if cfg.enableLatency {
start = time.Now()
}
// Handle request, store err for logging
chainErr := c.Next()
// Manually call error handler
if chainErr != nil {
if err := errHandler(c, chainErr); err != nil {
_ = c.SendStatus(fiber.StatusInternalServerError)
}
}
// Set latency stop time
if cfg.enableLatency {
stop = time.Now()
}
// Get new buffer
buf := bytebufferpool.Get()
// Default output when no custom Format or io.Writer is given
if cfg.enableColors {
// Format error if exist
formatErr := ""
if chainErr != nil {
formatErr = cRed + " | " + chainErr.Error() + cReset
}
// Format log to buffer
_, _ = buf.WriteString(fmt.Sprintf("%s |%s %3d %s| %7v | %15s |%s %-7s %s| %-"+errPaddingStr+"s %s\n",
timestamp.Load().(string),
statusColor(c.Response().StatusCode()), c.Response().StatusCode(), cReset,
stop.Sub(start).Round(time.Millisecond),
c.IP(),
methodColor(c.Method()), c.Method(), cReset,
c.Path(),
formatErr,
))
// Write buffer to output
_, _ = cfg.Output.Write(buf.Bytes())
// Put buffer back to pool
bytebufferpool.Put(buf)
// End chain
return nil
}
// Loop over template tags to replace it with the correct value
_, err = tmpl.ExecuteFunc(buf, func(w io.Writer, tag string) (int, error) {
switch tag {
case TagTime:
return buf.WriteString(timestamp.Load().(string))
case TagReferer:
return buf.WriteString(c.Get(fiber.HeaderReferer))
case TagProtocol:
return buf.WriteString(c.Protocol())
case TagPid:
return buf.WriteString(pid)
case TagIP:
return buf.WriteString(c.IP())
case TagIPs:
return buf.WriteString(c.Get(fiber.HeaderXForwardedFor))
case TagHost:
return buf.WriteString(c.Hostname())
case TagPath:
return buf.WriteString(c.Path())
case TagURL:
return buf.WriteString(c.OriginalURL())
case TagUA:
return buf.WriteString(c.Get(fiber.HeaderUserAgent))
case TagLatency:
return buf.WriteString(stop.Sub(start).String())
case TagBody:
return buf.Write(c.Body())
case TagBytesReceived:
return appendInt(buf, len(c.Request().Body()))
case TagBytesSent:
return appendInt(buf, len(c.Response().Body()))
case TagRoute:
return buf.WriteString(c.Route().Path)
case TagStatus:
return appendInt(buf, c.Response().StatusCode())
case TagMethod:
return buf.WriteString(c.Method())
case TagBlack:
return buf.WriteString(cBlack)
case TagRed:
return buf.WriteString(cRed)
case TagGreen:
return buf.WriteString(cGreen)
case TagYellow:
return buf.WriteString(cYellow)
case TagBlue:
return buf.WriteString(cBlue)
case TagMagenta:
return buf.WriteString(cMagenta)
case TagCyan:
return buf.WriteString(cCyan)
case TagWhite:
return buf.WriteString(cWhite)
case TagReset:
return buf.WriteString(cReset)
case TagError:
if chainErr != nil {
return buf.WriteString(chainErr.Error())
}
return buf.WriteString("-")
default:
// Check if we have a value tag i.e.: "header:x-key"
switch {
case strings.HasPrefix(tag, TagHeader):
return buf.WriteString(c.Get(tag[7:]))
case strings.HasPrefix(tag, TagQuery):
return buf.WriteString(c.Query(tag[6:]))
case strings.HasPrefix(tag, TagForm):
return buf.WriteString(c.FormValue(tag[5:]))
case strings.HasPrefix(tag, TagCookie):
return buf.WriteString(c.Cookies(tag[7:]))
}
}
return 0, nil
})
// Also write errors to the buffer
if err != nil {
_, _ = buf.WriteString(err.Error())
}
// Write buffer to output
if _, err := cfg.Output.Write(buf.Bytes()); err != nil {
// Write error to output
if _, err := cfg.Output.Write([]byte(err.Error())); err != nil {
// There is something wrong with the given io.Writer
// TODO: What should we do here?
}
}
// Put buffer back to pool
bytebufferpool.Put(buf)
return nil
}
}
func appendInt(buf *bytebufferpool.ByteBuffer, v int) (int, error) {
old := len(buf.B)
buf.B = fasthttp.AppendUint(buf.B, v)
return len(buf.B) - old, nil
}