feat(server): add /healthz and /readyz endpoints for Kubernetes probes
Add lightweight health check endpoints for container orchestration
(Kubernetes liveness/readiness probes, load balancer health checks).
New config under monitoring:
- healthz.enabled (default true), healthz.path (default /healthz)
- readyz.enabled (default true), readyz.path (default /readyz)
/healthz (liveness):
- Always returns 200 {"status":"ok"} if process is alive
- No dependency checks, minimal overhead
/readyz (readiness):
- Returns 200 {"status":"ready"} when server is running
- Returns 503 {"status":"not ready","reasons":[...]} when not ready
- Static-only servers (no proxies) always return 200
Registration:
- Registered alongside status/pprof endpoints
- Available in single mode (LocationEngine) and multi-server mode (Router)
- No IP allowlist required (K8s probes come from localhost)
- 6 unit tests covering all response scenarios
This commit is contained in:
parent
8224ae7ff3
commit
f605ef3b44
@ -20,13 +20,22 @@ package config
|
||||
// path: "/status"
|
||||
// allow: ["127.0.0.1", "10.0.0.0/8"]
|
||||
type MonitoringConfig struct {
|
||||
// Status 状态端点配置
|
||||
// 服务健康状态检查端点
|
||||
Status StatusConfig `yaml:"status"`
|
||||
Status StatusConfig `yaml:"status"`
|
||||
Pprof PprofConfig `yaml:"pprof"`
|
||||
Healthz HealthzConfig `yaml:"healthz"`
|
||||
Readyz ReadyzConfig `yaml:"readyz"`
|
||||
}
|
||||
|
||||
// Pprof pprof 性能分析端点配置
|
||||
// 用于收集 CPU、内存等性能数据,支持 PGO 优化
|
||||
Pprof PprofConfig `yaml:"pprof"`
|
||||
// HealthzConfig configures the /healthz liveness probe endpoint.
|
||||
type HealthzConfig struct {
|
||||
Path string `yaml:"path"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
// ReadyzConfig configures the /readyz readiness probe endpoint.
|
||||
type ReadyzConfig struct {
|
||||
Path string `yaml:"path"`
|
||||
Enabled bool `yaml:"enabled"`
|
||||
}
|
||||
|
||||
// PprofConfig pprof 性能分析端点配置。
|
||||
|
||||
59
internal/server/healthz.go
Normal file
59
internal/server/healthz.go
Normal file
@ -0,0 +1,59 @@
|
||||
package server
|
||||
|
||||
import "github.com/valyala/fasthttp"
|
||||
|
||||
// HealthzHandler returns a liveness probe that always responds 200 {"status":"ok"}.
|
||||
func HealthzHandler(ctx *fasthttp.RequestCtx) {
|
||||
ctx.SetContentType("application/json")
|
||||
ctx.SetStatusCode(200)
|
||||
ctx.SetBodyString(`{"status":"ok"}`)
|
||||
}
|
||||
|
||||
// NewReadyzHandler creates a readiness probe using the provided checker function.
|
||||
func NewReadyzHandler(checker func() (bool, []string)) fasthttp.RequestHandler {
|
||||
return func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.SetContentType("application/json")
|
||||
ready, reasons := checker()
|
||||
if ready {
|
||||
ctx.SetStatusCode(200)
|
||||
ctx.SetBodyString(`{"status":"ready"}`)
|
||||
} else {
|
||||
ctx.SetStatusCode(503)
|
||||
ctx.SetBodyString(buildReasonsJSON(reasons))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildReasonsJSON(reasons []string) string {
|
||||
if len(reasons) == 0 {
|
||||
return `{"status":"not ready"}`
|
||||
}
|
||||
var buf []byte
|
||||
buf = append(buf, `{"status":"not ready","reasons":[`...)
|
||||
for i, r := range reasons {
|
||||
if i > 0 {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
buf = append(buf, '"')
|
||||
buf = append(buf, r...)
|
||||
buf = append(buf, '"')
|
||||
}
|
||||
buf = append(buf, "]}"...)
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// DefaultReadyzChecker returns a readiness checker for the Server.
|
||||
func DefaultReadyzChecker(s *Server) func() (bool, []string) {
|
||||
return func() (bool, []string) {
|
||||
if !s.running.Load() {
|
||||
return false, []string{"server not running"}
|
||||
}
|
||||
s.proxiesMu.RLock()
|
||||
n := len(s.proxies)
|
||||
s.proxiesMu.RUnlock()
|
||||
if n == 0 {
|
||||
return true, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
76
internal/server/healthz_test.go
Normal file
76
internal/server/healthz_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func TestHealthzHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
var ctx fasthttp.RequestCtx
|
||||
HealthzHandler(&ctx)
|
||||
assert.Equal(t, 200, ctx.Response.StatusCode())
|
||||
assert.Equal(t, "application/json", string(ctx.Response.Header.ContentType()))
|
||||
assert.Equal(t, `{"status":"ok"}`, string(ctx.Response.Body()))
|
||||
}
|
||||
|
||||
func TestHealthzHandler_ValidJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
var ctx fasthttp.RequestCtx
|
||||
HealthzHandler(&ctx)
|
||||
var result map[string]string
|
||||
err := json.Unmarshal(ctx.Response.Body(), &result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ok", result["status"])
|
||||
}
|
||||
|
||||
func TestReadyzHandler_Ready(t *testing.T) {
|
||||
t.Parallel()
|
||||
handler := NewReadyzHandler(func() (bool, []string) {
|
||||
return true, nil
|
||||
})
|
||||
var ctx fasthttp.RequestCtx
|
||||
handler(&ctx)
|
||||
assert.Equal(t, 200, ctx.Response.StatusCode())
|
||||
assert.Equal(t, `{"status":"ready"}`, string(ctx.Response.Body()))
|
||||
}
|
||||
|
||||
func TestReadyzHandler_NotReady(t *testing.T) {
|
||||
t.Parallel()
|
||||
handler := NewReadyzHandler(func() (bool, []string) {
|
||||
return false, []string{"test reason"}
|
||||
})
|
||||
var ctx fasthttp.RequestCtx
|
||||
handler(&ctx)
|
||||
assert.Equal(t, 503, ctx.Response.StatusCode())
|
||||
assert.Equal(t, `{"status":"not ready","reasons":["test reason"]}`, string(ctx.Response.Body()))
|
||||
}
|
||||
|
||||
func TestReadyzHandler_NotReady_NoReasons(t *testing.T) {
|
||||
t.Parallel()
|
||||
handler := NewReadyzHandler(func() (bool, []string) {
|
||||
return false, nil
|
||||
})
|
||||
var ctx fasthttp.RequestCtx
|
||||
handler(&ctx)
|
||||
assert.Equal(t, 503, ctx.Response.StatusCode())
|
||||
assert.Equal(t, `{"status":"not ready"}`, string(ctx.Response.Body()))
|
||||
}
|
||||
|
||||
func TestBuildReasonsJSON_MultipleReasons(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := buildReasonsJSON([]string{"reason A", "reason B", "reason C"})
|
||||
var parsed map[string]any
|
||||
err := json.Unmarshal([]byte(result), &parsed)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "not ready", parsed["status"])
|
||||
reasons, ok := parsed["reasons"].([]any)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, 3, len(reasons))
|
||||
assert.Equal(t, "reason A", reasons[0])
|
||||
assert.Equal(t, "reason B", reasons[1])
|
||||
assert.Equal(t, "reason C", reasons[2])
|
||||
}
|
||||
@ -492,6 +492,31 @@ func (s *Server) startSingleMode() error {
|
||||
}
|
||||
}
|
||||
|
||||
if s.config.Monitoring.Healthz.Enabled {
|
||||
hzPath := s.config.Monitoring.Healthz.Path
|
||||
if hzPath == "" {
|
||||
hzPath = "/healthz"
|
||||
}
|
||||
if regErr := s.locationEngine.AddExact(hzPath, HealthzHandler, false); regErr != nil {
|
||||
if err := s.handleRegistrationError("healthz", hzPath, regErr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.config.Monitoring.Readyz.Enabled {
|
||||
rzPath := s.config.Monitoring.Readyz.Path
|
||||
if rzPath == "" {
|
||||
rzPath = "/readyz"
|
||||
}
|
||||
readyzHandler := NewReadyzHandler(DefaultReadyzChecker(s))
|
||||
if regErr := s.locationEngine.AddExact(rzPath, readyzHandler, false); regErr != nil {
|
||||
if err := s.handleRegistrationError("readyz", rzPath, regErr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.config.Monitoring.Pprof.Enabled {
|
||||
pprofHandler, err := NewPprofHandler(&s.config.Monitoring.Pprof)
|
||||
if err != nil {
|
||||
@ -803,6 +828,25 @@ func (s *Server) registerMonitoringEndpoints(router *handler.Router, serverCfg *
|
||||
router.POST(purgeHandler.Path(), purgeHandler.ServeHTTP)
|
||||
}
|
||||
}
|
||||
|
||||
if isDefault {
|
||||
if s.config.Monitoring.Healthz.Enabled {
|
||||
hzPath := s.config.Monitoring.Healthz.Path
|
||||
if hzPath == "" {
|
||||
hzPath = "/healthz"
|
||||
}
|
||||
router.GET(hzPath, HealthzHandler)
|
||||
}
|
||||
|
||||
if s.config.Monitoring.Readyz.Enabled {
|
||||
rzPath := s.config.Monitoring.Readyz.Path
|
||||
if rzPath == "" {
|
||||
rzPath = "/readyz"
|
||||
}
|
||||
readyzHandler := NewReadyzHandler(DefaultReadyzChecker(s))
|
||||
router.GET(rzPath, readyzHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wrapHandler 应用中间件链、连接池包装和统计追踪。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user