Răsfoiți Sursa

feat(log): 优化请求日志记录功能

- 添加对 multipart/form-data 请求的处理
- 改进 JSON 请求体和响应体的记录方式
- 移除敏感字段脱敏功能
- 优化日志输出格式
fusu 2 luni în urmă
părinte
comite
2b4d823bca
1 a modificat fișierele cu 140 adăugiri și 107 ștergeri
  1. 140 107
      internal/middleware/log.go

+ 140 - 107
internal/middleware/log.go

@@ -2,8 +2,10 @@ package middleware
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	"io"
+	"mime/multipart"
 	"strings"
 	"time"
 
@@ -36,14 +38,6 @@ var (
 		"user-agent",
 		"content-type",
 	}
-
-	// 敏感字段
-	sensitiveFields = []string{
-		"password", "passwd", "pwd",
-		"token", "access_token", "refresh_token",
-		"secret", "api_key", "apikey",
-		"authorization",
-	}
 )
 
 func RequestLogMiddleware(logger *log.Logger) gin.HandlerFunc {
@@ -78,8 +72,23 @@ func RequestLogMiddleware(logger *log.Logger) gin.HandlerFunc {
 
 		// 记录请求体(仅限特定方法)
 		if shouldLogRequestBody(ctx) {
-			if bodyLog := getRequestBody(ctx); bodyLog != "" {
-				fields = append(fields, zap.String("body", bodyLog))
+			contentType := ctx.GetHeader("Content-Type")
+
+			// 特殊处理 multipart/form-data
+			if strings.Contains(contentType, "multipart/form-data") {
+				if formData := parseMultipartData(ctx); formData != nil {
+					fields = append(fields, zap.Any("form_data", formData))
+				}
+			} else if strings.Contains(contentType, "application/json") {
+				// 处理 JSON 请求体
+				if bodyData := getJSONBody(ctx); bodyData != nil {
+					fields = append(fields, zap.Any("body", bodyData))
+				}
+			} else {
+				// 处理其他类型的请求体
+				if bodyLog := getRequestBody(ctx); bodyLog != "" {
+					fields = append(fields, zap.String("body", bodyLog))
+				}
 			}
 		}
 
@@ -133,7 +142,17 @@ func ResponseLogMiddleware(logger *log.Logger) gin.HandlerFunc {
 			if len(bodyStr) > MaxBodySize {
 				fields = append(fields, zap.String("body", fmt.Sprintf("[TRUNCATED: %d bytes]", len(bodyStr))))
 			} else if len(bodyStr) > 0 {
-				fields = append(fields, zap.String("body", maskSensitiveData(bodyStr)))
+				// 尝试解析 JSON 响应
+				if json.Valid([]byte(bodyStr)) {
+					var jsonData interface{}
+					if err := json.Unmarshal([]byte(bodyStr), &jsonData); err == nil {
+						fields = append(fields, zap.Any("body", jsonData))
+					} else {
+						fields = append(fields, zap.String("body", bodyStr))
+					}
+				} else {
+					fields = append(fields, zap.String("body", bodyStr))
+				}
 			}
 		}
 
@@ -157,6 +176,115 @@ func ResponseLogMiddleware(logger *log.Logger) gin.HandlerFunc {
 	}
 }
 
+// 获取 JSON 请求体并解析为 map
+func getJSONBody(ctx *gin.Context) interface{} {
+	if ctx.Request.Body == nil {
+		return nil
+	}
+
+	bodyBytes, err := ctx.GetRawData()
+	if err != nil {
+		return nil
+	}
+
+	// 重置请求体
+	ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
+
+	// 检查大小
+	if len(bodyBytes) == 0 {
+		return nil
+	}
+	if len(bodyBytes) > MaxBodySize {
+		return fmt.Sprintf("[TRUNCATED: %d bytes]", len(bodyBytes))
+	}
+
+	// 解析 JSON
+	var data interface{}
+	if err := json.Unmarshal(bodyBytes, &data); err != nil {
+		// 如果解析失败,返回原始字符串
+		return string(bodyBytes)
+	}
+
+	return data
+}
+
+// 解析 multipart/form-data
+func parseMultipartData(ctx *gin.Context) map[string]interface{} {
+	// 保存原始请求体
+	bodyBytes, err := ctx.GetRawData()
+	if err != nil {
+		return nil
+	}
+
+	// 重置请求体
+	ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
+
+	// 创建新的 reader
+	reader := multipart.NewReader(bytes.NewReader(bodyBytes), extractBoundary(ctx.GetHeader("Content-Type")))
+	if reader == nil {
+		return nil
+	}
+
+	formData := make(map[string]interface{})
+
+	for {
+		part, err := reader.NextPart()
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return nil
+		}
+
+		name := part.FormName()
+		if name == "" {
+			continue
+		}
+
+		// 读取内容
+		value, err := io.ReadAll(part)
+		if err != nil {
+			continue
+		}
+
+		valueStr := string(value)
+
+		// 尝试解析为 JSON
+		if json.Valid(value) {
+			var jsonData interface{}
+			if err := json.Unmarshal(value, &jsonData); err == nil {
+				formData[name] = jsonData
+			} else {
+				formData[name] = valueStr
+			}
+		} else {
+			formData[name] = valueStr
+		}
+
+		part.Close()
+	}
+
+	return formData
+}
+
+// 提取 boundary
+func extractBoundary(contentType string) string {
+	if !strings.Contains(contentType, "boundary=") {
+		return ""
+	}
+
+	parts := strings.Split(contentType, "boundary=")
+	if len(parts) < 2 {
+		return ""
+	}
+
+	boundary := parts[1]
+	// 移除可能的引号
+	boundary = strings.Trim(boundary, `"`)
+
+	return boundary
+}
+
 // bodyLogWriter 包装响应写入器以捕获响应体
 type bodyLogWriter struct {
 	gin.ResponseWriter
@@ -219,100 +347,5 @@ func getRequestBody(ctx *gin.Context) string {
 		return fmt.Sprintf("[TRUNCATED: %d bytes]", len(bodyBytes))
 	}
 
-	// 脱敏处理
-	return maskSensitiveData(string(bodyBytes))
-}
-
-func maskSensitiveData(data string) string {
-	result := data
-	for _, field := range sensitiveFields {
-		// 简单的JSON字段脱敏
-		result = maskJSONField(result, field)
-		// URL参数脱敏
-		result = maskURLParam(result, field)
-	}
-	return result
-}
-
-func maskJSONField(data, field string) string {
-	lowerData := strings.ToLower(data)
-	lowerField := strings.ToLower(field)
-
-	// 查找字段位置(不区分大小写)
-	idx := strings.Index(lowerData, `"`+lowerField+`"`)
-	if idx == -1 {
-		idx = strings.Index(lowerData, `'`+lowerField+`'`)
-		if idx == -1 {
-			return data
-		}
-	}
-
-	// 找到冒号位置
-	colonIdx := strings.Index(data[idx:], ":")
-	if colonIdx == -1 {
-		return data
-	}
-	colonIdx += idx
-
-	// 找到值的开始和结束位置
-	valueStart := colonIdx + 1
-	for valueStart < len(data) && (data[valueStart] == ' ' || data[valueStart] == '\t') {
-		valueStart++
-	}
-
-	if valueStart >= len(data) {
-		return data
-	}
-
-	// 判断值的类型
-	var valueEnd int
-	if data[valueStart] == '"' || data[valueStart] == '\'' {
-		// 字符串值
-		quote := data[valueStart]
-		valueEnd = valueStart + 1
-		for valueEnd < len(data) && data[valueEnd] != quote {
-			if data[valueEnd] == '\\' {
-				valueEnd++ // 跳过转义字符
-			}
-			valueEnd++
-		}
-		if valueEnd < len(data) {
-			valueEnd++ // 包含结束引号
-		}
-	} else {
-		// 非字符串值(数字、布尔值等)
-		valueEnd = valueStart
-		for valueEnd < len(data) && data[valueEnd] != ',' && data[valueEnd] != '}' && data[valueEnd] != ']' && data[valueEnd] != '\n' && data[valueEnd] != '\r' {
-			valueEnd++
-		}
-	}
-
-	// 替换为脱敏值
-	return data[:valueStart] + `"***"` + data[valueEnd:]
-}
-
-func maskURLParam(data, param string) string {
-	lowerData := strings.ToLower(data)
-	lowerParam := strings.ToLower(param)
-
-	// 查找参数位置
-	idx := strings.Index(lowerData, lowerParam+"=")
-	if idx == -1 {
-		return data
-	}
-
-	// 确保是参数开始位置(前面是?或&)
-	if idx > 0 && data[idx-1] != '?' && data[idx-1] != '&' && data[idx-1] != ' ' && data[idx-1] != '\n' {
-		return data
-	}
-
-	// 找到参数值结束位置
-	valueStart := idx + len(param) + 1
-	valueEnd := valueStart
-	for valueEnd < len(data) && data[valueEnd] != '&' && data[valueEnd] != ' ' && data[valueEnd] != '\n' && data[valueEnd] != '\r' {
-		valueEnd++
-	}
-
-	// 替换为脱敏值
-	return data[:valueStart] + "***" + data[valueEnd:]
+	return string(bodyBytes)
 }