2025-12-18 13:50:39 +08:00
|
|
|
|
package middleware
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2026-02-12 16:27:29 +08:00
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
|
|
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
"github.com/gin-gonic/gin"
|
2026-02-12 16:27:29 +08:00
|
|
|
|
"go.uber.org/zap"
|
2025-12-18 13:50:39 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Logger 请求日志中间件
|
|
|
|
|
|
func Logger() gin.HandlerFunc {
|
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
|
// 开始时间
|
|
|
|
|
|
startTime := time.Now()
|
|
|
|
|
|
|
2026-02-07 23:29:24 +08:00
|
|
|
|
// 请求路径
|
|
|
|
|
|
path := c.Request.URL.Path
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
// 处理请求
|
|
|
|
|
|
c.Next()
|
|
|
|
|
|
|
2026-02-07 23:29:24 +08:00
|
|
|
|
// 跳过健康检查等高频探针路径的日志
|
|
|
|
|
|
if path == "/health" || path == "/setup/status" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
endTime := time.Now()
|
|
|
|
|
|
latency := endTime.Sub(startTime)
|
|
|
|
|
|
|
|
|
|
|
|
method := c.Request.Method
|
|
|
|
|
|
statusCode := c.Writer.Status()
|
|
|
|
|
|
clientIP := c.ClientIP()
|
2026-02-04 21:40:25 +08:00
|
|
|
|
protocol := c.Request.Proto
|
2026-02-12 16:27:29 +08:00
|
|
|
|
accountID, hasAccountID := c.Request.Context().Value(ctxkey.AccountID).(int64)
|
|
|
|
|
|
platform, _ := c.Request.Context().Value(ctxkey.Platform).(string)
|
|
|
|
|
|
model, _ := c.Request.Context().Value(ctxkey.Model).(string)
|
|
|
|
|
|
|
|
|
|
|
|
fields := []zap.Field{
|
|
|
|
|
|
zap.String("component", "http.access"),
|
|
|
|
|
|
zap.Int("status_code", statusCode),
|
|
|
|
|
|
zap.Int64("latency_ms", latency.Milliseconds()),
|
|
|
|
|
|
zap.String("client_ip", clientIP),
|
|
|
|
|
|
zap.String("protocol", protocol),
|
|
|
|
|
|
zap.String("method", method),
|
|
|
|
|
|
zap.String("path", path),
|
|
|
|
|
|
}
|
|
|
|
|
|
if hasAccountID && accountID > 0 {
|
|
|
|
|
|
fields = append(fields, zap.Int64("account_id", accountID))
|
|
|
|
|
|
}
|
|
|
|
|
|
if platform != "" {
|
|
|
|
|
|
fields = append(fields, zap.String("platform", platform))
|
|
|
|
|
|
}
|
|
|
|
|
|
if model != "" {
|
|
|
|
|
|
fields = append(fields, zap.String("model", model))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
l := logger.FromContext(c.Request.Context()).With(fields...)
|
|
|
|
|
|
l.Info("http request completed", zap.Time("completed_at", endTime))
|
2026-02-12 19:19:11 +08:00
|
|
|
|
// 当全局日志级别高于 info(如 warn/error)时,access info 不会进入 zap core,
|
|
|
|
|
|
// 这里补写一次 sink,保证 ops 系统日志仍可索引关键访问轨迹。
|
|
|
|
|
|
if !logger.L().Core().Enabled(logger.LevelInfo) {
|
|
|
|
|
|
sinkFields := map[string]any{
|
|
|
|
|
|
"component": "http.access",
|
|
|
|
|
|
"status_code": statusCode,
|
|
|
|
|
|
"latency_ms": latency.Milliseconds(),
|
|
|
|
|
|
"client_ip": clientIP,
|
|
|
|
|
|
"protocol": protocol,
|
|
|
|
|
|
"method": method,
|
|
|
|
|
|
"path": path,
|
|
|
|
|
|
"completed_at": endTime,
|
|
|
|
|
|
}
|
|
|
|
|
|
if requestID, ok := c.Request.Context().Value(ctxkey.RequestID).(string); ok && requestID != "" {
|
|
|
|
|
|
sinkFields["request_id"] = requestID
|
|
|
|
|
|
}
|
|
|
|
|
|
if clientRequestID, ok := c.Request.Context().Value(ctxkey.ClientRequestID).(string); ok && clientRequestID != "" {
|
|
|
|
|
|
sinkFields["client_request_id"] = clientRequestID
|
|
|
|
|
|
}
|
|
|
|
|
|
if hasAccountID && accountID > 0 {
|
|
|
|
|
|
sinkFields["account_id"] = accountID
|
|
|
|
|
|
}
|
|
|
|
|
|
if platform != "" {
|
|
|
|
|
|
sinkFields["platform"] = platform
|
|
|
|
|
|
}
|
|
|
|
|
|
if model != "" {
|
|
|
|
|
|
sinkFields["model"] = model
|
|
|
|
|
|
}
|
|
|
|
|
|
logger.WriteSinkEvent("info", "http.access", "http request completed", sinkFields)
|
|
|
|
|
|
}
|
2026-02-04 21:40:25 +08:00
|
|
|
|
|
2025-12-18 13:50:39 +08:00
|
|
|
|
if len(c.Errors) > 0 {
|
2026-02-12 16:27:29 +08:00
|
|
|
|
l.Warn("http request contains gin errors", zap.String("errors", c.Errors.String()))
|
2025-12-18 13:50:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|