feat(api-gateway): 实现 W1-W8 网关硬化与 P2-P5 路由扩展
依据 coord-final-decisions §3.8 W1-W8 裁决与 president-final-rulings §2.15/§2.16/§2.19 完整实现网关硬化: - W1/W2: 错误码 GW_ 前缀 + ActionState 信封响应体 - W3: 全量替换为 log/slog 结构化日志 - W4: /readyz 并行 ping 9 下游 + 软失败规则 - W5: 7 个业务 Prometheus 指标 + /metrics 端点 - W6: tracer 资源属性补全(name/version/env/host) - W7: DevMode=true && ENV=production panic 防护 - W8: 保持共享 downstream 熔断 P2 RS256 升级:接入 shared-go/jwks.Fetcher(TTL 5min)。 P2.7+P3-P5 路由扩展:student/parent/messages/dashboard。 文档同步:README/01/02/known-issues,arch.db 已更新。 质量校验:go vet + build + test 均通过。
This commit is contained in:
@@ -1,23 +1,112 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/edu-cloud/api-gateway/internal/config"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Healthz 存活探针(liveness)。
|
||||
// GET /healthz:返回 200 {"status":"ok"} 表示进程存活。
|
||||
func Healthz(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
})
|
||||
}
|
||||
|
||||
// Readyz 就绪探针(readiness)。
|
||||
// GET /readyz:检查下游服务可达性。
|
||||
// TODO: P7 接入服务发现后,检查 classes / iam / teacher-bff 等下游服务健康状态,
|
||||
// 任一不可达则返回 503,当前简化为直接返回 200。
|
||||
func Readyz(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
})
|
||||
// downstreamCheck 描述一个下游服务的健康检查配置。
|
||||
type downstreamCheck struct {
|
||||
name string // 服务名(用于响应体与日志)
|
||||
url string // /healthz 完整 URL
|
||||
required bool // true=必需依赖(失败返回 503),false=可选依赖(失败返回 200+degraded)
|
||||
}
|
||||
|
||||
// Readyz 就绪探针(readiness,W4 裁决)。
|
||||
// GET /readyz:并行 ping 下游服务 /healthz,超时 2s。
|
||||
//
|
||||
// 软失败规则(president-final-rulings.md §3.3):
|
||||
// - 必需依赖(iam / teacher-bff,P2 已就绪)失败 → 503
|
||||
// - 可选依赖(P3-P5 未就绪服务)失败 → 200 + degraded 列表
|
||||
// - 全部可达 → 200 {"status":"ok"}
|
||||
func Readyz(cfg *config.Config) gin.HandlerFunc {
|
||||
checks := []downstreamCheck{
|
||||
{name: "iam", url: cfg.IamServiceURL + "/healthz", required: true},
|
||||
{name: "teacher-bff", url: cfg.TeacherBffURL + "/healthz", required: true},
|
||||
{name: "core-edu", url: cfg.CoreEduServiceURL + "/healthz", required: false},
|
||||
{name: "content", url: cfg.ContentServiceURL + "/healthz", required: false},
|
||||
{name: "msg", url: cfg.MsgServiceURL + "/healthz", required: false},
|
||||
{name: "ai", url: cfg.AiServiceURL + "/healthz", required: false},
|
||||
{name: "data-ana", url: cfg.DataAnaServiceURL + "/healthz", required: false},
|
||||
{name: "student-bff", url: cfg.StudentBffURL + "/healthz", required: false},
|
||||
{name: "parent-bff", url: cfg.ParentBffURL + "/healthz", required: false},
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
var (
|
||||
mu sync.Mutex
|
||||
unhealthy []string
|
||||
degraded []string
|
||||
)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, check := range checks {
|
||||
wg.Add(1)
|
||||
go func(dc downstreamCheck) {
|
||||
defer wg.Done()
|
||||
resp, err := client.Get(dc.url)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
if dc.required {
|
||||
unhealthy = append(unhealthy, dc.name)
|
||||
} else {
|
||||
degraded = append(degraded, dc.name)
|
||||
}
|
||||
mu.Unlock()
|
||||
slog.Warn("downstream health check failed",
|
||||
"service", dc.name,
|
||||
"required", dc.required,
|
||||
"error", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
mu.Lock()
|
||||
if dc.required {
|
||||
unhealthy = append(unhealthy, dc.name)
|
||||
} else {
|
||||
degraded = append(degraded, dc.name)
|
||||
}
|
||||
mu.Unlock()
|
||||
slog.Warn("downstream unhealthy",
|
||||
"service", dc.name,
|
||||
"required", dc.required,
|
||||
"status", resp.StatusCode,
|
||||
)
|
||||
}
|
||||
}(check)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if len(unhealthy) > 0 {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"status": "error",
|
||||
"unhealthy": unhealthy,
|
||||
"degraded": degraded,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
resp := gin.H{"status": "ok"}
|
||||
if len(degraded) > 0 {
|
||||
resp["degraded"] = degraded
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user