Files
lijiaoqiao/supply-api/internal/httpapi/healthcheck.go
Your Name 90fc95bc27 fix(supply-api): add missing runtime support sources
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.
2026-04-11 09:45:37 +08:00

294 lines
7.4 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 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)
}