补齐相关日志日志
This commit is contained in:
@@ -2,6 +2,7 @@ package middleware
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -30,6 +31,17 @@ func sanitizeQuery(query string) string {
|
||||
return values.Encode()
|
||||
}
|
||||
|
||||
// maxUserAgentLen user_agent 日志字段最大长度:超出截断,避免异常长 UA 撑大日志体积。
|
||||
const maxUserAgentLen = 256
|
||||
|
||||
// truncateUserAgent 截断超长 User-Agent(仅用于日志展示)。
|
||||
func truncateUserAgent(ua string) string {
|
||||
if len(ua) <= maxUserAgentLen {
|
||||
return ua
|
||||
}
|
||||
return ua[:maxUserAgentLen] + "...(truncated)"
|
||||
}
|
||||
|
||||
func GinSlogMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
@@ -40,16 +52,26 @@ func GinSlogMiddleware() gin.HandlerFunc {
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 请求结束后打日志
|
||||
slog.InfoContext(c.Request.Context(), "GIN_REQUEST",
|
||||
"request_id", GetRequestID(c.Request.Context()),
|
||||
// 请求结束后按状态码分级打日志:5xx=Error、4xx(除 401)=Warn、其余=Info。
|
||||
ctx := c.Request.Context()
|
||||
status := c.Writer.Status()
|
||||
attrs := []any{
|
||||
"request_id", GetRequestID(ctx),
|
||||
"method", c.Request.Method,
|
||||
"path", path,
|
||||
"query", query,
|
||||
"status", c.Writer.Status(),
|
||||
"status", status,
|
||||
"ip", c.ClientIP(),
|
||||
"latency", time.Since(start).String(),
|
||||
"user_agent", c.Request.UserAgent(),
|
||||
)
|
||||
"user_agent", truncateUserAgent(c.Request.UserAgent()),
|
||||
}
|
||||
switch {
|
||||
case status >= http.StatusInternalServerError:
|
||||
slog.ErrorContext(ctx, "GIN_REQUEST", attrs...)
|
||||
case status >= http.StatusBadRequest && status != http.StatusUnauthorized:
|
||||
slog.WarnContext(ctx, "GIN_REQUEST", attrs...)
|
||||
default:
|
||||
slog.InfoContext(ctx, "GIN_REQUEST", attrs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ func JWTAuth() gin.HandlerFunc {
|
||||
// 1. 从 Authorization 头取 token:Bearer xxx.yyy.zzz
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
slog.WarnContext(c.Request.Context(), "jwt auth: authorization header missing",
|
||||
"method", c.Request.Method, "path", c.Request.URL.Path, "ip", c.ClientIP())
|
||||
response.ToErrorResponse(errcode.UnauthorizedTokenMissing)
|
||||
c.Abort()
|
||||
return
|
||||
@@ -35,6 +37,8 @@ func JWTAuth() gin.HandlerFunc {
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
slog.WarnContext(c.Request.Context(), "jwt auth: invalid authorization format",
|
||||
"method", c.Request.Method, "path", c.Request.URL.Path, "ip", c.ClientIP())
|
||||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("invalid Authorization format"))
|
||||
c.Abort()
|
||||
return
|
||||
@@ -45,10 +49,14 @@ func JWTAuth() gin.HandlerFunc {
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, lzcwyjwt.ErrTokenExpired):
|
||||
slog.InfoContext(c.Request.Context(), "jwt auth: token expired", "path", c.Request.URL.Path)
|
||||
response.ToErrorResponse(errcode.UnauthorizedTokenTimeout)
|
||||
case errors.Is(err, lzcwyjwt.ErrTokenWrongType):
|
||||
slog.WarnContext(c.Request.Context(), "jwt auth: token type mismatch", "path", c.Request.URL.Path)
|
||||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("token type mismatch"))
|
||||
default:
|
||||
slog.WarnContext(c.Request.Context(), "jwt auth: parse token failed",
|
||||
"path", c.Request.URL.Path, "ip", c.ClientIP(), "error", err)
|
||||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails(err.Error()))
|
||||
}
|
||||
c.Abort()
|
||||
@@ -59,6 +67,8 @@ func JWTAuth() gin.HandlerFunc {
|
||||
sessionUID, serr := redisdao.AuthUserIDByAccess(c.Request.Context(), parts[1])
|
||||
if serr != nil {
|
||||
if errors.Is(serr, redis.Nil) {
|
||||
slog.WarnContext(c.Request.Context(), "jwt auth: token session invalid",
|
||||
"user_id", claims.UserID, "path", c.Request.URL.Path)
|
||||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("token session invalid"))
|
||||
c.Abort()
|
||||
return
|
||||
@@ -67,8 +77,12 @@ func JWTAuth() gin.HandlerFunc {
|
||||
// 已确认的安全权衡决策——优先保证可用性,避免 Redis 抖动导致全员不可用。
|
||||
// 代价是 Redis 故障窗口内,已登出/被吊销但未过期的 token 仍可访问;
|
||||
// 封禁用户仍会被下方 DB 回查(Status != 1)拒绝,不会因本降级放行而绕过封禁。
|
||||
slog.WarnContext(c.Request.Context(), "check access token session failed, allow by fallback", "error", serr)
|
||||
slog.WarnContext(c.Request.Context(), "check access token session failed, allow by fallback",
|
||||
"user_id", claims.UserID, "path", c.Request.URL.Path, "ip", c.ClientIP(), "error", serr)
|
||||
} else if sessionUID != claims.UserID {
|
||||
slog.ErrorContext(c.Request.Context(), "jwt auth: token user mismatch",
|
||||
"user_id", claims.UserID, "session_user_id", sessionUID,
|
||||
"path", c.Request.URL.Path, "ip", c.ClientIP())
|
||||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("token user mismatch"))
|
||||
c.Abort()
|
||||
return
|
||||
@@ -78,21 +92,29 @@ func JWTAuth() gin.HandlerFunc {
|
||||
userInfo, err := redisdao.UserInfoGet(c.Request.Context(), claims.UserID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, redis.Nil) {
|
||||
slog.WarnContext(c.Request.Context(), "get user info from cache failed, fallback to db", "error", err)
|
||||
slog.WarnContext(c.Request.Context(), "get user info from cache failed, fallback to db",
|
||||
"user_id", claims.UserID, "error", err)
|
||||
}
|
||||
// 缓存未命中或 Redis 抖动:回落数据库。
|
||||
dbUser, dbErr := mysqldao.NewUserDao().UserGetByID(c.Request.Context(), claims.UserID)
|
||||
if dbErr != nil {
|
||||
slog.WarnContext(c.Request.Context(), "jwt auth: query user failed",
|
||||
"user_id", claims.UserID, "path", c.Request.URL.Path, "error", dbErr)
|
||||
response.ToErrorResponse(errcode.UnauthorizedTokenError.WithDetails("用户不存在"))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
userInfo = dbUser
|
||||
_ = redisdao.UserInfoSet(c.Request.Context(), dbUser, 0)
|
||||
if cerr := redisdao.UserInfoSet(c.Request.Context(), dbUser, 0); cerr != nil {
|
||||
slog.DebugContext(c.Request.Context(), "jwt auth: backfill user cache failed",
|
||||
"user_id", claims.UserID, "error", cerr)
|
||||
}
|
||||
}
|
||||
|
||||
// 状态非正常(1=正常)一律拒绝
|
||||
if userInfo.Status != 1 {
|
||||
slog.WarnContext(c.Request.Context(), "jwt auth: user disabled",
|
||||
"user_id", claims.UserID, "status", userInfo.Status, "path", c.Request.URL.Path)
|
||||
response.ToErrorResponse(errcode.UserDisabled)
|
||||
c.Abort()
|
||||
return
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Recovery 替代 gin.Recovery():panic 恢复语义保持一致(客户端断开类 panic 仅记录并中止,
|
||||
// 其余 panic 返回 500 Internal Server Error),但 panic 与堆栈统一走 slog 结构化输出,
|
||||
// 与全站 JSON 日志(含 request_id / trace_id)对齐,避免默认实现写 stderr 非结构化日志。
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ctx := c.Request.Context()
|
||||
if isBrokenPipe(r) {
|
||||
// 连接已断开,无法再写响应:仅记录并中止。
|
||||
slog.ErrorContext(ctx, "GIN_PANIC_BROKEN_PIPE",
|
||||
"method", c.Request.Method, "path", c.Request.URL.Path, "recover", fmt.Sprint(r))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
slog.ErrorContext(ctx, "GIN_PANIC",
|
||||
"method", c.Request.Method, "path", c.Request.URL.Path, "ip", c.ClientIP(),
|
||||
"recover", fmt.Sprint(r), "stack", string(debug.Stack()))
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// isBrokenPipe 判定 panic 是否源于客户端断开(broken pipe / connection reset),与 gin.Recovery 判定一致。
|
||||
func isBrokenPipe(r any) bool {
|
||||
ne, ok := r.(*net.OpError)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
var se *os.SyscallError
|
||||
if !errors.As(ne, &se) {
|
||||
return false
|
||||
}
|
||||
seStr := strings.ToLower(se.Error())
|
||||
return strings.Contains(seStr, "broken pipe") || strings.Contains(seStr, "connection reset by peer")
|
||||
}
|
||||
Reference in New Issue
Block a user