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(http.StatusOK, 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) } }