2 Commits

Author SHA1 Message Date
2aa7ee2444 chore: bump go directive from 1.25 to 1.26 2026-05-12 02:06:45 +00:00
8d6930b087 feat(health)!: promote to v1.0.0 — configurable check timeout via NewHandlerWithConfig
Add Config struct and NewHandlerWithConfig(logger, cfg, checks...) constructor.
Config.CheckTimeout sets the per-request deadline for all health checks; zero
value defaults to 5 seconds. NewHandler remains unchanged as a backward-compatible
wrapper. API committed as stable.
2026-05-11 19:05:27 -06:00
4 changed files with 68 additions and 7 deletions

View File

@@ -5,6 +5,22 @@ All notable changes to this module will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] — 2026-05-12
### Added
- `Config` struct — `CheckTimeout time.Duration`; zero value defaults to 5 seconds.
- `NewHandlerWithConfig(logger Logger, cfg Config, checks ...Checkable) http.Handler`
constructor with explicit configuration. `NewHandler` is now a backward-compatible
wrapper that calls `NewHandlerWithConfig` with zero `Config`.
### Unchanged
All existing API (`Level`, `LevelCritical`, `LevelDegraded`, `Checkable`, `Logger`,
`ComponentStatus`, `Response`, `NewHandler`) is API-compatible with v0.9.0.
[1.0.0]: https://code.nochebuena.dev/go/health/releases/tag/v1.0.0
## [0.9.0] - 2026-03-18 ## [0.9.0] - 2026-03-18
### Added ### Added

2
go.mod
View File

@@ -1,3 +1,3 @@
module code.nochebuena.dev/go/health module code.nochebuena.dev/go/health
go 1.25 go 1.26

View File

@@ -48,23 +48,44 @@ type Response struct {
Components map[string]ComponentStatus `json:"components"` Components map[string]ComponentStatus `json:"components"`
} }
type handler struct { // Config configures a health handler.
logger Logger // The zero value is valid: 5-second check timeout.
checks []Checkable type Config struct {
// CheckTimeout is the per-request deadline for all health checks.
// Defaults to 5 seconds when zero.
CheckTimeout time.Duration
} }
// NewHandler returns an http.Handler for the health endpoint. const defaultCheckTimeout = 5 * time.Second
type handler struct {
logger Logger
checks []Checkable
timeout time.Duration
}
// NewHandler returns an http.Handler for the health endpoint with default configuration.
// Runs all checks concurrently with a 5-second timeout. // Runs all checks concurrently with a 5-second timeout.
// Returns 200 (UP/DEGRADED) or 503 (DOWN). // Returns 200 (UP/DEGRADED) or 503 (DOWN).
func NewHandler(logger Logger, checks ...Checkable) http.Handler { func NewHandler(logger Logger, checks ...Checkable) http.Handler {
return &handler{logger: logger, checks: checks} return NewHandlerWithConfig(logger, Config{}, checks...)
}
// NewHandlerWithConfig returns an http.Handler configured by cfg.
// If cfg.CheckTimeout is zero, a 5-second default is used.
func NewHandlerWithConfig(logger Logger, cfg Config, checks ...Checkable) http.Handler {
timeout := cfg.CheckTimeout
if timeout == 0 {
timeout = defaultCheckTimeout
}
return &handler{logger: logger, checks: checks, timeout: timeout}
} }
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logger := h.logger.WithContext(r.Context()) logger := h.logger.WithContext(r.Context())
logger.Debug("health: running checks") logger.Debug("health: running checks")
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) ctx, cancel := context.WithTimeout(r.Context(), h.timeout)
defer cancel() defer cancel()
type result struct { type result struct {

View File

@@ -167,6 +167,30 @@ func TestHandler_JSON_Shape(t *testing.T) {
} }
} }
func TestHandlerWithConfig_CustomTimeout(t *testing.T) {
// 200ms delay check with a 50ms timeout — should be reported as DOWN.
h := NewHandlerWithConfig(&noopLogger{}, Config{CheckTimeout: 50 * time.Millisecond},
&mockCheck{name: "slow", priority: LevelCritical, delay: 200 * time.Millisecond},
)
code, resp := doRequest(t, h)
if code != http.StatusServiceUnavailable {
t.Errorf("want 503, got %d", code)
}
if resp.Status != "DOWN" {
t.Errorf("want DOWN, got %s", resp.Status)
}
}
func TestHandlerWithConfig_ZeroTimeout_UsesDefault(t *testing.T) {
h := NewHandlerWithConfig(&noopLogger{}, Config{},
&mockCheck{name: "db", priority: LevelCritical},
)
code, resp := doRequest(t, h)
if code != http.StatusOK || resp.Status != "UP" {
t.Errorf("want 200 UP, got %d %s", code, resp.Status)
}
}
func TestHandler_ContextTimeout(t *testing.T) { func TestHandler_ContextTimeout(t *testing.T) {
// Check that times out faster than the 5s global timeout when client cancels. // Check that times out faster than the 5s global timeout when client cancels.
h := NewHandler(&noopLogger{}, h := NewHandler(&noopLogger{},