1
0
mirror of https://github.com/gofiber/fiber.git synced 2025-02-21 20:33:08 +00:00
fiber/middleware/logger.go
kiyon 76c67ca05a 🚀 use atomic.Value instead of sync.Mutex
To improve the performance and precision
2020-07-29 14:42:17 +08:00

465 lines
12 KiB
Go

package middleware
import (
"bytes"
"fmt"
"io"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
fiber "github.com/gofiber/fiber"
utils "github.com/gofiber/utils"
colorable "github.com/mattn/go-colorable"
isatty "github.com/mattn/go-isatty"
bytebufferpool "github.com/valyala/bytebufferpool"
)
// Middleware types
type (
// LoggerConfig defines the config for Logger middleware.
LoggerConfig struct {
// Next defines a function to skip this middleware if returned true.
Next func(*fiber.Ctx) bool
// Format defines the logging tags
//
// - pid
// - time
// - ip
// - ips
// - url
// - host
// - method
// - methodColored
// - path
// - protocol
// - route
// - referer
// - ua
// - latency
// - status
// - statusColored
// - body
// - error
// - bytesSent
// - bytesReceived
// - header:<key>
// - query:<key>
// - form:<key>
// - cookie:<key>
//
// Optional. Default: ${time} - ${ip} - ${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
// Colors are only supported if no custom Output is given
enableColors bool
// timeZoneLocation holds the compiled timezone
timeZoneLocation *time.Location
}
)
// Logger variables
const (
LoggerTagPid = "pid"
LoggerTagTime = "time"
LoggerTagReferer = "referer"
LoggerTagProtocol = "protocol"
LoggerTagIP = "ip"
LoggerTagIPs = "ips"
LoggerTagHost = "host"
LoggerTagMethod = "method"
LoggerTagPath = "path"
LoggerTagURL = "url"
LoggerTagUA = "ua"
LoggerTagLatency = "latency"
LoggerTagStatus = "status"
LoggerTagBody = "body"
LoggerTagBytesSent = "bytesSent"
LoggerTagBytesReceived = "bytesReceived"
LoggerTagRoute = "route"
LoggerTagError = "error"
LoggerTagHeader = "header:"
LoggerTagQuery = "query:"
LoggerTagForm = "form:"
LoggerTagCookie = "cookie:"
LoggerTagColorBlack = "black"
LoggerTagColorRed = "red"
LoggerTagColorGreen = "green"
LoggerTagColorYellow = "yellow"
LoggerTagColorBlue = "blue"
LoggerTagColorMagenta = "magenta"
LoggerTagColorCyan = "cyan"
LoggerTagColorWhite = "white"
LoggerTagColorReset = "resetColor"
// LoggerTagStatusColor = "statusColor"
// LoggerTagMethodColor = "methodColor"
)
// NEW : Color variables
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"
)
// for colorizing response status and request method
var (
statusColor string
responseStatus int
methodColor string
requestMethod string
)
// LoggerConfigDefault is the default config
var LoggerConfigDefault = LoggerConfig{
Next: nil,
Format: "#${pid} - ${time} ${status} - ${latency} ${method} ${path}\n",
TimeFormat: "2006/01/02 15:04:05",
TimeZone: "Local",
Output: os.Stderr,
}
/*
Logger allows the following config arguments in any order:
- Logger()
- Logger(next func(*fiber.Ctx) bool)
- Logger(output io.Writer)
- Logger(format string)
- Logger(timeZone string)
- Logger(timeFormat string)
- Logger(config LoggerConfig)
*/
func Logger(options ...interface{}) fiber.Handler {
// Create default config
var config = LoggerConfig{}
// Assert options if provided to adjust the config
if len(options) > 0 {
for i := range options {
switch opt := options[i].(type) {
case func(*fiber.Ctx) bool:
config.Next = opt
case string:
if strings.Contains(opt, "${") {
config.Format = opt
} else if tzl := getTimeZoneLocation(opt); tzl != nil {
config.TimeZone = opt
config.timeZoneLocation = tzl
} else {
config.TimeFormat = opt
}
case io.Writer:
config.Output = opt
case LoggerConfig:
config = opt
default:
panic("Logger: the following option types are allowed: string, io.Writer, LoggerConfig")
}
}
}
// Return logger
return logger(config)
}
func logger(config LoggerConfig) fiber.Handler {
// Set config default values
if config.Format == "" {
config.Format = LoggerConfigDefault.Format
}
if config.TimeZone == "" {
config.TimeZone = LoggerConfigDefault.TimeZone
}
if config.TimeFormat == "" {
config.TimeFormat = LoggerConfigDefault.TimeFormat
}
if config.Output == nil {
// Check if colors should be disabled
if os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) {
config.Output = LoggerConfigDefault.Output
} else {
config.enableColors = true
config.Output = colorable.NewColorableStderr()
}
}
var tmpl loggerTemplate
tmpl.new(config.Format, "${", "}")
var timestamp atomic.Value
timestamp.Store(nowTimeString(config.timeZoneLocation, config.TimeFormat))
// Update date/time every millisecond in a separate go routine
if strings.Contains(config.Format, "${time}") {
go func() {
for {
time.Sleep(time.Millisecond)
timestamp.Store(nowTimeString(config.timeZoneLocation, config.TimeFormat))
}
}()
}
pid := fmt.Sprintf("%-5s", strconv.Itoa(os.Getpid()))
// Return handler
return func(c *fiber.Ctx) {
// Don't execute the middleware if Next returns true
if config.Next != nil && config.Next(c) {
c.Next()
return
}
// Middleware logic...
start := time.Now()
// handle request
c.Next()
// build log
stop := time.Now()
// Get new buffer
buf := bytebufferpool.Get()
_, err := tmpl.executeFunc(buf, func(w io.Writer, tag string) (int, error) {
switch tag {
case LoggerTagTime:
return buf.WriteString(timestamp.Load().(string))
case LoggerTagReferer:
return buf.WriteString(c.Get(fiber.HeaderReferer))
case LoggerTagProtocol:
return buf.WriteString(c.Protocol())
case LoggerTagPid:
return buf.WriteString(pid)
case LoggerTagIP:
return buf.WriteString(c.IP())
case LoggerTagIPs:
return buf.WriteString(c.Get(fiber.HeaderXForwardedFor))
case LoggerTagHost:
return buf.WriteString(c.Hostname())
case LoggerTagPath:
return buf.WriteString(c.Path())
case LoggerTagURL:
return buf.WriteString(c.OriginalURL())
case LoggerTagUA:
return buf.WriteString(c.Get(fiber.HeaderUserAgent))
case LoggerTagLatency:
return buf.WriteString(fmt.Sprintf("%-6s", stop.Sub(start).Round(1*time.Millisecond)))
// return buf.WriteString(stop.Sub(start).String())
case LoggerTagBody:
return buf.WriteString(c.Body())
case LoggerTagBytesReceived:
return buf.WriteString(strconv.Itoa(len(c.Fasthttp.Request.Body())))
case LoggerTagBytesSent:
return buf.WriteString(strconv.Itoa(len(c.Fasthttp.Response.Body())))
case LoggerTagRoute:
return buf.WriteString(c.Route().Path)
case LoggerTagError:
if c.Error() != nil {
return buf.WriteString(c.Error().Error())
}
case LoggerTagColorBlack:
return buf.WriteString(cBlack)
case LoggerTagColorRed:
return buf.WriteString(cRed)
case LoggerTagColorGreen:
return buf.WriteString(cGreen)
case LoggerTagColorYellow:
return buf.WriteString(cYellow)
case LoggerTagColorBlue:
return buf.WriteString(cBlue)
case LoggerTagColorMagenta:
return buf.WriteString(cMagenta)
case LoggerTagColorCyan:
return buf.WriteString(cCyan)
case LoggerTagColorWhite:
return buf.WriteString(cWhite)
case LoggerTagColorReset:
return buf.WriteString(cReset)
case LoggerTagStatus:
responseStatus = c.Fasthttp.Response.StatusCode()
if !config.enableColors {
return buf.WriteString(strconv.Itoa(responseStatus))
}
switch {
case responseStatus >= 200 && responseStatus < 300:
statusColor = cGreen
case responseStatus >= 300 && responseStatus < 400:
statusColor = cBlue
case responseStatus >= 400 && responseStatus < 500:
statusColor = cYellow
default:
statusColor = cRed
}
return buf.WriteString(statusColor + strconv.Itoa(responseStatus) + cReset)
case LoggerTagMethod:
requestMethod = c.Method()
if !config.enableColors {
return buf.WriteString(requestMethod)
}
switch requestMethod {
case fiber.MethodGet:
methodColor = cGreen
case fiber.MethodPost:
methodColor = cCyan
case fiber.MethodPut:
methodColor = cYellow
case fiber.MethodDelete:
methodColor = cRed
case fiber.MethodPatch:
methodColor = cBlue
case fiber.MethodHead:
methodColor = cMagenta
case fiber.MethodOptions:
methodColor = cBlack
default:
methodColor = cReset
}
return buf.WriteString(fmt.Sprintf("%s%7s%s", methodColor, requestMethod, cReset))
//return buf.WriteString(methodColor + requestMethod + cReset)
default:
switch {
case strings.HasPrefix(tag, LoggerTagHeader):
return buf.WriteString(c.Get(tag[7:]))
case strings.HasPrefix(tag, LoggerTagQuery):
return buf.WriteString(c.Query(tag[6:]))
case strings.HasPrefix(tag, LoggerTagForm):
return buf.WriteString(c.FormValue(tag[5:]))
case strings.HasPrefix(tag, LoggerTagCookie):
return buf.WriteString(c.Cookies(tag[7:]))
}
}
return 0, nil
})
if err != nil {
_, _ = buf.WriteString(err.Error())
}
if _, err := config.Output.Write(buf.Bytes()); err != nil {
fmt.Println(err)
}
bytebufferpool.Put(buf)
}
}
func nowTimeString(tzl *time.Location, layout string) string {
// This is different from Golang's time package which returns UTC, and Local is better than it
if tzl == nil {
return time.Now().Format(layout)
}
return time.Now().In(tzl).Format(layout)
}
// Use Golang's time package to determine whether the TimeZone is available
func getTimeZoneLocation(name string) *time.Location {
tz, _ := time.LoadLocation(name)
return tz
}
// MIT License fasttemplate
// Copyright (c) 2015 Aliaksandr Valialkin
// https://github.com/valyala/fasttemplate/blob/master/LICENSE
type (
loggerTemplate struct {
template string
startTag string
endTag string
texts [][]byte
tags []string
}
loggerTagFunc func(w io.Writer, tag string) (int, error)
)
func (t *loggerTemplate) new(template, startTag, endTag string) {
t.template = template
t.startTag = startTag
t.endTag = endTag
t.texts = t.texts[:0]
t.tags = t.tags[:0]
if len(startTag) == 0 {
panic("startTag cannot be empty")
}
if len(endTag) == 0 {
panic("endTag cannot be empty")
}
s := utils.GetBytes(template)
a := utils.GetBytes(startTag)
b := utils.GetBytes(endTag)
tagsCount := bytes.Count(s, a)
if tagsCount == 0 {
return
}
if tagsCount+1 > cap(t.texts) {
t.texts = make([][]byte, 0, tagsCount+1)
}
if tagsCount > cap(t.tags) {
t.tags = make([]string, 0, tagsCount)
}
for {
n := bytes.Index(s, a)
if n < 0 {
t.texts = append(t.texts, s)
break
}
t.texts = append(t.texts, s[:n])
s = s[n+len(a):]
n = bytes.Index(s, b)
if n < 0 {
panic(fmt.Errorf("cannot find end tag=%q in the template=%q starting from %q", endTag, template, s))
}
t.tags = append(t.tags, utils.GetString(s[:n]))
s = s[n+len(b):]
}
}
func (t *loggerTemplate) executeFunc(w io.Writer, f loggerTagFunc) (int64, error) {
var nn int64
n := len(t.texts) - 1
if n == -1 {
ni, err := w.Write(utils.GetBytes(t.template))
return int64(ni), err
}
for i := 0; i < n; i++ {
ni, err := w.Write(t.texts[i])
nn += int64(ni)
if err != nil {
return nn, err
}
ni, err = f(w, t.tags[i])
nn += int64(ni)
if err != nil {
return nn, err
}
}
ni, err := w.Write(t.texts[n])
nn += int64(ni)
return nn, err
}