This commit is contained in:
GitLab Deploy Bot
2025-10-21 23:45:13 +07:00
parent 6c387b420c
commit bb60e987e5
3548 changed files with 4952576 additions and 116 deletions
+136
View File
@@ -0,0 +1,136 @@
package logger
import (
"io"
"os"
"time"
"github.com/gofiber/fiber/v2"
)
// 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
// Done is a function that is called after the log string for a request is written to Output,
// and pass the log string as parameter.
//
// Optional. Default: nil
Done func(c *fiber.Ctx, logString []byte)
// tagFunctions defines the custom tag action
//
// Optional. Default: map[string]LogFunc
CustomTags map[string]LogFunc
// Format defines the logging tags
//
// Optional. Default: ${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\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
// TimeInterval is the delay before the timestamp is updated
//
// Optional. Default: 500 * time.Millisecond
TimeInterval time.Duration
// Output is a writer where logs are written
//
// Default: os.Stdout
Output io.Writer
// DisableColors defines if the logs output should be colorized
//
// Default: false
DisableColors bool
enableColors bool
enableLatency bool
timeZoneLocation *time.Location
}
const (
startTag = "${"
endTag = "}"
paramSeparator = ":"
)
type Buffer interface {
Len() int
ReadFrom(r io.Reader) (int64, error)
WriteTo(w io.Writer) (int64, error)
Bytes() []byte
Write(p []byte) (int, error)
WriteByte(c byte) error
WriteString(s string) (int, error)
Set(p []byte)
SetString(s string)
String() string
}
type LogFunc func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error)
// ConfigDefault is the default config
var ConfigDefault = Config{
Next: nil,
Done: nil,
Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\n",
TimeFormat: "15:04:05",
TimeZone: "Local",
TimeInterval: 500 * time.Millisecond,
Output: os.Stdout,
DisableColors: false,
enableColors: true,
}
// Helper function to set default values
func configDefault(config ...Config) Config {
// Return default config if nothing provided
if len(config) < 1 {
return ConfigDefault
}
// Override default config
cfg := config[0]
// Set default values
if cfg.Next == nil {
cfg.Next = ConfigDefault.Next
}
if cfg.Done == nil {
cfg.Done = ConfigDefault.Done
}
if cfg.Format == "" {
cfg.Format = ConfigDefault.Format
}
if cfg.TimeZone == "" {
cfg.TimeZone = ConfigDefault.TimeZone
}
if cfg.TimeFormat == "" {
cfg.TimeFormat = ConfigDefault.TimeFormat
}
if int(cfg.TimeInterval) <= 0 {
cfg.TimeInterval = ConfigDefault.TimeInterval
}
if cfg.Output == nil {
cfg.Output = ConfigDefault.Output
}
if !cfg.DisableColors && cfg.Output == ConfigDefault.Output {
cfg.enableColors = true
}
return cfg
}
+16
View File
@@ -0,0 +1,16 @@
package logger
import (
"sync/atomic"
"time"
)
// Data is a struct to define some variables to use in custom logger function.
type Data struct {
Pid string
ErrPaddingStr string
ChainErr error
Start time.Time
Stop time.Time
Timestamp atomic.Value
}
+182
View File
@@ -0,0 +1,182 @@
package logger
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/utils"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/valyala/bytebufferpool"
"github.com/valyala/fasthttp"
)
// New creates a new middleware handler
func New(config ...Config) fiber.Handler {
// Set default config
cfg := configDefault(config...)
// 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, "${"+TagLatency+"}")
var timestamp atomic.Value
// Create correct timeformat
timestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat))
// Update date/time every 500 milliseconds in a separate go routine
if strings.Contains(cfg.Format, "${"+TagTime+"}") {
go func() {
for {
time.Sleep(cfg.TimeInterval)
timestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat))
}
}()
}
// Set PID once
pid := strconv.Itoa(os.Getpid())
// Set variables
var (
once sync.Once
mu sync.Mutex
errHandler fiber.ErrorHandler
dataPool = sync.Pool{New: func() interface{} { return new(Data) }}
)
// If colors are enabled, check terminal compatibility
if cfg.enableColors {
cfg.Output = colorable.NewColorableStdout()
if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) {
cfg.Output = colorable.NewNonColorable(os.Stdout)
}
}
errPadding := 15
errPaddingStr := strconv.Itoa(errPadding)
// instead of analyzing the template inside(handler) each time, this is done once before
// and we create several slices of the same length with the functions to be executed and fixed parts.
templateChain, logFunChain, err := buildLogFuncChain(&cfg, createTagMap(&cfg))
if err != nil {
panic(err)
}
// Return new handler
return func(c *fiber.Ctx) 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() {
// get longested possible path
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)
}
}
}
// override error handler
errHandler = c.App().ErrorHandler
})
// Logger data
data := dataPool.Get().(*Data) //nolint:forcetypeassert,errcheck // We store nothing else in the pool
// no need for a reset, as long as we always override everything
data.Pid = pid
data.ErrPaddingStr = errPaddingStr
data.Timestamp = timestamp
// put data back in the pool
defer dataPool.Put(data)
// Set latency start time
if cfg.enableLatency {
data.Start = time.Now()
}
// Handle request, store err for logging
chainErr := c.Next()
data.ChainErr = chainErr
// Manually call error handler
if chainErr != nil {
if err := errHandler(c, chainErr); err != nil {
_ = c.SendStatus(fiber.StatusInternalServerError) //nolint:errcheck // TODO: Explain why we ignore the error here
}
}
// Set latency stop time
if cfg.enableLatency {
data.Stop = time.Now()
}
// Get new buffer
buf := bytebufferpool.Get()
var err error
// Loop over template parts execute dynamic parts and add fixed parts to the buffer
for i, logFunc := range logFunChain {
if logFunc == nil {
_, _ = buf.Write(templateChain[i]) //nolint:errcheck // This will never fail
} else if templateChain[i] == nil {
_, err = logFunc(buf, c, data, "")
} else {
_, err = logFunc(buf, c, data, utils.UnsafeString(templateChain[i]))
}
if err != nil {
break
}
}
// Also write errors to the buffer
if err != nil {
_, _ = buf.WriteString(err.Error()) //nolint:errcheck // This will never fail
}
mu.Lock()
// 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
_, _ = fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
}
mu.Unlock()
if cfg.Done != nil {
cfg.Done(c, buf.Bytes())
}
// Put buffer back to pool
bytebufferpool.Put(buf)
return nil
}
}
func appendInt(output Buffer, v int) (int, error) {
old := output.Len()
output.Set(fasthttp.AppendUint(output.Bytes(), v))
return output.Len() - old, nil
}
+211
View File
@@ -0,0 +1,211 @@
package logger
import (
"fmt"
"strings"
"github.com/gofiber/fiber/v2"
)
// Logger variables
const (
TagPid = "pid"
TagTime = "time"
TagReferer = "referer"
TagProtocol = "protocol"
TagPort = "port"
TagIP = "ip"
TagIPs = "ips"
TagHost = "host"
TagMethod = "method"
TagPath = "path"
TagURL = "url"
TagUA = "ua"
TagLatency = "latency"
TagStatus = "status"
TagResBody = "resBody"
TagReqHeaders = "reqHeaders"
TagQueryStringParams = "queryParams"
TagBody = "body"
TagBytesSent = "bytesSent"
TagBytesReceived = "bytesReceived"
TagRoute = "route"
TagError = "error"
// Deprecated: Use TagReqHeader instead
TagHeader = "header:"
TagReqHeader = "reqHeader:"
TagRespHeader = "respHeader:"
TagLocals = "locals:"
TagQuery = "query:"
TagForm = "form:"
TagCookie = "cookie:"
TagBlack = "black"
TagRed = "red"
TagGreen = "green"
TagYellow = "yellow"
TagBlue = "blue"
TagMagenta = "magenta"
TagCyan = "cyan"
TagWhite = "white"
TagReset = "reset"
)
// createTagMap function merged the default with the custom tags
func createTagMap(cfg *Config) map[string]LogFunc {
// Set default tags
tagFunctions := map[string]LogFunc{
TagReferer: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Get(fiber.HeaderReferer))
},
TagProtocol: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Protocol())
},
TagPort: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Port())
},
TagIP: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.IP())
},
TagIPs: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Get(fiber.HeaderXForwardedFor))
},
TagHost: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Hostname())
},
TagPath: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Path())
},
TagURL: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.OriginalURL())
},
TagUA: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Get(fiber.HeaderUserAgent))
},
TagBody: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.Write(c.Body())
},
TagBytesReceived: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return appendInt(output, len(c.Request().Body()))
},
TagBytesSent: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
if c.Response().Header.ContentLength() < 0 {
return appendInt(output, 0)
}
return appendInt(output, len(c.Response().Body()))
},
TagRoute: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Route().Path)
},
TagResBody: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.Write(c.Response().Body())
},
TagReqHeaders: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
reqHeaders := make([]string, 0)
for k, v := range c.GetReqHeaders() {
reqHeaders = append(reqHeaders, k+"="+strings.Join(v, ","))
}
return output.Write([]byte(strings.Join(reqHeaders, "&")))
},
TagQueryStringParams: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Request().URI().QueryArgs().String())
},
TagBlack: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.App().Config().ColorScheme.Black)
},
TagRed: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.App().Config().ColorScheme.Red)
},
TagGreen: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.App().Config().ColorScheme.Green)
},
TagYellow: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.App().Config().ColorScheme.Yellow)
},
TagBlue: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.App().Config().ColorScheme.Blue)
},
TagMagenta: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.App().Config().ColorScheme.Magenta)
},
TagCyan: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.App().Config().ColorScheme.Cyan)
},
TagWhite: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.App().Config().ColorScheme.White)
},
TagReset: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.App().Config().ColorScheme.Reset)
},
TagError: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
if data.ChainErr != nil {
if cfg.enableColors {
colors := c.App().Config().ColorScheme
return output.WriteString(fmt.Sprintf("%s%s%s", colors.Red, data.ChainErr.Error(), colors.Reset))
}
return output.WriteString(data.ChainErr.Error())
}
return output.WriteString("-")
},
TagReqHeader: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Get(extraParam))
},
TagHeader: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Get(extraParam))
},
TagRespHeader: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.GetRespHeader(extraParam))
},
TagQuery: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Query(extraParam))
},
TagForm: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.FormValue(extraParam))
},
TagCookie: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(c.Cookies(extraParam))
},
TagLocals: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
switch v := c.Locals(extraParam).(type) {
case []byte:
return output.Write(v)
case string:
return output.WriteString(v)
case nil:
return 0, nil
default:
return output.WriteString(fmt.Sprintf("%v", v))
}
},
TagStatus: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
if cfg.enableColors {
colors := c.App().Config().ColorScheme
return output.WriteString(fmt.Sprintf("%s%3d%s", statusColor(c.Response().StatusCode(), colors), c.Response().StatusCode(), colors.Reset))
}
return appendInt(output, c.Response().StatusCode())
},
TagMethod: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
if cfg.enableColors {
colors := c.App().Config().ColorScheme
return output.WriteString(fmt.Sprintf("%s%s%s", methodColor(c.Method(), colors), c.Method(), colors.Reset))
}
return output.WriteString(c.Method())
},
TagPid: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(data.Pid)
},
TagLatency: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
latency := data.Stop.Sub(data.Start)
return output.WriteString(fmt.Sprintf("%13v", latency))
},
TagTime: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) {
return output.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert // We always store a string in here
},
}
// merge with custom tags from user
for k, v := range cfg.CustomTags {
tagFunctions[k] = v
}
return tagFunctions
}
+70
View File
@@ -0,0 +1,70 @@
package logger
import (
"bytes"
"errors"
"github.com/gofiber/fiber/v2/utils"
)
// buildLogFuncChain analyzes the template and creates slices with the functions for execution and
// slices with the fixed parts of the template and the parameters
//
// fixParts contains the fixed parts of the template or parameters if a function is stored in the funcChain at this position
// funcChain contains for the parts which exist the functions for the dynamic parts
// funcChain and fixParts always have the same length and contain nil for the parts where no data is required in the chain,
// if a function exists for the part, a parameter for it can also exist in the fixParts slice
func buildLogFuncChain(cfg *Config, tagFunctions map[string]LogFunc) ([][]byte, []LogFunc, error) {
// process flow is copied from the fasttemplate flow https://github.com/valyala/fasttemplate/blob/2a2d1afadadf9715bfa19683cdaeac8347e5d9f9/template.go#L23-L62
templateB := utils.UnsafeBytes(cfg.Format)
startTagB := utils.UnsafeBytes(startTag)
endTagB := utils.UnsafeBytes(endTag)
paramSeparatorB := utils.UnsafeBytes(paramSeparator)
var fixParts [][]byte
var funcChain []LogFunc
for {
currentPos := bytes.Index(templateB, startTagB)
if currentPos < 0 {
// no starting tag found in the existing template part
break
}
// add fixed part
funcChain = append(funcChain, nil)
fixParts = append(fixParts, templateB[:currentPos])
templateB = templateB[currentPos+len(startTagB):]
currentPos = bytes.Index(templateB, endTagB)
if currentPos < 0 {
// cannot find end tag - just write it to the output.
funcChain = append(funcChain, nil)
fixParts = append(fixParts, startTagB)
break
}
// ## function block ##
// first check for tags with parameters
if index := bytes.Index(templateB[:currentPos], paramSeparatorB); index != -1 {
logFunc, ok := tagFunctions[utils.UnsafeString(templateB[:index+1])]
if !ok {
return nil, nil, errors.New("No parameter found in \"" + utils.UnsafeString(templateB[:currentPos]) + "\"")
}
funcChain = append(funcChain, logFunc)
// add param to the fixParts
fixParts = append(fixParts, templateB[index+1:currentPos])
} else if logFunc, ok := tagFunctions[utils.UnsafeString(templateB[:currentPos])]; ok {
// add functions without parameter
funcChain = append(funcChain, logFunc)
fixParts = append(fixParts, nil)
}
// ## function block end ##
// reduce the template string
templateB = templateB[currentPos+len(endTagB):]
}
// set the rest
funcChain = append(funcChain, nil)
fixParts = append(fixParts, templateB)
return fixParts, funcChain, nil
}
+39
View File
@@ -0,0 +1,39 @@
package logger
import (
"github.com/gofiber/fiber/v2"
)
func methodColor(method string, colors fiber.Colors) string {
switch method {
case fiber.MethodGet:
return colors.Cyan
case fiber.MethodPost:
return colors.Green
case fiber.MethodPut:
return colors.Yellow
case fiber.MethodDelete:
return colors.Red
case fiber.MethodPatch:
return colors.White
case fiber.MethodHead:
return colors.Magenta
case fiber.MethodOptions:
return colors.Blue
default:
return colors.Reset
}
}
func statusColor(code int, colors fiber.Colors) string {
switch {
case code >= fiber.StatusOK && code < fiber.StatusMultipleChoices:
return colors.Green
case code >= fiber.StatusMultipleChoices && code < fiber.StatusBadRequest:
return colors.Blue
case code >= fiber.StatusBadRequest && code < fiber.StatusInternalServerError:
return colors.Yellow
default:
return colors.Red
}
}