Check in the healthcheck, structured logging, outbox broker, partition manager, and token status repository files that the committed supply-api runtime already imports. Verified with fresh go test runs for cmd/supply-api, internal/httpapi, internal/pkg/logging, internal/repository, and internal/outbox.
294 lines
7.4 KiB
Go
294 lines
7.4 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"net/http"
|
||
"time"
|
||
)
|
||
|
||
// ==================== P1-007 健康检查和就绪探针 ====================
|
||
|
||
// HealthChecker 健康检查接口
|
||
type HealthChecker interface {
|
||
// Check 执行健康检查
|
||
Check(ctx context.Context) error
|
||
// Name 返回检查器名称
|
||
Name() string
|
||
}
|
||
|
||
// HealthResponse 健康检查响应
|
||
type HealthResponse struct {
|
||
Status string `json:"status"` // "healthy" | "unhealthy" | "degraded"
|
||
Timestamp time.Time `json:"timestamp"`
|
||
DurationMs int64 `json:"duration_ms"`
|
||
Checks []HealthCheckResult `json:"checks,omitempty"`
|
||
}
|
||
|
||
// HealthCheckResult 单个检查结果
|
||
type HealthCheckResult struct {
|
||
Name string `json:"name"`
|
||
Status string `json:"status"` // "ok" | "error" | "warn"
|
||
DurationMs int64 `json:"duration_ms"`
|
||
Error string `json:"error,omitempty"`
|
||
Message string `json:"message,omitempty"`
|
||
}
|
||
|
||
// ReadinessResponse 就绪检查响应
|
||
type ReadinessResponse struct {
|
||
Status string `json:"status"` // "ready" | "not_ready"
|
||
Timestamp time.Time `json:"timestamp"`
|
||
Checks []ReadinessCheckResult `json:"checks,omitempty"`
|
||
}
|
||
|
||
// ReadinessCheckResult 就绪检查结果
|
||
type ReadinessCheckResult struct {
|
||
Name string `json:"name"`
|
||
Status string `json:"status"` // "ready" | "not_ready"
|
||
Error string `json:"error,omitempty"`
|
||
}
|
||
|
||
// LivenessResponse 存活检查响应
|
||
type LivenessResponse struct {
|
||
Status string `json:"status"` // "alive"
|
||
Timestamp time.Time `json:"timestamp"`
|
||
}
|
||
|
||
// DefaultHealthChecker 默认健康检查器
|
||
type DefaultHealthChecker struct {
|
||
checks []HealthChecker
|
||
}
|
||
|
||
// NewDefaultHealthChecker 创建默认健康检查器
|
||
func NewDefaultHealthChecker() *DefaultHealthChecker {
|
||
return &DefaultHealthChecker{
|
||
checks: make([]HealthChecker, 0),
|
||
}
|
||
}
|
||
|
||
// AddCheck 添加健康检查
|
||
func (h *DefaultHealthChecker) AddCheck(checker HealthChecker) {
|
||
h.checks = append(h.checks, checker)
|
||
}
|
||
|
||
// Check 执行所有健康检查
|
||
func (h *DefaultHealthChecker) Check(ctx context.Context) error {
|
||
for _, checker := range h.checks {
|
||
if err := checker.Check(ctx); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// Name 返回名称
|
||
func (h *DefaultHealthChecker) Name() string {
|
||
return "default"
|
||
}
|
||
|
||
// DBHealthChecker 数据库健康检查
|
||
type DBHealthChecker struct {
|
||
checkFn func(ctx context.Context) error
|
||
}
|
||
|
||
// NewDBHealthChecker 创建数据库健康检查
|
||
func NewDBHealthChecker(checkFn func(ctx context.Context) error) *DBHealthChecker {
|
||
return &DBHealthChecker{checkFn: checkFn}
|
||
}
|
||
|
||
func (c *DBHealthChecker) Check(ctx context.Context) error {
|
||
return c.checkFn(ctx)
|
||
}
|
||
|
||
func (c *DBHealthChecker) Name() string {
|
||
return "database"
|
||
}
|
||
|
||
// CacheHealthChecker 缓存健康检查
|
||
type CacheHealthChecker struct {
|
||
checkFn func(ctx context.Context) error
|
||
}
|
||
|
||
// NewCacheHealthChecker 创建缓存健康检查
|
||
func NewCacheHealthChecker(checkFn func(ctx context.Context) error) *CacheHealthChecker {
|
||
return &CacheHealthChecker{checkFn: checkFn}
|
||
}
|
||
|
||
func (c *CacheHealthChecker) Check(ctx context.Context) error {
|
||
return c.checkFn(ctx)
|
||
}
|
||
|
||
func (c *CacheHealthChecker) Name() string {
|
||
return "cache"
|
||
}
|
||
|
||
// HealthHandler 健康检查处理器
|
||
type HealthHandler struct {
|
||
healthChecker *DefaultHealthChecker
|
||
readinessChecks []HealthChecker
|
||
livenessChecks []HealthChecker
|
||
}
|
||
|
||
// NewHealthHandler 创建健康检查处理器
|
||
func NewHealthHandler() *HealthHandler {
|
||
return &HealthHandler{
|
||
healthChecker: NewDefaultHealthChecker(),
|
||
readinessChecks: make([]HealthChecker, 0),
|
||
livenessChecks: make([]HealthChecker, 0),
|
||
}
|
||
}
|
||
|
||
// NewHealthHandlerWithDefaults 创建带默认检查器的健康检查处理器
|
||
// P1-007修复: 统一健康检查实现,消除重复的inline handlers
|
||
func NewHealthHandlerWithDefaults(
|
||
dbHealthCheck func(ctx context.Context) error,
|
||
redisHealthCheck func(ctx context.Context) error,
|
||
) *HealthHandler {
|
||
h := NewHealthHandler()
|
||
|
||
if dbHealthCheck != nil {
|
||
h.AddHealthCheck(NewDBHealthChecker(dbHealthCheck))
|
||
h.AddReadinessCheck(NewDBHealthChecker(dbHealthCheck))
|
||
}
|
||
|
||
if redisHealthCheck != nil {
|
||
h.AddHealthCheck(NewCacheHealthChecker(redisHealthCheck))
|
||
h.AddReadinessCheck(NewCacheHealthChecker(redisHealthCheck))
|
||
}
|
||
|
||
// 存活检查总是返回OK(不需要外部依赖)
|
||
h.AddLivenessCheck(&staticHealthChecker{name: "liveness", err: nil})
|
||
|
||
return h
|
||
}
|
||
|
||
// staticHealthChecker 静态健康检查器
|
||
type staticHealthChecker struct {
|
||
name string
|
||
err error
|
||
}
|
||
|
||
func (c *staticHealthChecker) Check(ctx context.Context) error {
|
||
return c.err
|
||
}
|
||
|
||
func (c *staticHealthChecker) Name() string {
|
||
return c.name
|
||
}
|
||
|
||
// AddHealthCheck 添加健康检查
|
||
func (h *HealthHandler) AddHealthCheck(checker HealthChecker) {
|
||
h.healthChecker.AddCheck(checker)
|
||
}
|
||
|
||
// AddReadinessCheck 添加就绪检查
|
||
func (h *HealthHandler) AddReadinessCheck(checker HealthChecker) {
|
||
h.readinessChecks = append(h.readinessChecks, checker)
|
||
}
|
||
|
||
// AddLivenessCheck 添加存活检查
|
||
func (h *HealthHandler) AddLivenessCheck(checker HealthChecker) {
|
||
h.livenessChecks = append(h.livenessChecks, checker)
|
||
}
|
||
|
||
// ServeHealth 处理健康检查请求
|
||
func (h *HealthHandler) ServeHealth(w http.ResponseWriter, r *http.Request) {
|
||
ctx := r.Context()
|
||
start := time.Now()
|
||
|
||
response := HealthResponse{
|
||
Timestamp: start,
|
||
DurationMs: 0,
|
||
Checks: make([]HealthCheckResult, 0),
|
||
}
|
||
|
||
overallStatus := "healthy"
|
||
|
||
for _, checker := range h.healthChecker.checks {
|
||
checkStart := time.Now()
|
||
err := checker.Check(ctx)
|
||
|
||
result := HealthCheckResult{
|
||
Name: checker.Name(),
|
||
DurationMs: time.Since(checkStart).Milliseconds(),
|
||
}
|
||
|
||
if err != nil {
|
||
result.Status = "error"
|
||
result.Error = err.Error()
|
||
overallStatus = "unhealthy"
|
||
} else {
|
||
result.Status = "ok"
|
||
}
|
||
|
||
response.Checks = append(response.Checks, result)
|
||
}
|
||
|
||
response.Status = overallStatus
|
||
response.DurationMs = time.Since(start).Milliseconds()
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusOK)
|
||
json.NewEncoder(w).Encode(response)
|
||
}
|
||
|
||
// ServeReadiness 处理就绪检查请求
|
||
func (h *HealthHandler) ServeReadiness(w http.ResponseWriter, r *http.Request) {
|
||
ctx := r.Context()
|
||
|
||
response := ReadinessResponse{
|
||
Timestamp: time.Now(),
|
||
Checks: make([]ReadinessCheckResult, 0),
|
||
}
|
||
|
||
overallStatus := "ready"
|
||
|
||
for _, checker := range h.readinessChecks {
|
||
err := checker.Check(ctx)
|
||
|
||
result := ReadinessCheckResult{
|
||
Name: checker.Name(),
|
||
}
|
||
|
||
if err != nil {
|
||
result.Status = "not_ready"
|
||
result.Error = err.Error()
|
||
overallStatus = "not_ready"
|
||
} else {
|
||
result.Status = "ready"
|
||
}
|
||
|
||
response.Checks = append(response.Checks, result)
|
||
}
|
||
|
||
response.Status = overallStatus
|
||
|
||
statusCode := http.StatusOK
|
||
if overallStatus == "not_ready" {
|
||
statusCode = http.StatusServiceUnavailable
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(statusCode)
|
||
json.NewEncoder(w).Encode(response)
|
||
}
|
||
|
||
// ServeLiveness 处理存活检查请求
|
||
func (h *HealthHandler) ServeLiveness(w http.ResponseWriter, r *http.Request) {
|
||
response := LivenessResponse{
|
||
Status: "alive",
|
||
Timestamp: time.Now(),
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
w.WriteHeader(http.StatusOK)
|
||
json.NewEncoder(w).Encode(response)
|
||
}
|
||
|
||
// RegisterHealthRoutes 注册健康检查路由
|
||
func (h *HealthHandler) RegisterRoutes(mux *http.ServeMux) {
|
||
mux.HandleFunc("/actuator/health", h.ServeHealth)
|
||
mux.HandleFunc("/actuator/health/ready", h.ServeReadiness)
|
||
mux.HandleFunc("/actuator/health/live", h.ServeLiveness)
|
||
}
|