package middleware import ( "bytes" "io" "log/slog" "strings" "time" "kra/internal/conf" "github.com/gin-gonic/gin" ) // AccessLog is the single global request/response capture point, matching // the reference middleware ordering and making every HTTP request observable. func AccessLog(runtime *conf.Runtime, logger *slog.Logger) gin.HandlerFunc { return func(c *gin.Context) { var requestBody []byte multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") if c.Request.Body != nil && !multipart { requestBody, _ = io.ReadAll(c.Request.Body) c.Request.Body = io.NopCloser(bytes.NewReader(requestBody)) } maxBytes := 1 << 20 writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes} c.Writer = writer started := time.Now() c.Next() if logger == nil { return } requestText, responseText := "", "" config := runtime.Admin() logLimit := 32768 if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 { logLimit = int(config.Zap.AccessLogMaxBytes) } if config == nil || config.Zap == nil || config.Zap.AccessReqBody { if multipart { requestText = "[文件]" } else { requestText = redactJSONLimit(requestBody, logLimit) } } if config == nil || config.Zap == nil || config.Zap.AccessRespData { responseText = redactJSONLimit(writer.body.Bytes(), logLimit) } userID, authorityID := uint(0), uint(0) if claims := Claims(c); claims != nil { userID, authorityID = claims.ID, claims.AuthorityID } route := c.FullPath() if route == "" { route = "unmatched" } attributes := []any{ "ip", c.ClientIP(), "method", c.Request.Method, "path", c.Request.URL.Path, "http_route", route, "status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(), "request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"), "bytes_in", len(requestBody), "bytes_out", c.Writer.Size(), "user_id", userID, "authority_id", authorityID, "ua", c.Request.UserAgent(), "query", c.Request.URL.RawQuery, "request", requestText, "response", responseText} if config != nil && config.Zap != nil && config.Zap.AccessReqHeaders { attributes = append(attributes, "headers", redactHeaders(c.Request.Header)) } logger.InfoContext(c.Request.Context(), "http access", attributes...) } } func redactHeaders(headers map[string][]string) map[string][]string { out := make(map[string][]string, len(headers)) for key, values := range headers { lower := strings.ToLower(key) if strings.Contains(lower, "token") || lower == "authorization" || lower == "cookie" { out[key] = []string{"******"} } else { out[key] = values } } return out } func stringValueFromContext(c *gin.Context, key string) string { value, _ := c.Get(key) return stringValue(value) }