Files
Edu/services/api-gateway/internal/health/health.go
SpecialX 83b6919185 feat(api-gateway): retire teacher/student/parent-bff, route GraphQL to apollo-router
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
2026-07-15 08:14:24 +08:00

113 lines
3.0 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 / apollo-router失败 → 503
// - 可选依赖core-edu / content / msg / ai / data-ana失败 → 200 + degraded 列表
// - 全部可达 → 200 {"status":"ok"}
//
// v2.1 M9teacher-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)
}
}