diff --git a/PROGRESS.md b/PROGRESS.md index 9d85c84..5b5ab9a 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -31,6 +31,7 @@ - [x] 模型配置控制面(按 kind 经 NATS 下发给 dispatcher / mcp-go) - [x] 独立运维控制台 sundynix-admin(模型 / 数据源页) - [x] SSE 回流:Token 流 / 执行轨迹 / 入库进度 +- [x] 可观测性:Prometheus /metrics(请求数/耗时/在途,路由模板低基数)· 结构化 JSON 访问日志 + X-Request-ID · /healthz(存活) + /readyz(就绪) 探针 - [x] Harness **输入**护栏(拦提示词注入 + 超大体,纯逻辑 `internal/guardrail` + 单测 + 实跑验证) - [x] Harness **输出**护栏(dispatcher 发射层逐片脱敏疑似密钥/令牌 sk-/AKIA/JWT/Bearer + 轨迹标记 + 单测) - [ ] 🟡 商业化与计费模块(占位,仅统计任务数) diff --git a/sundynix-gateway/go.mod b/sundynix-gateway/go.mod index a55a57e..60c531a 100644 --- a/sundynix-gateway/go.mod +++ b/sundynix-gateway/go.mod @@ -5,9 +5,12 @@ go 1.25.0 require ( github.com/bwmarrin/snowflake v0.3.0 github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/minio/minio-go/v7 v7.2.0 + github.com/prometheus/client_golang v1.23.2 github.com/redis/go-redis/v9 v9.20.0 github.com/sundynix/sundynix-shared v0.0.0 + golang.org/x/crypto v0.53.0 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.1 ) @@ -15,6 +18,7 @@ require ( replace github.com/sundynix/sundynix-shared => ../sundynix-shared require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect @@ -27,7 +31,6 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -40,26 +43,31 @@ require ( github.com/klauspost/cpuid/v2 v2.2.11 // indirect github.com/klauspost/crc32 v1.3.0 // indirect github.com/kr/text v0.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nats-io/nats.go v1.37.0 // indirect github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pelletier/go-toml/v2 v2.3.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/tinylib/msgp v1.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.11.0 // indirect - golang.org/x/crypto v0.53.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect diff --git a/sundynix-gateway/go.sum b/sundynix-gateway/go.sum index 90533c1..23ee497 100644 --- a/sundynix-gateway/go.sum +++ b/sundynix-gateway/go.sum @@ -1,3 +1,5 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -64,10 +66,12 @@ github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4O github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -85,6 +89,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nats-io/jwt/v2 v2.5.8 h1:uvdSzwWiEGWGXf+0Q+70qv6AQdvcvxrv9hPM0RiPamE= github.com/nats-io/jwt/v2 v2.5.8/go.mod h1:ZdWS1nZa6WMZfFwwgpEaqBV8EPGVgOTDHN/wTbz0Y5A= github.com/nats-io/nats-server/v2 v2.10.20 h1:CXDTYNHeBiAKBTAIP2gjpgbWap2GhATnTLgP8etyvEI= @@ -101,6 +107,14 @@ github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/redis/go-redis/v9 v9.20.0 h1:WnQYxLkgO2xiXTCJY0ldIiI8dNqCDlQAG+AtaH7a2a0= github.com/redis/go-redis/v9 v9.20.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -131,29 +145,23 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= diff --git a/sundynix-gateway/internal/handler/task_handler.go b/sundynix-gateway/internal/handler/task_handler.go index 61941b4..e444a1c 100644 --- a/sundynix-gateway/internal/handler/task_handler.go +++ b/sundynix-gateway/internal/handler/task_handler.go @@ -95,6 +95,22 @@ func (h *Handler) StreamTask(c *gin.Context) { }) } +// Healthz: GET /healthz —— 存活探针(liveness):进程能应答即 200,不查依赖。 +func (h *Handler) Healthz(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} + +// Readyz: GET /readyz —— 就绪探针(readiness):核心依赖(DB/Redis)可用才 200,否则 503。 +// 供 k8s 等编排器在依赖未就绪时暂不导流。NATS 在启动时即连(连不上会 fatal),故不单列。 +func (h *Handler) Readyz(c *gin.Context) { + deps := gin.H{"db": h.db.Enabled(), "redis": h.cache.Enabled()} + if h.db.Enabled() && h.cache.Enabled() { + c.JSON(http.StatusOK, gin.H{"status": "ready", "deps": deps}) + return + } + c.JSON(http.StatusServiceUnavailable, gin.H{"status": "not_ready", "deps": deps}) +} + // Health: GET /api/v1/health —— 聚合各依赖子系统健康,供桌面端顶栏五盏灯实时点亮。 // gateway/db/redis/nats 网关本地可判;milvus/neo4j 经 mcp-go health 工具取(不可用则置否)。 func (h *Handler) Health(c *gin.Context) { diff --git a/sundynix-gateway/internal/middleware/observability.go b/sundynix-gateway/internal/middleware/observability.go new file mode 100644 index 0000000..659d888 --- /dev/null +++ b/sundynix-gateway/internal/middleware/observability.go @@ -0,0 +1,94 @@ +package middleware + +import ( + "crypto/rand" + "encoding/hex" + "log/slog" + "os" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// CtxRequestID 是请求 ID 在 gin.Context 中的键。 +const CtxRequestID = "request_id" + +// ---- Prometheus 指标 ---- + +var ( + httpRequests = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "sundynix_http_requests_total", + Help: "HTTP 请求总数(按方法/路由模板/状态码)。", + }, []string{"method", "route", "status"}) + + httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "sundynix_http_request_duration_seconds", + Help: "HTTP 请求耗时(秒)。", + Buckets: prometheus.DefBuckets, + }, []string{"method", "route"}) + + httpInFlight = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "sundynix_http_requests_in_flight", + Help: "当前处理中的 HTTP 请求数。", + }) +) + +// accessLogger 是结构化访问日志器(JSON 到 stderr)。 +var accessLogger = slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) + +// RequestID 为每个请求生成/透传 X-Request-ID,写入上下文与响应头,供日志关联。 +func RequestID() gin.HandlerFunc { + return func(c *gin.Context) { + id := c.GetHeader("X-Request-ID") + if id == "" { + id = newRequestID() + } + c.Set(CtxRequestID, id) + c.Header("X-Request-ID", id) + c.Next() + } +} + +// Observe 记录 Prometheus 指标 + 结构化访问日志。放在中间件链较前位置。 +func Observe() gin.HandlerFunc { + return func(c *gin.Context) { + start := time.Now() + httpInFlight.Inc() + c.Next() + httpInFlight.Dec() + + route := c.FullPath() // 路由模板(/tasks/:id/...),避免按真实路径产生高基数 + if route == "" { + route = "unmatched" + } + status := c.Writer.Status() + dur := time.Since(start) + method := c.Request.Method + + httpRequests.WithLabelValues(method, route, strconv.Itoa(status)).Inc() + httpDuration.WithLabelValues(method, route).Observe(dur.Seconds()) + + uid, _ := c.Get(CtxUserID) + rid, _ := c.Get(CtxRequestID) + accessLogger.Info("http", + "request_id", rid, + "method", method, + "route", route, + "path", c.Request.URL.Path, + "status", status, + "latency_ms", dur.Milliseconds(), + "ip", c.ClientIP(), + "uid", uid, + "bytes", c.Writer.Size(), + ) + } +} + +func newRequestID() string { + var b [8]byte + _, _ = rand.Read(b[:]) + return hex.EncodeToString(b[:]) +} diff --git a/sundynix-gateway/internal/middleware/observability_test.go b/sundynix-gateway/internal/middleware/observability_test.go new file mode 100644 index 0000000..3b91e29 --- /dev/null +++ b/sundynix-gateway/internal/middleware/observability_test.go @@ -0,0 +1,48 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus/testutil" +) + +func newEngine() *gin.Engine { + gin.SetMode(gin.TestMode) + r := gin.New() + r.Use(RequestID(), Observe()) + r.GET("/ping", func(c *gin.Context) { c.String(http.StatusOK, "pong") }) + return r +} + +func TestObserve_CountsAndRequestID(t *testing.T) { + r := newEngine() + before := testutil.ToFloat64(httpRequests.WithLabelValues("GET", "/ping", "200")) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/ping", nil)) + + if w.Code != 200 { + t.Fatalf("状态码=%d", w.Code) + } + if w.Header().Get("X-Request-ID") == "" { + t.Error("应自动生成并回写 X-Request-ID") + } + after := testutil.ToFloat64(httpRequests.WithLabelValues("GET", "/ping", "200")) + if after != before+1 { + t.Errorf("请求计数应 +1:before=%v after=%v", before, after) + } +} + +func TestRequestID_PropagatesIncoming(t *testing.T) { + r := newEngine() + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/ping", nil) + req.Header.Set("X-Request-ID", "trace-abc-123") + r.ServeHTTP(w, req) + if got := w.Header().Get("X-Request-ID"); got != "trace-abc-123" { + t.Errorf("应透传入站 X-Request-ID,got %q", got) + } +} diff --git a/sundynix-gateway/internal/router/router.go b/sundynix-gateway/internal/router/router.go index bdb938c..55dac0b 100644 --- a/sundynix-gateway/internal/router/router.go +++ b/sundynix-gateway/internal/router/router.go @@ -5,6 +5,7 @@ import ( "os" "github.com/gin-gonic/gin" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sundynix/sundynix-gateway/internal/blob" "github.com/sundynix/sundynix-gateway/internal/handler" @@ -15,13 +16,22 @@ import ( // New 构建带有 Guardrail / 限流中间件的 Gin 引擎。 func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.Store) *gin.Engine { - r := gin.Default() - r.Use(cors()) // 桌面端/浏览器跨源访问(开发期放开) + r := gin.New() + r.Use(gin.Recovery()) // panic 兜底 + r.Use(middleware.RequestID()) // 生成/透传 X-Request-ID(日志关联) + r.Use(middleware.Observe()) // Prometheus 指标 + 结构化访问日志(替代 gin 默认文本日志) + r.Use(cors()) // 桌面端/浏览器跨源访问 r.Use(middleware.RateLimit(cache)) r.Use(middleware.Auth()) // 解析 Bearer JWT,注入已验证 userID(非阻断) r.Use(middleware.Guardrail()) // Harness: Input Guardrail h := handler.New(db, cache, bus, blobStore) + + // 可观测性根端点:Prometheus 抓取 + k8s 存活/就绪探针(不挂业务中间件鉴权)。 + r.GET("/metrics", gin.WrapH(promhttp.Handler())) + r.GET("/healthz", h.Healthz) + r.GET("/readyz", h.Readyz) + api := r.Group("/api/v1") { // —— 公开:鉴权端点 / 健康 / 按 task_id 寻址的 SSE 与导出(EventSource/下载无法带 Bearer)——