新增单行文本编码器与结构化 GORM 日志,统一错误记录与请求日志策略,收紧日志文件权限并修复按天切分与压缩,支付回调参数脱敏,生产强制阿里云短信,RequestID 校验防注入,日志文案中文化。
104 lines
3.0 KiB
Go
104 lines
3.0 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"hfb_sys/backend/pkg/response"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// 正常请求不记录访问日志;只有慢请求、服务端错误、限流和有诊断价值的认证失败会输出。
|
|
const slowRequestThresholdMs = 500
|
|
|
|
func RequestLogger(logger *zap.Logger) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
start := time.Now()
|
|
c.Next()
|
|
|
|
latencyMs := float64(time.Since(start).Microseconds()) / 1000
|
|
status := c.Writer.Status()
|
|
path := c.Request.URL.Path
|
|
route := c.FullPath()
|
|
authFailure := meaningfulAuthFailure(c)
|
|
|
|
if shouldSkipHTTPLog(path, route, status, latencyMs) && !authFailure {
|
|
return
|
|
}
|
|
|
|
fields := []zap.Field{
|
|
zap.String("request_id", GetRequestID(c)),
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("path", path),
|
|
zap.String("route", route),
|
|
zap.Int("status", status),
|
|
zap.String("code", response.CodeFromContext(c)),
|
|
zap.Float64("duration_ms", latencyMs),
|
|
zap.String("client_ip", c.ClientIP()),
|
|
}
|
|
if userID, ok := c.Get(ContextUserID); ok {
|
|
fields = append(fields, zap.Any("user_id", userID))
|
|
}
|
|
if adminID, ok := c.Get(ContextAdminID); ok {
|
|
fields = append(fields, zap.Any("admin_id", adminID))
|
|
}
|
|
if reason, ok := c.Get(ContextAuthFailureReason); ok {
|
|
fields = append(fields, zap.Any("auth_failure_reason", reason))
|
|
}
|
|
if source, ok := c.Get(ContextAuthTokenSource); ok {
|
|
fields = append(fields, zap.Any("auth_token_source", source))
|
|
}
|
|
if detail, ok := c.Get(ContextAuthFailureDetail); ok {
|
|
fields = append(fields, zap.Any("auth_failure_detail", detail))
|
|
}
|
|
if tokenVersion, ok := c.Get(ContextAuthTokenVersion); ok {
|
|
fields = append(fields, zap.Any("auth_token_version", tokenVersion))
|
|
}
|
|
if currentVersion, ok := c.Get(ContextAuthCurrentVersion); ok {
|
|
fields = append(fields, zap.Any("auth_current_token_version", currentVersion))
|
|
}
|
|
if len(c.Errors) > 0 {
|
|
fields = append(fields,
|
|
zap.String("error", c.Errors.Last().Err.Error()),
|
|
zap.Int("error_count", len(c.Errors)),
|
|
)
|
|
}
|
|
if stack, ok := c.Get(contextPanicStack); ok {
|
|
fields = append(fields, zap.Any("stack", stack))
|
|
}
|
|
|
|
switch {
|
|
case status >= 500:
|
|
logger.Error("HTTP 请求失败", fields...)
|
|
case status == http.StatusTooManyRequests:
|
|
logger.Warn("HTTP 请求被限流", fields...)
|
|
case authFailure:
|
|
logger.Warn("后台认证失败", fields...)
|
|
case latencyMs >= slowRequestThresholdMs:
|
|
logger.Warn("HTTP 慢请求", fields...)
|
|
}
|
|
}
|
|
}
|
|
|
|
// shouldSkipHTTPLog 判断是否为无需记录的普通请求。
|
|
func shouldSkipHTTPLog(_, _ string, status int, latencyMs float64) bool {
|
|
if status >= 500 || status == http.StatusTooManyRequests {
|
|
return false
|
|
}
|
|
if latencyMs >= slowRequestThresholdMs {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func meaningfulAuthFailure(c *gin.Context) bool {
|
|
value, ok := c.Get(ContextAuthFailureReason)
|
|
if !ok {
|
|
return false
|
|
}
|
|
reason, _ := value.(string)
|
|
return reason != "" && reason != "missing"
|
|
}
|