package middleware import ( "net/url" "regexp" "strings" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" "github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/response" ) func Logger(logger *zap.Logger) gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() c.Next() fields := []zap.Field{ 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()), } if route := c.FullPath(); route != "" { fields = append(fields, zap.String("route", route)) } if rawQuery := c.Request.URL.RawQuery; rawQuery != "" { fields = append(fields, zap.String("query", redactRawQuery(rawQuery))) } 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)) } 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)) } if appErr.Cause != nil { fields = append(fields, zap.Error(appErr.Cause)) } } else if len(c.Errors) > 0 { fields = append(fields, zap.String("error", c.Errors.String())) } 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)) } } switch status := c.Writer.Status(); { case status >= 500: logger.Error("request", fields...) case status >= 400: logger.Warn("request", fields...) default: logger.Info("request", fields...) } } } 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, "&") }