package middleware import ( "bytes" "io" "log/slog" "net/http" "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, version string) gin.HandlerFunc { return func(c *gin.Context) { started := time.Now() var requestBody []byte multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") bytesIn := c.Request.ContentLength if c.Request.Body != nil && !multipart { requestBody, _ = io.ReadAll(c.Request.Body) c.Request.Body = io.NopCloser(bytes.NewReader(requestBody)) bytesIn = int64(len(requestBody)) } if bytesIn < 0 { bytesIn = 0 } maxBytes := 1 << 20 writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes} c.Writer = writer c.Header("X-Kra-Version", version) config := runtime.Admin() logLimit := 1024 if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 { logLimit = int(config.Zap.AccessLogMaxBytes) } requestText := "" if multipart { requestText = "[文件]" } else { requestText = redactJSON(requestBody, c.GetHeader("Content-Type"), logLimit) } c.Set(ctxReqBodyKey, requestText) c.Set(ctxRespBufferKey, &writer.body) c.Next() if logger == nil { return } responseText := redactJSON(writer.body.Bytes(), c.Writer.Header().Get("Content-Type"), 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" } bytesOut := int64(c.Writer.Size()) if bytesOut < 0 { bytesOut = 0 } privateErrors := strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n") attributes := []any{ "mod", "http", "ip", c.ClientIP(), "method", c.Request.Method, "http_path", c.Request.URL.Path, "http_route", route, "http_status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(), "request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"), "bytes_in", bytesIn, "bytes_out", bytesOut, "user_id", userID, "authority_id", authorityID, "error", c.Writer.Status() >= http.StatusInternalServerError || privateErrors != "", "ua", c.Request.UserAgent(), "req_query", c.Request.URL.RawQuery} if config != nil && config.Zap != nil && config.Zap.AccessReqHeaders { attributes = append(attributes, "req_headers", redactHeaders(c.Request.Header)) } if config != nil && config.Zap != nil && config.Zap.AccessReqBody { attributes = append(attributes, "req_body", requestText) } if config != nil && config.Zap != nil && config.Zap.AccessRespData { attributes = append(attributes, "resp_data", responseText) } if privateErrors != "" { attributes = append(attributes, "error_msg", privateErrors) } logger.InfoContext(c.Request.Context(), "请求完成", 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 lower == "authorization" || lower == "cookie" || lower == "set-cookie" || lower == "x-token" { out[key] = "***" } else { out[key] = strings.Join(values, ",") } } return out } func stringValueFromContext(c *gin.Context, key string) string { value, _ := c.Get(key) return stringValue(value) }