136 lines
3.1 KiB
Go

package logger
import (
"path"
"regexp"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
const (
LoggerContextKey = "logger"
LoggerRealContextKey = "loggerReal"
)
// GinMiddleware will construct Gin middleware which will log requests and provide logger with unique request ID.
func GinMiddleware(log Logger, skipPaths ...string) gin.HandlerFunc {
var (
skip map[string]struct{}
matchSkip []*skippedPath
)
if length := len(skipPaths); length > 0 {
skip = make(map[string]struct{}, length)
for _, path := range skipPaths {
if skipped, ok := newSkippedPath(path); ok {
matchSkip = append(matchSkip, skipped)
continue
}
skip[path] = struct{}{}
}
}
nilLogger := NewNil()
return func(c *gin.Context) {
// Start timer
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
_, shouldSkip := skip[path]
streamID := generateStreamID()
log := log.With(StreamID(streamID))
if !shouldSkip && len(matchSkip) > 0 {
for _, skipper := range matchSkip {
if skipper.match(path) {
shouldSkip = true
break
}
}
}
if shouldSkip {
c.Set(LoggerRealContextKey, log)
log = nilLogger
}
c.Set(StreamIDAttr, streamID)
c.Set(LoggerContextKey, log)
// Process request
c.Next()
end := time.Now()
if raw != "" {
path = path + "?" + raw
}
if !shouldSkip {
log.Info("request",
zap.String(HandlerAttr, "GIN"),
zap.String("startTime", start.Format(time.RFC3339)),
zap.String("endTime", end.Format(time.RFC3339)),
zap.Any("latency", end.Sub(start)/time.Millisecond),
zap.String("remoteAddress", c.ClientIP()),
zap.String(HTTPMethodAttr, c.Request.Method),
zap.String("path", path),
zap.Int("bodySize", c.Writer.Size()),
)
}
}
}
func MustGet(c *gin.Context) Logger {
return c.MustGet("logger").(Logger)
}
func MustGetReal(c *gin.Context) Logger {
log, ok := c.Get(LoggerContextKey)
if _, isNil := log.(*Nil); !ok || isNil {
return c.MustGet(LoggerRealContextKey).(Logger)
}
return log.(Logger)
}
var (
hasParamsMatcher = regexp.MustCompile(`/:\w+`)
hasWildcardParamsMatcher = regexp.MustCompile(`/\*\w+.*`)
)
type skippedPath struct {
path string
expr *regexp.Regexp
}
// newSkippedPath returns new path skipping struct. It returns nil, false if expr is simple and
// no complex logic is needed.
func newSkippedPath(expr string) (result *skippedPath, compatible bool) {
hasParams, hasWildcard := hasParamsMatcher.MatchString(expr), hasWildcardParamsMatcher.MatchString(expr)
if !hasParams && !hasWildcard {
return nil, false
}
if hasWildcard {
return &skippedPath{expr: matcherForWildcard(expr)}, true
}
return &skippedPath{path: matcherForPath(expr)}, true
}
func matcherForWildcard(expr string) *regexp.Regexp {
return regexp.MustCompile(hasWildcardParamsMatcher.ReplaceAllString(expr, "/[\\w/]+"))
}
func matcherForPath(expr string) string {
return hasParamsMatcher.ReplaceAllString(expr, "/*")
}
func (p *skippedPath) match(route string) bool {
if p.expr != nil {
return p.expr.MatchString(route)
}
result, err := path.Match(p.path, route)
return result && err == nil
}