Files
Edu/services/api-gateway/internal/health/health.go
SpecialX 4307f6b73c 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 均通过。
2026-07-10 18:15:48 +08:00

113 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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=必需依赖(失败返回 503false=可选依赖(失败返回 200+degraded
}
// Readyz 就绪探针readinessW4 裁决)。
// GET /readyz并行 ping 下游服务 /healthz超时 2s。
//
// 软失败规则president-final-rulings.md §3.3
// - 必需依赖iam / teacher-bffP2 已就绪)失败 → 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)
}
}