Files
bitmagnet/internal/boilerplate/httpserver/ginzap/ginzap.go
T
mgdigital c16f76130c Classifier rewrite (#213)
The classifier has been re-implemented and now uses a DSL allowing for full customisation. Several bugs have also been fixed.

- Closes https://github.com/bitmagnet-io/bitmagnet/issues/182
- Closes https://github.com/bitmagnet-io/bitmagnet/issues/70
- Closes https://github.com/bitmagnet-io/bitmagnet/issues/68
- Hopefully fixes https://github.com/bitmagnet-io/bitmagnet/issues/126
2024-04-21 16:24:10 +01:00

163 lines
4.7 KiB
Go

// Package ginzap provides log handling using zap package.
// Code structure based on ginrus package.
package ginzap
import (
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type Fn func(c *gin.Context) []zapcore.Field
// ZapLogger is the minimal logger interface compatible with zap.Logger
type ZapLogger interface {
Debug(msg string, fields ...zap.Field)
Info(msg string, fields ...zap.Field)
Error(msg string, fields ...zap.Field)
}
// Config is config setting for Ginzap
type Config struct {
TimeFormat string
UTC bool
SkipPaths []string
Context Fn
}
// Ginzap returns a gin.HandlerFunc (middleware) that logs requests using uber-go/zap.
//
// Requests with errors are logged using zap.Error().
// Requests without errors are logged using zap.Info().
//
// It receives:
// 1. A time package format string (e.g. time.RFC3339).
// 2. A boolean stating whether to use UTC time zone or local.
func Ginzap(logger ZapLogger, timeFormat string, utc bool) gin.HandlerFunc {
return GinzapWithConfig(logger, &Config{TimeFormat: timeFormat, UTC: utc})
}
// GinzapWithConfig returns a gin.HandlerFunc using configs
func GinzapWithConfig(logger ZapLogger, conf *Config) gin.HandlerFunc {
skipPaths := make(map[string]bool, len(conf.SkipPaths))
for _, path := range conf.SkipPaths {
skipPaths[path] = true
}
return func(c *gin.Context) {
start := time.Now()
// some evil middlewares modify this values
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
if _, ok := skipPaths[path]; !ok {
end := time.Now()
latency := end.Sub(start)
if conf.UTC {
end = end.UTC()
}
fields := []zapcore.Field{
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.Duration("latency", latency),
}
if conf.TimeFormat != "" {
fields = append(fields, zap.String("time", end.Format(conf.TimeFormat)))
}
if conf.Context != nil {
fields = append(fields, conf.Context(c)...)
}
if len(c.Errors) > 0 {
// Append error field if this is an erroneous request.
for _, e := range c.Errors.Errors() {
logger.Error(e, fields...)
}
} else {
logger.Debug(path, fields...)
}
}
}
}
func defaultHandleRecovery(c *gin.Context, err interface{}) {
c.AbortWithStatus(http.StatusInternalServerError)
}
// RecoveryWithZap returns a gin.HandlerFunc (middleware)
// that recovers from any panics and logs requests using uber-go/zap.
// All errors are logged using zap.Error().
// stack means whether output the stack info.
// The stack info is easy to find where the error occurs but the stack info is too large.
func RecoveryWithZap(logger ZapLogger, stack bool) gin.HandlerFunc {
return CustomRecoveryWithZap(logger, stack, defaultHandleRecovery)
}
// CustomRecoveryWithZap returns a gin.HandlerFunc (middleware) with a custom recovery handler
// that recovers from any panics and logs requests using uber-go/zap.
// All errors are logged using zap.Error().
// stack means whether output the stack info.
// The stack info is easy to find where the error occurs but the stack info is too large.
func CustomRecoveryWithZap(logger ZapLogger, stack bool, recovery gin.RecoveryFunc) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
// check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck
c.Abort()
return
}
if stack {
logger.Error("[Recovery from panic]",
zap.Time("time", time.Now()),
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
logger.Error("[Recovery from panic]",
zap.Time("time", time.Now()),
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
recovery(c, err)
}
}()
c.Next()
}
}