refactor(middleware): 精简请求日志 — 采用社区标准分层策略

- 移除 Request Body 记录(交由 Controller 层以结构化参数形式记录)
- 移除正常响应 Body 的 DEBUG 记录(减少日志量)
- 保留 HTTP 元信息:method/path/handler/status/latency/ip/query
- 保留错误响应(4xx/5xx)的 Response Body 记录
- 导出 IsSensitivePath 供 Controller 层调用
- 重写架构文档 8.6 节:分层日志架构表、字段说明、分级策略
- 更新 phase1 实施计划中间件描述

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-02-28 16:10:07 +08:00
parent a6ef49a3e0
commit 4d3d486902
3 changed files with 93 additions and 139 deletions

View File

@@ -2,7 +2,6 @@ package middleware
import (
"bytes"
"io"
"strings"
"time"
@@ -12,19 +11,10 @@ import (
)
const (
maxBodyLogSize = 4096 // 请求/响应 Body 记录的最大字节数
maxResponseLogSize = 2048 // 响应 Body 记录的最大字节数
maxResponseLogSize = 2048 // 错误响应 Body 记录的最大字节数
)
// 敏感路径:这些路径的请求 Body 中密码字段需要脱敏
var sensitivePathPrefixes = []string{
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/admin/auth/login",
"/api/v1/auth/password",
}
// responseBodyWriter 包装 gin.ResponseWriter 以捕获响应 Body
// responseBodyWriter 包装 gin.ResponseWriter 以捕获响应 Body仅用于错误响应记录
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
@@ -42,20 +32,22 @@ func (w *responseBodyWriter) Write(b []byte) (int, error) {
return w.ResponseWriter.Write(b)
}
// Logger 请求日志中间件
// 记录每个请求的完整信息方法、路径、请求参数、状态码、耗时、IP、User-Agent
// 请求参数Query + Body在 INFO 级别记录(所有环境生效
// 响应 Body 在 DEBUG 级别记录,错误响应(4xx/5xx)在 INFO 级别也记录
// 自动携带 trace_id慢请求>500ms记录 WARN
// Logger 请求日志中间件Access Log 层)
//
// 职责:记录 HTTP 请求级元信息,不记录请求 Body由 Controller 层以结构化参数形式记录
// 记录字段method / path / handler / status / latency / ip / user_agent / query
// 错误响应4xx/5xx额外记录响应 Body便于排查接口返回内容
//
// 分层日志策略(符合社区最佳实践):
// - 中间件层HTTP 元信息 + 错误响应
// - Controller 层结构化请求参数ShouldBindJSON 后caller 准确指向业务代码)
// - Service/DAO 层:业务逻辑关键节点和异常
// - 通过 trace_id 串联同一请求的所有层级日志
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
// --- 请求阶段:捕获请求参数 ---
query := c.Request.URL.RawQuery
reqBody := readRequestBody(c)
// 包装 ResponseWriter 以捕获响应
rbw := &responseBodyWriter{
ResponseWriter: c.Writer,
body: bytes.NewBufferString(""),
@@ -64,53 +56,34 @@ func Logger() gin.HandlerFunc {
c.Next()
// --- 响应阶段:记录日志 ---
latency := time.Since(start)
ctx := c.Request.Context()
funcName := "middleware.Logger"
status := rbw.Status()
path := c.Request.URL.Path
// handler 名称:如 "main.main.func1" 或 "controller.auth_controller.Login"
handler := c.HandlerName()
fields := []zap.Field{
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("handler", handler),
zap.String("handler", c.HandlerName()),
zap.Int("status", status),
zap.Duration("latency", latency),
zap.String("ip", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
}
// Query 参数(始终记录)
if query != "" {
fields = append(fields, zap.String("query", query))
}
// Request Body始终记录敏感路径脱敏
if reqBody != "" {
if isSensitivePath(path) {
reqBody = maskSensitiveBody(reqBody)
}
fields = append(fields, zap.String("req_body", reqBody))
}
// Response Body错误响应(4xx/5xx)始终记录,正常响应仅 DEBUG
respBody := rbw.body.String()
if respBody != "" {
if status >= 400 {
// 错误响应(4xx/5xx):额外记录响应 Body
if status >= 400 {
respBody := rbw.body.String()
if respBody != "" {
fields = append(fields, zap.String("resp_body", truncate(respBody, maxResponseLogSize)))
} else {
logs.Debug(ctx, funcName, "响应数据",
zap.String("path", path),
zap.String("resp_body", truncate(respBody, maxResponseLogSize)),
)
}
}
// 按状态分级输出
if len(c.Errors) > 0 {
fields = append(fields, zap.String("error", c.Errors.String()))
logs.Error(ctx, funcName, "请求处理异常", fields...)
@@ -136,33 +109,19 @@ func Logger() gin.HandlerFunc {
}
}
// readRequestBody 读取请求 Body读完后重新填回不影响后续 Handler
func readRequestBody(c *gin.Context) string {
if c.Request.Body == nil {
return ""
// truncate 截断字符串到指定长度
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
// 文件上传不记录 Body
contentType := c.GetHeader("Content-Type")
if strings.Contains(contentType, "multipart/form-data") {
return "[file upload]"
}
body, err := io.ReadAll(io.LimitReader(c.Request.Body, maxBodyLogSize+1))
if err != nil {
return "[read error]"
}
// 将 Body 重新填回,供后续 Handler 使用
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
if len(body) > maxBodyLogSize {
return string(body[:maxBodyLogSize]) + "...[truncated]"
}
return string(body)
return s[:maxLen] + "...[truncated]"
}
// isSensitivePath 判断是否为敏感路径(包含密码等字段的接口)
func isSensitivePath(path string) bool {
// --- 以下工具函数保留供 Controller 层使用 ---
// IsSensitivePath 判断是否为敏感路径(包含密码等字段的接口)
// Controller 层记录参数前可调用此函数决定是否脱敏
func IsSensitivePath(path string) bool {
for _, prefix := range sensitivePathPrefixes {
if strings.HasPrefix(path, prefix) {
return true
@@ -171,32 +130,9 @@ func isSensitivePath(path string) bool {
return false
}
// maskSensitiveBody 对敏感 Body 中的密码字段进行脱敏
// 简单策略:将 "password":"xxx" 替换为 "password":"***"
func maskSensitiveBody(body string) string {
// 处理 JSON 中的 password 字段
for _, field := range []string{"password", "old_password", "new_password", "confirm_password"} {
for {
key := `"` + field + `":"`
idx := strings.Index(body, key)
if idx < 0 {
break
}
start := idx + len(key)
end := strings.Index(body[start:], `"`)
if end < 0 {
break
}
body = body[:start] + "***" + body[start+end:]
}
}
return body
}
// truncate 截断字符串到指定长度
func truncate(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen] + "...[truncated]"
var sensitivePathPrefixes = []string{
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/admin/auth/login",
"/api/v1/auth/password",
}