254 lines
6.1 KiB
Go
254 lines
6.1 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// LogResponseWriter 自定义ResponseWriter以捕获响应内容
|
|
type LogResponseWriter struct {
|
|
gin.ResponseWriter
|
|
body *bytes.Buffer
|
|
}
|
|
|
|
func (w *LogResponseWriter) Write(b []byte) (int, error) {
|
|
if w.body != nil {
|
|
w.body.Write(b)
|
|
}
|
|
return w.ResponseWriter.Write(b)
|
|
}
|
|
|
|
func (w *LogResponseWriter) WriteString(s string) (int, error) {
|
|
if w.body != nil {
|
|
w.body.WriteString(s)
|
|
}
|
|
return w.ResponseWriter.WriteString(s)
|
|
}
|
|
|
|
// Logger 请求日志中间件
|
|
func Logger(logger *zap.Logger) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// 开始时间
|
|
startTime := time.Now()
|
|
|
|
// 请求方法
|
|
httpMethod := c.Request.Method
|
|
|
|
// 请求路径
|
|
reqUri := c.Request.RequestURI
|
|
|
|
// 客户端IP
|
|
clientIP := c.ClientIP()
|
|
|
|
// 用户代理
|
|
userAgent := c.Request.UserAgent()
|
|
|
|
// 请求ID
|
|
requestID := c.GetHeader("X-Request-ID")
|
|
if requestID == "" {
|
|
requestID = generateRequestID()
|
|
c.Set("requestID", requestID)
|
|
} else {
|
|
c.Set("requestID", requestID)
|
|
}
|
|
|
|
// 记录原始请求体(如果不是文件上传等大请求)
|
|
var requestBody []byte
|
|
if c.Request.ContentLength > 0 && c.Request.ContentLength < 1024*1024 && // 1MB限制
|
|
c.Request.Header.Get("Content-Type") != "multipart/form-data" {
|
|
// 读取请求体
|
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
|
if err == nil {
|
|
requestBody = bodyBytes
|
|
// 重置请求体以便后续使用
|
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
|
|
|
// 尝试解析JSON
|
|
var jsonBody interface{}
|
|
if err := json.Unmarshal(bodyBytes, &jsonBody); err == nil {
|
|
// 敏感信息过滤(如密码)
|
|
if m, ok := jsonBody.(map[string]interface{}); ok {
|
|
if _, exists := m["password"]; exists {
|
|
m["password"] = "***REDACTED***"
|
|
}
|
|
if _, exists := m["oldPassword"]; exists {
|
|
m["oldPassword"] = "***REDACTED***"
|
|
}
|
|
if _, exists := m["newPassword"]; exists {
|
|
m["newPassword"] = "***REDACTED***"
|
|
}
|
|
if _, exists := m["confirmPassword"]; exists {
|
|
m["confirmPassword"] = "***REDACTED***"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 包装ResponseWriter以捕获响应
|
|
blw := &LogResponseWriter{
|
|
ResponseWriter: c.Writer,
|
|
body: bytes.NewBufferString(""),
|
|
}
|
|
c.Writer = blw
|
|
|
|
// 处理请求
|
|
c.Next()
|
|
|
|
// 结束时间
|
|
endTime := time.Now()
|
|
|
|
// 执行时间
|
|
latency := endTime.Sub(startTime)
|
|
|
|
// 响应状态码
|
|
statusCode := c.Writer.Status()
|
|
|
|
// 错误信息
|
|
errors := c.Errors.ByType(gin.ErrorTypePrivate).String()
|
|
if errors == "" {
|
|
errors = c.Errors.ByType(gin.ErrorTypePublic).String()
|
|
}
|
|
|
|
// 响应体(如果不是文件等大型响应)
|
|
var responseBody interface{}
|
|
var responseMap map[string]interface{}
|
|
if blw.body != nil && blw.body.Len() > 0 && blw.body.Len() < 10000 { // 10KB限制
|
|
bodyBytes := blw.body.Bytes()
|
|
if err := json.Unmarshal(bodyBytes, &responseMap); err == nil {
|
|
responseBody = responseMap
|
|
} else {
|
|
responseBody = string(bodyBytes)
|
|
}
|
|
}
|
|
|
|
// 根据状态码决定日志级别
|
|
fields := []zap.Field{
|
|
zap.String("request_id", requestID),
|
|
zap.String("method", httpMethod),
|
|
zap.String("uri", reqUri),
|
|
zap.String("client_ip", clientIP),
|
|
zap.String("user_agent", userAgent),
|
|
zap.Int("status", statusCode),
|
|
zap.Duration("latency", latency),
|
|
}
|
|
|
|
// 添加请求体(如果存在且不是太大)
|
|
if len(requestBody) > 0 && len(requestBody) < 10000 {
|
|
var reqBody interface{}
|
|
if err := json.Unmarshal(requestBody, &reqBody); err == nil {
|
|
fields = append(fields, zap.Any("request_body", reqBody))
|
|
}
|
|
}
|
|
|
|
// 添加响应体(如果存在且不是太大)
|
|
if responseBody != nil {
|
|
fields = append(fields, zap.Any("response_body", responseBody))
|
|
}
|
|
|
|
// 添加错误信息
|
|
if errors != "" {
|
|
fields = append(fields, zap.String("error", errors))
|
|
}
|
|
|
|
// 获取用户标识(如果有)
|
|
if cookieValue := GetCookieValue(c); cookieValue != "" {
|
|
fields = append(fields, zap.String("auth_cookie_truncated", truncateString(cookieValue, 8)))
|
|
}
|
|
if authToken := GetAuthToken(c); authToken != "" {
|
|
fields = append(fields, zap.String("auth_token_truncated", truncateString(authToken, 8)))
|
|
}
|
|
|
|
// 记录日志
|
|
logFunc := logger.Info
|
|
if statusCode >= 400 && statusCode < 500 {
|
|
logFunc = logger.Warn
|
|
} else if statusCode >= 500 {
|
|
logFunc = logger.Error
|
|
}
|
|
|
|
logFunc("HTTP request", fields...)
|
|
}
|
|
}
|
|
|
|
// Recovery 恢复中间件
|
|
func Recovery(logger *zap.Logger) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
defer func() {
|
|
if err := recover(); err != nil {
|
|
// 获取请求ID
|
|
requestID, _ := c.Get("requestID")
|
|
|
|
// 记录Panic
|
|
logger.Error("HTTP panic recovered",
|
|
zap.Any("error", err),
|
|
zap.String("request_id", requestID.(string)),
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("uri", c.Request.RequestURI),
|
|
zap.String("client_ip", c.ClientIP()),
|
|
)
|
|
|
|
// 返回500错误
|
|
c.JSON(500, gin.H{
|
|
"code": "500",
|
|
"message": "Internal server error",
|
|
"data": nil,
|
|
})
|
|
|
|
c.Abort()
|
|
}
|
|
}()
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// 辅助函数
|
|
func generateRequestID() string {
|
|
return time.Now().Format("20060102150405") + "-" + shortRandString()
|
|
}
|
|
|
|
func truncateString(s string, length int) string {
|
|
if len(s) <= length {
|
|
return s
|
|
}
|
|
return s[:length] + "..."
|
|
}
|
|
|
|
func shortRandString() string {
|
|
// 简化的随机字符串生成
|
|
return time.Now().Format("150405")
|
|
}
|
|
|
|
// SimpleLogger 简易日志中间件(用于开发和测试)
|
|
func SimpleLogger() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
start := time.Now()
|
|
|
|
// 处理请求
|
|
c.Next()
|
|
|
|
// 记录请求信息
|
|
latency := time.Since(start)
|
|
clientIP := c.ClientIP()
|
|
method := c.Request.Method
|
|
statusCode := c.Writer.Status()
|
|
path := c.Request.URL.Path
|
|
|
|
// 输出到控制台
|
|
fmt.Printf("[GIN] %v | %3d | %13v | %15s | %-7s %s\n",
|
|
time.Now().Format("2006/01/02 - 15:04:05"),
|
|
statusCode,
|
|
latency,
|
|
clientIP,
|
|
method,
|
|
path,
|
|
)
|
|
}
|
|
} |