Documentation
¶
Overview ¶
Package health provides a health-probe SDK for samber/do v2 containers. It turns the three-probe Kubernetes pattern (liveness, readiness, startup) into a single Probe type with sensible defaults.
The package separates three distinct concerns that are often wrongly conflated into a single /health endpoint:
- Liveness: "Is the process alive?" — trivially fast, dependency-free.
- Readiness: "Can I serve traffic?" — checks all services, gates on critical.
- Startup: "Am I done booting?" — latches once all critical services pass.
Quick Start ¶
injector := do.New()
// ... register and invoke services ...
probe := health.New(injector,
health.WithCriticalServices("database", "redis"),
health.WithVersion("1.0.0"),
)
if err := probe.Start(ctx); err != nil {
log.Fatal(err)
}
defer probe.Shutdown()
mux := http.NewServeMux()
probe.RegisterRoutes(mux, health.DefaultRoutes())
Audit Integration (Optional) ¶
When a HealthRecorder is provided via WithHealthRecorder, every health-check batch is delegated to the recorder instead of the raw injector. This allows an external system to observe health checks.
github.com/larsartmann/samber-do-auditlog.Plugin satisfies HealthRecorder implicitly — pass it directly:
plugin, _ := auditlog.New(auditlog.Config{Enabled: true})
injector := do.NewWithOpts(plugin.Opts())
probe := health.New(injector, health.WithHealthRecorder(plugin))
Why Three Probes? ¶
A single /health endpoint conflates "process alive" with "dependencies reachable." When a dependency blips, the endpoint returns 503, the kubelet restarts the pod, and a restart cascade follows — even though the process itself is fine. Splitting probes breaks this coupling:
- /healthz (liveness) never checks dependencies. Only a deadlocked or crashed process fails.
- /readyz (readiness) checks dependencies but only returns 503 for critical failures. Non-critical failures (e.g. metrics exporter) are surfaced in the response body without removing the pod from rotation.
- /startupz (startup) lets slow-booting apps use a generous kubelet failureThreshold without affecting liveness/readiness sensitivity.
Background Caching ¶
Kubelet and load balancers poll health endpoints frequently (often every second). Without caching, each readiness check calls Ping() on every dependency, hammering downstream systems. The Probe runs health checks on a bounded background loop (default: every 1 second) and serves cached results so the HTTP endpoint is always O(1).
Disable caching for low-traffic or development scenarios:
probe := health.New(injector, health.WithRefreshInterval(0))
Shutdown Awareness ¶
Call Probe.Shutdown (or Probe.MarkShuttingDown for two-phase graceful shutdown) during your server's graceful-drain path. Readiness immediately returns 503 so load balancers stop sending traffic before connections close. Liveness stays 200 because the process is still alive.
Timeouts ¶
WithTimeout sets a batch-level deadline: all services in one evaluation share the same context. A slow dependency can starve faster ones of their time budget. For per-service isolation, configure samber/do's native option at injector creation time:
injector := do.NewWithOpts(do.WithHealthCheckTimeout(2 * time.Second))
This library does not override that setting; it only controls the outer batch deadline (default: 5 seconds).
Index ¶
- Variables
- type Check
- type HealthRecorder
- type Option
- type Probe
- func (p *Probe) CachedResponse() Response
- func (p *Probe) Evaluate(ctx context.Context) Response
- func (p *Probe) LivenessHandler() http.HandlerFunc
- func (p *Probe) MarkShuttingDown()
- func (p *Probe) ReadinessHandler() http.HandlerFunc
- func (p *Probe) RefreshInterval() time.Duration
- func (p *Probe) RegisterRoutes(mux *http.ServeMux, routes Routes)
- func (p *Probe) Shutdown()
- func (p *Probe) Start(ctx context.Context) error
- func (p *Probe) StartupComplete() bool
- func (p *Probe) StartupHandler() http.HandlerFunc
- func (p *Probe) Validate() error
- type Response
- type Routes
- type Status
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidRefreshInterval = errors.New("health: refresh interval must not be negative")
ErrInvalidRefreshInterval is returned by Probe.Validate when the configured refresh interval is negative.
var ErrInvalidTimeout = errors.New("health: timeout must be positive")
ErrInvalidTimeout is returned by Probe.Validate when the configured timeout is zero or negative.
Functions ¶
This section is empty.
Types ¶
type Check ¶
type Check struct {
// Status is the health status of this individual check.
Status Status `json:"status"`
// Error contains the failure message when Status is not pass.
Error string `json:"error,omitempty"`
}
Check is the per-service health result.
type HealthRecorder ¶
type HealthRecorder interface {
RecordHealthCheckWithContext(ctx context.Context, injector do.Injector) map[string]error
}
HealthRecorder wraps health-check execution so that external systems (e.g. an audit-log plugin) can observe every check batch. Any type with a method matching this signature can be wired via WithHealthRecorder.
github.com/larsartmann/samber-do-auditlog.Plugin satisfies this interface implicitly — pass it directly when you want audit-log integration.
type Option ¶
type Option func(*config)
Option configures a Probe. Use the With* functions to create options.
func WithBootTime ¶
WithBootTime overrides the boot timestamp used to compute uptime. Defaults to the time New was called.
func WithCriticalServices ¶
WithCriticalServices marks the named services as critical: if any of them fails its health check, readiness returns 503. Services not listed here are non-critical; their failures appear in the response body but do not change the HTTP status code.
func WithGETOnly ¶
func WithGETOnly() Option
WithGETOnly wraps all handlers so they reject non-GET requests with 405 Method Not Allowed. Kubernetes probes always use GET; enabling this surfaces misconfigurations (e.g. a load balancer sending HEAD or POST) early.
func WithHealthRecorder ¶
func WithHealthRecorder(r HealthRecorder) Option
WithHealthRecorder wires a HealthRecorder so that every health-check batch is observable by an external system. When nil (the default), checks run against the raw injector without any recording layer.
Pass an [auditlog.Plugin] directly when you want audit-log integration:
probe := health.New(injector, health.WithHealthRecorder(plugin))
func WithRefreshInterval ¶
WithRefreshInterval sets the background cache refresh cadence. When greater than zero, Probe.Start launches a goroutine that re-evaluates health checks on this interval and readiness handlers serve the cached result. When zero, readiness handlers evaluate live on every request (no background goroutine).
Use caching (the default) when kubelet or load-balancer polling could overwhelm downstream dependencies. Use live evaluation for low-traffic or development scenarios.
func WithTimeout ¶
WithTimeout sets the batch-level context deadline shared across ALL services in a single health-check evaluation. All checks run concurrently against the same deadline — a slow dependency silently steals time from every other check.
For per-service isolation, configure samber/do's native option at injector creation time:
injector := do.NewWithOpts(do.WithHealthCheckTimeout(2 * time.Second))
This library does not override that setting; it only controls the outer batch deadline.
func WithVersion ¶
WithVersion sets the application version included in health responses.
type Probe ¶
type Probe struct {
// contains filtered or unexported fields
}
Probe orchestrates health checks against a samber/do v2 injector and exposes three distinct HTTP endpoints: liveness, readiness, and startup.
The health-check capability is resolved once at construction time (see New) so the Probe holds a resolved function value, never the injector itself. Probe classifies registered services into critical and non-critical: only critical service failures cause readiness to return 503; non-critical failures are surfaced as individual check entries but do not affect the HTTP status code.
Probe is safe for concurrent use by multiple goroutines.
func New ¶
New creates a Probe wired to the given injector.
The injector must be the root container created via do.NewWithOpts. The health-check capability (and, when configured, the HealthRecorder) is resolved here at construction time, so the returned Probe holds only the resolved function — never the container itself.
Example ¶
ExampleNew shows how to create a health Probe wired to a samber/do injector, register a critical service, and evaluate its health.
package main
import (
"context"
"fmt"
"github.com/larsartmann/go-health"
"github.com/samber/do/v2"
)
// exampleDB is a minimal service that satisfies do.HealthcheckerWithContext.
type exampleDB struct{}
func (*exampleDB) HealthCheck(_ context.Context) error { return nil }
func main() {
injector := do.New()
do.ProvideNamed(injector, "database", func(_ do.Injector) (*exampleDB, error) {
return &exampleDB{}, nil
})
_ = do.MustInvokeNamed[*exampleDB](injector, "database")
probe := health.New(injector,
health.WithVersion("1.0.0"),
health.WithCriticalServices("database"),
)
resp := probe.Evaluate(context.Background())
fmt.Println("status:", resp.Status)
fmt.Println("checks:", len(resp.Checks))
}
Output: status: pass checks: 1
func (*Probe) CachedResponse ¶ added in v0.0.2
CachedResponse returns the last background-refreshed health Response. When the background cache is active (Probe.Start called with a non-zero WithRefreshInterval), this reads the atomic p.latest pointer — lock-free, zero dependency calls. When no cache exists (live mode or before the first refresh), it returns a zero-value Response with StatusPass.
The live shuttingDown flag is overlaid on the cached value so a stale cached response (evaluated before Probe.Shutdown was called) still reflects the current shutdown state.
func (*Probe) Evaluate ¶
Evaluate runs a full health-check batch against the injector and returns the aggregate response. This is the core evaluation logic used by the readiness and startup handlers, exposed publicly for testing and custom handler scenarios.
The context should carry a deadline; Probe.Start applies [Probe.timeout] automatically. When a HealthRecorder is configured, checks are delegated to it instead of the raw injector.
func (*Probe) LivenessHandler ¶
func (p *Probe) LivenessHandler() http.HandlerFunc
LivenessHandler returns an http.HandlerFunc that answers the liveness question: "Is the process alive and not deadlocked?"
Liveness performs zero dependency checks and returns in microseconds. It always returns 200 with status "pass". This prevents restart cascades caused by downstream dependency blips.
Example ¶
ExampleProbe_LivenessHandler shows the liveness handler in action. Liveness never checks dependencies and always returns 200 with status "pass".
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"github.com/larsartmann/go-health"
"github.com/samber/do/v2"
)
func main() {
injector := do.New()
probe := health.New(injector, health.WithVersion("1.0.0"))
handler := probe.LivenessHandler()
w := httptest.NewRecorder()
r, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/healthz", nil)
if err != nil {
panic(err)
}
handler(w, r)
fmt.Println("HTTP status:", w.Code)
var resp health.Response
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
panic(err)
}
fmt.Println("health:", resp.Status)
}
Output: HTTP status: 200 health: pass
func (*Probe) MarkShuttingDown ¶
func (p *Probe) MarkShuttingDown()
MarkShuttingDown flips the shutdown flag without stopping the background loop. Use this for a two-phase graceful shutdown: mark first so load balancers start draining, then call Probe.Shutdown after a grace period to stop the refresh loop.
func (*Probe) ReadinessHandler ¶
func (p *Probe) ReadinessHandler() http.HandlerFunc
ReadinessHandler returns an http.HandlerFunc that answers the readiness question: "Can this instance serve traffic right now?"
Readiness runs a full health-check batch against the injector, classifies results into critical and non-critical, and returns:
- 200 when all critical services pass (non-critical failures appear as individual check entries but do not change the status code).
- 503 when any critical service fails or the probe is shutting down.
When a background cache is active (RefreshInterval > 0 and Probe.Start called), the handler serves the cached result for O(1) response time. When no cache is available, it evaluates live with a timeout-bounded context.
Example ¶
ExampleProbe_ReadinessHandler shows the readiness handler checking critical services. All healthy services return 200; a failing critical service would return 503.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/larsartmann/go-health"
"github.com/samber/do/v2"
)
// exampleDB is a minimal service that satisfies do.HealthcheckerWithContext.
type exampleDB struct{}
func (*exampleDB) HealthCheck(_ context.Context) error { return nil }
func main() {
injector := do.New()
do.ProvideNamed(injector, "database", func(_ do.Injector) (*exampleDB, error) {
return &exampleDB{}, nil
})
_ = do.MustInvokeNamed[*exampleDB](injector, "database")
probe := health.New(injector,
health.WithCriticalServices("database"),
health.WithRefreshInterval(0), // live evaluation for deterministic output
)
handler := probe.ReadinessHandler()
w := httptest.NewRecorder()
r, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/readyz", nil)
if err != nil {
panic(err)
}
handler(w, r)
fmt.Println("HTTP status:", w.Code)
}
Output: HTTP status: 200
func (*Probe) RefreshInterval ¶ added in v0.0.2
RefreshInterval returns the configured background cache refresh interval. Returns zero when the probe is in live evaluation mode.
func (*Probe) RegisterRoutes ¶
RegisterRoutes registers all three probe handlers on the given mux using the provided routes. Pass DefaultRoutes for conventional Kubernetes paths.
Example ¶
ExampleProbe_RegisterRoutes shows the one-liner for mounting all three Kubernetes probe endpoints on a standard http.ServeMux.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"github.com/larsartmann/go-health"
"github.com/samber/do/v2"
)
func main() {
injector := do.New()
probe := health.New(injector, health.WithRefreshInterval(0))
mux := http.NewServeMux()
probe.RegisterRoutes(mux, health.DefaultRoutes())
for _, path := range []string{"/healthz", "/readyz", "/startupz"} {
w := httptest.NewRecorder()
r, err := http.NewRequestWithContext(context.Background(), http.MethodGet, path, nil)
if err != nil {
panic(err)
}
mux.ServeHTTP(w, r)
fmt.Printf("%s: %d\n", path, w.Code)
}
}
Output: /healthz: 200 /readyz: 200 /startupz: 200
func (*Probe) Shutdown ¶
func (p *Probe) Shutdown()
Shutdown marks the probe as shutting down and stops the background refresh loop if one is running. After Shutdown:
- Liveness continues to return 200 (the process is still alive).
- Readiness returns 503 so load balancers drain traffic.
- Startup returns its latched value (200 if it had previously passed).
func (*Probe) Start ¶
Start validates the Probe configuration and, if valid, launches the background cache refresh loop (when RefreshInterval > 0) and performs an immediate evaluation so the cache is populated before the first request arrives. Calling Start more than once is a no-op.
Returns ErrInvalidTimeout or ErrInvalidRefreshInterval if the configuration is unusable. Call Probe.Validate separately to check configuration before starting.
The provided ctx controls the lifetime of the background goroutine. Call Probe.Shutdown to stop the loop and mark the probe as shutting down.
func (*Probe) StartupComplete ¶
StartupComplete returns true once all critical services have passed their health checks at least once during a startup evaluation. After this returns true it always returns true (the latch is one-way).
func (*Probe) StartupHandler ¶
func (p *Probe) StartupHandler() http.HandlerFunc
StartupHandler returns an http.HandlerFunc that answers the startup question: "Is the application done booting?"
Startup evaluates critical services on every request until all of them are present and healthy. Once that condition is met, the latch flips and all subsequent calls return 200 immediately without re-checking. This allows Kubernetes to use a generous failureThreshold for slow-booting applications without affecting liveness or readiness sensitivity.
func (*Probe) Validate ¶
Validate checks that the Probe configuration is internally consistent. Returns nil when the configuration is safe to use.
This catches the two common mistakes that cause runtime problems:
- Timeout <= 0 creates an already-expired context, so every health check fails immediately with "context deadline exceeded".
- RefreshInterval < 0 is treated the same as 0 (live evaluation) by Start, but callers likely intended a positive interval.
type Response ¶
type Response struct {
// Status is the overall roll-up: fail if any critical service is down
// or the probe is shutting down, warn if only non-critical services
// are degraded, pass when all services are healthy.
Status Status `json:"status"`
// Version is the application version, if configured.
Version string `json:"version,omitempty"`
// Uptime is human-readable duration since boot.
Uptime string `json:"uptime,omitempty"`
// ShuttingDown is true when the probe has been marked for shutdown.
// Readiness returns 503 when this is set; liveness stays 200.
ShuttingDown bool `json:"shutting_down,omitempty"`
// TotalLatencyMs is the wall-clock time spent running the health-check
// batch. Populated by readiness and startup evaluations; always zero
// for liveness (which performs no dependency checks).
TotalLatencyMs int64 `json:"total_latency_ms,omitempty"`
// Checks maps each service name to its individual result.
Checks map[string]Check `json:"checks"`
}
Response is the aggregate health-check response served by all probe handlers.
type Routes ¶
Routes configures the URL paths for Probe.RegisterRoutes.
func DefaultRoutes ¶
func DefaultRoutes() Routes
DefaultRoutes returns the conventional Kubernetes health-probe paths.
type Status ¶
type Status string
Status is the roll-up health status of a check or the overall response.
const ( // StatusPass means the service or system is healthy. StatusPass Status = "pass" // StatusFail means the service or system is unhealthy. StatusFail Status = "fail" // StatusWarn means the service is degraded but functional. // Used for non-critical service failures in readiness responses. StatusWarn Status = "warn" )