重构日志与可观测性体系

新增单行文本编码器与结构化 GORM 日志,统一错误记录与请求日志策略,收紧日志文件权限并修复按天切分与压缩,支付回调参数脱敏,生产强制阿里云短信,RequestID 校验防注入,日志文案中文化。
This commit is contained in:
yml2213
2026-07-29 16:19:35 +08:00
parent e75f1a8519
commit 88d74aca7d
64 changed files with 896 additions and 277 deletions
+33 -40
View File
@@ -1,15 +1,17 @@
package middleware
import (
"strings"
"net/http"
"time"
"hfb_sys/backend/pkg/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// 成功且延迟低于该阈值的热路径请求不再写访问日志(错误/慢请求仍全量记录)
const slowRequestThresholdMs = 200
// 正常请求不记录访问日志;只有慢请求、服务端错误、限流和有诊断价值的认证失败会输出
const slowRequestThresholdMs = 500
func RequestLogger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
@@ -20,8 +22,9 @@ func RequestLogger(logger *zap.Logger) gin.HandlerFunc {
status := c.Writer.Status()
path := c.Request.URL.Path
route := c.FullPath()
authFailure := meaningfulAuthFailure(c)
if shouldSkipHTTPLog(path, route, status, latencyMs) {
if shouldSkipHTTPLog(path, route, status, latencyMs) && !authFailure {
return
}
@@ -31,16 +34,9 @@ func RequestLogger(logger *zap.Logger) gin.HandlerFunc {
zap.String("path", path),
zap.String("route", route),
zap.Int("status", status),
zap.Float64("latency_ms", latencyMs),
zap.String("code", response.CodeFromContext(c)),
zap.Float64("duration_ms", latencyMs),
zap.String("client_ip", c.ClientIP()),
zap.Int("response_size", c.Writer.Size()),
}
// 成功响应省略 UA/referer,错误与 4xx/5xx 保留完整现场
if status >= 400 {
fields = append(fields,
zap.String("user_agent", c.GetHeader("User-Agent")),
zap.String("referer", c.GetHeader("Referer")),
)
}
if userID, ok := c.Get(ContextUserID); ok {
fields = append(fields, zap.Any("user_id", userID))
@@ -64,47 +60,44 @@ func RequestLogger(logger *zap.Logger) gin.HandlerFunc {
fields = append(fields, zap.Any("auth_current_token_version", currentVersion))
}
if len(c.Errors) > 0 {
fields = append(fields, zap.String("errors", strings.TrimSpace(c.Errors.String())))
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 request", fields...)
case status >= 400:
logger.Warn("http request", fields...)
default:
logger.Info("http request", fields...)
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(path, route string, status int, latencyMs float64) bool {
if status >= 400 {
// shouldSkipHTTPLog 判断是否为无需记录的普通请求
func shouldSkipHTTPLog(_, _ string, status int, latencyMs float64) bool {
if status >= 500 || status == http.StatusTooManyRequests {
return false
}
if latencyMs >= slowRequestThresholdMs {
return false
}
return isHotPath(path, route)
return true
}
func isHotPath(path, route string) bool {
if strings.HasSuffix(path, "/unread-count") {
return true
func meaningfulAuthFailure(c *gin.Context) bool {
value, ok := c.Get(ContextAuthFailureReason)
if !ok {
return false
}
if path == "/api/wallet/balance" {
return true
}
if path == "/api/mobile-home-config" {
return true
}
if strings.HasSuffix(path, "/cover") || route == "/api/listings/:id/cover" {
return true
}
if path == "/api/files/object" || path == "/api/admin/files/object" ||
route == "/api/files/object" || route == "/api/admin/files/object" {
return true
}
return false
reason, _ := value.(string)
return reason != "" && reason != "missing"
}