18e4f7e891
- ServerConfig 新增 Host 字段:空=监听所有接口(兼容),设值=绑定指定地址 (127.0.0.1 仅本机 / 内网IP 绑定指定网卡),避免生产暴露 0.0.0.0 - app.go 启动地址改 host:port,日志区分"所有接口"/指定地址 - 面向用户/调用的文案统一中文: - middleware/recover.go: "Panic recovered"→"panic 已恢复","Panic: %v"→"服务器内部错误: %v" - middleware/logger.go: 5 处日志消息改中文 - middleware/metrics.go: 3 个 Prometheus Help 改中文 - app.go/database/manager.go/logger.go: 英文 error 改中文 - 保留英文:JSON 协议字段名、Prometheus metric Name、MySQL 错误串匹配、技术专有名词 - CLI 模板与 example 配置加 host 注释 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
243 lines
6.5 KiB
Go
243 lines
6.5 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/EthanCodeCraft/xlgo-core/logger"
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// LoggerConfig 日志中间件配置
|
|
type LoggerConfig struct {
|
|
// LogRequestBody 是否记录请求体
|
|
LogRequestBody bool
|
|
// LogResponseBody 是否记录响应体
|
|
LogResponseBody bool
|
|
// MaxBodyLength 最大记录的请求/响应体长度(字节)
|
|
MaxBodyLength int
|
|
// SkipPaths 不记录日志的路径
|
|
SkipPaths []string
|
|
// SkipPathPrefixes 不记录日志的路径前缀
|
|
SkipPathPrefixes []string
|
|
// SlowRequestThreshold 慢请求阈值(超过此时间记录警告)
|
|
SlowRequestThreshold time.Duration
|
|
}
|
|
|
|
// DefaultLoggerConfig 默认日志配置
|
|
var DefaultLoggerConfig = LoggerConfig{
|
|
LogRequestBody: false, // 默认不记录请求体(敏感信息风险)
|
|
LogResponseBody: false, // 默认不记录响应体
|
|
MaxBodyLength: 1024, // 最大 1KB
|
|
SkipPaths: []string{"/health", "/swagger"},
|
|
SkipPathPrefixes: []string{"/public", "/static"},
|
|
SlowRequestThreshold: 500 * time.Millisecond,
|
|
}
|
|
|
|
// Logger 日志中间件(使用默认配置)
|
|
func Logger() gin.HandlerFunc {
|
|
return LoggerWithConfig(DefaultLoggerConfig)
|
|
}
|
|
|
|
// LoggerWithConfig 使用自定义配置的日志中间件
|
|
func LoggerWithConfig(cfg LoggerConfig) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// 检查是否跳过此路径
|
|
path := c.Request.URL.Path
|
|
if shouldSkipPath(path, cfg) {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
// 记录请求体(可选)
|
|
var requestBody []byte
|
|
if cfg.LogRequestBody && c.Request.Body != nil {
|
|
requestBody, _ = io.ReadAll(c.Request.Body)
|
|
// 恢复请求体供后续处理
|
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
|
// 限制记录长度
|
|
if len(requestBody) > cfg.MaxBodyLength {
|
|
requestBody = requestBody[:cfg.MaxBodyLength]
|
|
}
|
|
}
|
|
|
|
// 记录响应体(可选)
|
|
var responseBody []byte
|
|
if cfg.LogResponseBody {
|
|
// 使用 ResponseWriter 包装器捕获响应体
|
|
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
|
|
c.Writer = blw
|
|
c.Next()
|
|
responseBody = blw.body.Bytes()
|
|
if len(responseBody) > cfg.MaxBodyLength {
|
|
responseBody = responseBody[:cfg.MaxBodyLength]
|
|
}
|
|
} else {
|
|
c.Next()
|
|
}
|
|
|
|
latency := time.Since(start)
|
|
|
|
// 构建日志字段
|
|
fields := []zap.Field{
|
|
zap.Int("status", c.Writer.Status()),
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("path", path),
|
|
zap.String("query", c.Request.URL.RawQuery),
|
|
zap.String("ip", c.ClientIP()),
|
|
zap.Duration("latency", latency),
|
|
zap.String("user-agent", c.Request.UserAgent()),
|
|
zap.Int("body_size", c.Writer.Size()),
|
|
zap.String("request_id", GetRequestID(c)),
|
|
}
|
|
|
|
// 添加请求体
|
|
if cfg.LogRequestBody && len(requestBody) > 0 {
|
|
// 过滤敏感字段
|
|
safeBody := filterSensitiveFields(requestBody)
|
|
fields = append(fields, zap.String("request_body", safeBody))
|
|
}
|
|
|
|
// 添加响应体
|
|
if cfg.LogResponseBody && len(responseBody) > 0 {
|
|
fields = append(fields, zap.String("response_body", string(responseBody)))
|
|
}
|
|
|
|
// 添加用户信息(如果已登录)
|
|
userID := GetUserID(c)
|
|
if userID > 0 {
|
|
fields = append(fields,
|
|
zap.Uint("user_id", userID),
|
|
zap.String("username", GetUsername(c)),
|
|
zap.String("user_type", GetUserType(c)),
|
|
)
|
|
}
|
|
|
|
// 根据状态码和延迟选择日志级别
|
|
status := c.Writer.Status()
|
|
|
|
if latency > cfg.SlowRequestThreshold {
|
|
// 慢请求警告
|
|
fields = append(fields, zap.Bool("slow_request", true))
|
|
logger.APILog().Warn("慢请求", fields...)
|
|
} else if status >= 500 {
|
|
logger.APILog().Error("请求错误", fields...)
|
|
} else if status >= 400 {
|
|
logger.APILog().Warn("客户端请求错误", fields...)
|
|
} else {
|
|
logger.APILog().Info("API 请求", fields...)
|
|
}
|
|
}
|
|
}
|
|
|
|
// bodyLogWriter 响应体记录包装器
|
|
type bodyLogWriter struct {
|
|
gin.ResponseWriter
|
|
body *bytes.Buffer
|
|
}
|
|
|
|
// Write 捕获响应体
|
|
func (w *bodyLogWriter) Write(b []byte) (int, error) {
|
|
w.body.Write(b)
|
|
return w.ResponseWriter.Write(b)
|
|
}
|
|
|
|
// WriteString 捕获字符串响应
|
|
func (w *bodyLogWriter) WriteString(s string) (int, error) {
|
|
w.body.WriteString(s)
|
|
return w.ResponseWriter.WriteString(s)
|
|
}
|
|
|
|
// shouldSkipPath 检查是否跳过路径
|
|
func shouldSkipPath(path string, cfg LoggerConfig) bool {
|
|
// 检查完整路径
|
|
for _, p := range cfg.SkipPaths {
|
|
if path == p {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// 检查路径前缀
|
|
for _, prefix := range cfg.SkipPathPrefixes {
|
|
if strings.HasPrefix(path, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// filterSensitiveFields 过滤敏感字段(密码、token等)
|
|
func filterSensitiveFields(body []byte) string {
|
|
bodyStr := string(body)
|
|
|
|
// 过滤常见敏感字段(简单字符串替换)
|
|
sensitiveFields := []string{
|
|
"password", "passwd", "pwd",
|
|
"token", "access_token", "refresh_token",
|
|
"secret", "api_key", "apikey",
|
|
"credit_card", "card_number",
|
|
}
|
|
|
|
for _, field := range sensitiveFields {
|
|
// 检查是否包含敏感字段
|
|
keyPattern := `"` + field + `":`
|
|
if strings.Contains(bodyStr, keyPattern) {
|
|
// 简单替换:将值替换为 [FILTERED]
|
|
// 注意:这是一个简化的实现,复杂的 JSON 可能需要更精确的处理
|
|
bodyStr = strings.ReplaceAll(bodyStr, keyPattern, keyPattern+"\"[FILTERED]\"")
|
|
}
|
|
}
|
|
|
|
return bodyStr
|
|
}
|
|
|
|
// LoggerForAPI API 专用日志中间件(更详细)
|
|
func LoggerForAPI() gin.HandlerFunc {
|
|
cfg := DefaultLoggerConfig
|
|
cfg.LogRequestBody = true
|
|
cfg.LogResponseBody = false // 响应体通常较大
|
|
return LoggerWithConfig(cfg)
|
|
}
|
|
|
|
// LoggerForDebug 调试专用日志中间件(最详细)
|
|
func LoggerForDebug() gin.HandlerFunc {
|
|
cfg := LoggerConfig{
|
|
LogRequestBody: true,
|
|
LogResponseBody: true,
|
|
MaxBodyLength: 4096, // 4KB
|
|
SkipPaths: []string{"/health"},
|
|
SkipPathPrefixes: []string{},
|
|
SlowRequestThreshold: 200 * time.Millisecond,
|
|
}
|
|
return LoggerWithConfig(cfg)
|
|
}
|
|
|
|
// LoggerMinimal 最简日志中间件(只记录基本信息)
|
|
func LoggerMinimal() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
path := c.Request.URL.Path
|
|
|
|
// 跳过健康检查和静态资源
|
|
if path == "/health" || strings.HasPrefix(path, "/public") || strings.HasPrefix(path, "/static") {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
start := time.Now()
|
|
c.Next()
|
|
|
|
logger.APILog().Info("请求",
|
|
zap.Int("status", c.Writer.Status()),
|
|
zap.String("method", c.Request.Method),
|
|
zap.String("path", path),
|
|
zap.String("ip", c.ClientIP()),
|
|
zap.Duration("latency", time.Since(start)),
|
|
)
|
|
}
|
|
} |