依据 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 均通过。
113 lines
3.1 KiB
Go
113 lines
3.1 KiB
Go
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)
|
||
}
|
||
}
|