2026-04-01 00:58:42 +08:00
|
|
|
package middleware
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-14 11:05:20 +08:00
|
|
|
"net/url"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strings"
|
2026-04-01 00:58:42 +08:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"go.uber.org/zap"
|
2026-04-05 17:14:13 +08:00
|
|
|
|
2026-04-18 17:16:57 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
2026-04-05 17:14:13 +08:00
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
2026-04-01 00:58:42 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func Logger(logger *zap.Logger) gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
start := time.Now()
|
|
|
|
|
c.Next()
|
2026-04-05 17:14:13 +08:00
|
|
|
|
|
|
|
|
fields := []zap.Field{
|
2026-04-01 00:58:42 +08:00
|
|
|
zap.String("method", c.Request.Method),
|
|
|
|
|
zap.String("path", c.Request.URL.Path),
|
|
|
|
|
zap.Int("status", c.Writer.Status()),
|
|
|
|
|
zap.Duration("latency", time.Since(start)),
|
|
|
|
|
zap.String("request_id", RequestIDFromGin(c)),
|
|
|
|
|
zap.String("client_ip", c.ClientIP()),
|
2026-04-05 17:14:13 +08:00
|
|
|
}
|
2026-04-18 17:16:57 +08:00
|
|
|
if route := c.FullPath(); route != "" {
|
|
|
|
|
fields = append(fields, zap.String("route", route))
|
|
|
|
|
}
|
|
|
|
|
if rawQuery := c.Request.URL.RawQuery; rawQuery != "" {
|
2026-05-14 11:05:20 +08:00
|
|
|
fields = append(fields, zap.String("query", redactRawQuery(rawQuery)))
|
2026-04-18 17:16:57 +08:00
|
|
|
}
|
|
|
|
|
if userAgent := c.Request.UserAgent(); userAgent != "" {
|
|
|
|
|
fields = append(fields, zap.String("user_agent", userAgent))
|
|
|
|
|
}
|
|
|
|
|
if referer := c.Request.Referer(); referer != "" {
|
|
|
|
|
fields = append(fields, zap.String("referer", referer))
|
|
|
|
|
}
|
2026-04-05 17:14:13 +08:00
|
|
|
|
|
|
|
|
if appErr, ok := response.AppErrorFromGin(c); ok {
|
|
|
|
|
fields = append(fields,
|
|
|
|
|
zap.Int("app_code", appErr.Code),
|
|
|
|
|
zap.String("app_message", appErr.Message),
|
|
|
|
|
)
|
|
|
|
|
if appErr.Detail != "" {
|
|
|
|
|
fields = append(fields, zap.String("app_detail", appErr.Detail))
|
|
|
|
|
}
|
2026-04-12 09:56:18 +08:00
|
|
|
if appErr.Cause != nil {
|
|
|
|
|
fields = append(fields, zap.Error(appErr.Cause))
|
|
|
|
|
}
|
2026-04-05 17:14:13 +08:00
|
|
|
} else if len(c.Errors) > 0 {
|
|
|
|
|
fields = append(fields, zap.String("error", c.Errors.String()))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 17:16:57 +08:00
|
|
|
if authCtx, ok := auth.RequestLogContextFromGin(c); ok {
|
|
|
|
|
fields = append(fields, zap.Bool("auth_header_present", authCtx.HeaderPresent))
|
|
|
|
|
if authCtx.Scheme != "" {
|
|
|
|
|
fields = append(fields, zap.String("auth_scheme", authCtx.Scheme))
|
|
|
|
|
}
|
|
|
|
|
if authCtx.TokenFingerprint != "" {
|
|
|
|
|
fields = append(fields, zap.String("auth_token_fingerprint", authCtx.TokenFingerprint))
|
|
|
|
|
}
|
|
|
|
|
if authCtx.FailureStage != "" {
|
|
|
|
|
fields = append(fields, zap.String("auth_failure_stage", authCtx.FailureStage))
|
|
|
|
|
}
|
|
|
|
|
if authCtx.FailureReason != "" {
|
|
|
|
|
fields = append(fields, zap.String("auth_failure_reason", authCtx.FailureReason))
|
|
|
|
|
}
|
|
|
|
|
if authCtx.ParseError != "" {
|
|
|
|
|
fields = append(fields, zap.String("auth_parse_error", authCtx.ParseError))
|
|
|
|
|
}
|
|
|
|
|
if authCtx.TokenSubject != "" {
|
|
|
|
|
fields = append(fields, zap.String("auth_token_subject", authCtx.TokenSubject))
|
|
|
|
|
}
|
|
|
|
|
if authCtx.TokenExpiresAt != "" {
|
|
|
|
|
fields = append(fields, zap.String("auth_token_expires_at", authCtx.TokenExpiresAt))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 17:14:13 +08:00
|
|
|
switch status := c.Writer.Status(); {
|
|
|
|
|
case status >= 500:
|
|
|
|
|
logger.Error("request", fields...)
|
|
|
|
|
case status >= 400:
|
|
|
|
|
logger.Warn("request", fields...)
|
|
|
|
|
default:
|
|
|
|
|
logger.Info("request", fields...)
|
|
|
|
|
}
|
2026-04-01 00:58:42 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-14 11:05:20 +08:00
|
|
|
|
|
|
|
|
var sensitiveQueryKeyPattern = regexp.MustCompile(`(?i)(password|passwd|pwd|token|secret|authorization|credential|session|cookie|key|signature|sig)`)
|
|
|
|
|
|
|
|
|
|
func redactRawQuery(raw string) string {
|
|
|
|
|
values, err := url.ParseQuery(raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return redactQueryFallback(raw)
|
|
|
|
|
}
|
|
|
|
|
for key := range values {
|
|
|
|
|
if sensitiveQueryKeyPattern.MatchString(key) {
|
|
|
|
|
values[key] = []string{"[REDACTED]"}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return values.Encode()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func redactQueryFallback(raw string) string {
|
|
|
|
|
parts := strings.Split(raw, "&")
|
|
|
|
|
for i, part := range parts {
|
|
|
|
|
key, _, ok := strings.Cut(part, "=")
|
|
|
|
|
if ok && sensitiveQueryKeyPattern.MatchString(key) {
|
|
|
|
|
parts[i] = key + "=[REDACTED]"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(parts, "&")
|
|
|
|
|
}
|