v2.1 M9+M10: BFF layer and old portals retired.
- api-gateway: remove TeacherBffURL/StudentBffURL/ParentBffURL config
- api-gateway: add ApolloRouterURL config and /api/graphql route
- api-gateway: /api/admin/graphql now proxies to apollo-router
- api-gateway: health checks now ping apollo-router instead of BFF
- deploy.yml: replace teacher-bff service block with apollo-router
- deploy.yml: add config-service service block (M3 dependency)
- deploy.yml: remove teacher-portal and admin-portal service blocks
- source code in services/{teacher,student,parent}-bff/ and apps/*-portal/ retained for rollback
113 lines
3.0 KiB
Go
113 lines
3.0 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 / apollo-router)失败 → 503
|
||
// - 可选依赖(core-edu / content / msg / ai / data-ana)失败 → 200 + degraded 列表
|
||
// - 全部可达 → 200 {"status":"ok"}
|
||
//
|
||
// v2.1 M9:teacher-bff / student-bff / parent-bff 已下线,由 apollo-router 替代。
|
||
func Readyz(cfg *config.Config) gin.HandlerFunc {
|
||
checks := []downstreamCheck{
|
||
{name: "iam", url: cfg.IamServiceURL + "/healthz", required: true},
|
||
{name: "apollo-router", url: cfg.ApolloRouterURL + "/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},
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|