Files
EchoChat/backend/go-service/pkg/middleware/logger.go
bujinyuan a6ef49a3e0 feat(middleware): 请求日志新增 handler 字段定位实际处理函数
- 通过 c.HandlerName() 获取处理请求的 handler 函数名
- 解决 caller 始终指向中间件文件无法定位业务代码的问题
- 同步更新架构文档日志示例,补充 caller/handler 说明

Made-with: Cursor
2026-02-28 15:56:32 +08:00

203 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package middleware
import (
"bytes"
"io"
"strings"
"time"
"github.com/echochat/backend/pkg/logs"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
const (
maxBodyLogSize = 4096 // 请求/响应 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
type responseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w *responseBodyWriter) Write(b []byte) (int, error) {
if w.body.Len() < maxResponseLogSize {
remaining := maxResponseLogSize - w.body.Len()
if len(b) > remaining {
w.body.Write(b[:remaining])
} else {
w.body.Write(b)
}
}
return w.ResponseWriter.Write(b)
}
// Logger 请求日志中间件
// 记录每个请求的完整信息方法、路径、请求参数、状态码、耗时、IP、User-Agent
// 请求参数Query + Body在 INFO 级别记录(所有环境生效)
// 响应 Body 在 DEBUG 级别记录,错误响应(4xx/5xx)在 INFO 级别也记录
// 自动携带 trace_id慢请求>500ms记录 WARN
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(""),
}
c.Writer = rbw
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.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 {
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...)
return
}
if status >= 500 {
logs.Error(ctx, funcName, "服务器错误", fields...)
return
}
if status >= 400 {
logs.Warn(ctx, funcName, "客户端错误", fields...)
return
}
if latency > 500*time.Millisecond {
logs.Warn(ctx, funcName, "慢请求告警", fields...)
return
}
logs.Info(ctx, funcName, "请求完成", fields...)
}
}
// readRequestBody 读取请求 Body读完后重新填回不影响后续 Handler
func readRequestBody(c *gin.Context) string {
if c.Request.Body == nil {
return ""
}
// 文件上传不记录 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)
}
// isSensitivePath 判断是否为敏感路径(包含密码等字段的接口)
func isSensitivePath(path string) bool {
for _, prefix := range sensitivePathPrefixes {
if strings.HasPrefix(path, prefix) {
return true
}
}
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]"
}