From 0ab0168a8d260699b690861663d01e1930a91854 Mon Sep 17 00:00:00 2001 From: Elijah Duffy Date: Tue, 27 Jan 2026 18:13:33 -0800 Subject: [PATCH] go: implement readiness library --- go-health/liveness.go | 42 ++++++++ go-health/readiness.go | 221 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 go-health/liveness.go create mode 100644 go-health/readiness.go diff --git a/go-health/liveness.go b/go-health/liveness.go new file mode 100644 index 0000000..ba365fc --- /dev/null +++ b/go-health/liveness.go @@ -0,0 +1,42 @@ +package health + +import ( + "encoding/json" + "net/http" + "time" +) + +// Status of the service liveness. +type LivenessStatus string + +const ( + // Service is alive and functioning properly. + LivenessStatusAlive LivenessStatus = "ok" +) + +// Result of a liveness check. +type LivenessResult struct { + // Status of the service (always LivenessStatusAlive) + Status LivenessStatus + // Timestamp of the liveness check in milliseconds since Unix epoch + Timestamp int64 +} + +// Checks if the service is alive. +func Liveness() LivenessResult { + return LivenessResult{ + Status: LivenessStatusAlive, + Timestamp: time.Now().UnixMilli(), + } +} + +// Handler for liveness HTTP requests. +// Returns a response with status 200 and a JSON body matching LivenessResult. +func LivenessHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + result := Liveness() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(result) + } +} diff --git a/go-health/readiness.go b/go-health/readiness.go new file mode 100644 index 0000000..4243b40 --- /dev/null +++ b/go-health/readiness.go @@ -0,0 +1,221 @@ +package health + +import ( + "encoding/json" + "net/http" + "slices" + "time" +) + +// Status of the service readiness. +type ReadinessStatus string + +// String returns the string representation of the readiness status. +func (s ReadinessStatus) String() string { + return string(s) +} + +// HTTPStatus returns the corresponding HTTP status code for the readiness status. +func (s ReadinessStatus) HTTPStatus() int { + switch s { + case ReadinessStatusReady: + return http.StatusOK + case ReadinessStatusDegraded: + return http.StatusOK // 206 could also be suitable, but let's avoid false alarms + case ReadinessStatusFatal: + return http.StatusServiceUnavailable + case ReadinessStatusNotReady: + return http.StatusOK // treat as ok to avoid false alarms + default: + return http.StatusOK + } +} + +const ( + // Service is ready to handle requests. + ReadinessStatusReady ReadinessStatus = "ready" + // Service is not ready to handle requests. + ReadinessStatusNotReady ReadinessStatus = "not_ready" + // Service experienced a fatal error and cannot handle requests. + ReadinessStatusFatal ReadinessStatus = "fatal" + // Service is degraded but can still handle requests. + ReadinessStatusDegraded ReadinessStatus = "degraded" +) + +// Returns the worst readiness status from a list of statuses. +func worstStatus(statuses []ReadinessStatus) ReadinessStatus { + if slices.Contains(statuses, ReadinessStatusFatal) { + return ReadinessStatusFatal + } + if slices.Contains(statuses, ReadinessStatusNotReady) { + return ReadinessStatusNotReady + } + if slices.Contains(statuses, ReadinessStatusDegraded) { + return ReadinessStatusDegraded + } + return ReadinessStatusReady +} + +// Readiness check function return type. +type ReadinessReturn struct { + // Status of the readiness check. + Status ReadinessStatus + // Optional message providing additional information about the readiness status. + Message *string +} + +// Function that performs a readiness check. +type ReadinessCheckFn func() ReadinessReturn + +// Individual readiness check. +type ReadinessCheck struct { + // Name of the readiness check. + Name string + // Function that performs the readiness check. + Fn ReadinessCheckFn + // Timeout for the readiness check (default: 5 seconds) + Timeout time.Duration +} + +// System-generated result from an individual readiness check. +type ReadinessCheckResult struct { + // Name of the readiness check. + Name string + // Status of the readiness check. + Status ReadinessStatus + // Optional message providing additional information about the readiness status. + Message *string + // Duration of the readiness check in milliseconds. + Duration int64 +} + +// System readiness status. +type Readiness struct { + // Status of the service readiness. Aggregated as the worst status from + // all individual readiness checks. If no checks have been performed + // yet, the status is ReadinessStatusNotReady. + Status ReadinessStatus + // Start time of the readiness check in milliseconds since Unix epoch. + Start int64 + // Duration of the readiness check in milliseconds. + Duration int64 + // Individual readiness check results. + Details map[string]ReadinessCheckResult +} + +// Performs a full system readiness check given a list of individual checks. +func CheckReadiness(checks []ReadinessCheck) Readiness { + start := time.Now() + details := make(map[string]ReadinessCheckResult) + var statuses []ReadinessStatus + + for _, check := range checks { + checkt0 := time.Now() + result := checkWithTimeout(check) + duration := time.Since(checkt0).Milliseconds() + + details[check.Name] = ReadinessCheckResult{ + Name: check.Name, + Status: result.Status, + Message: result.Message, + Duration: duration, + } + statuses = append(statuses, result.Status) + } + + return Readiness{ + Status: worstStatus(statuses), + Start: start.UnixMilli(), + Duration: time.Since(start).Milliseconds(), + Details: details, + } +} + +// Creates a handler function for readiness HTTP requests. +func ReadinessHandler(checks []ReadinessCheck) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + respondWithResult(w, CheckReadiness(checks)) + } +} + +// Runs a readiness check with a timeout. +func checkWithTimeout(check ReadinessCheck) ReadinessReturn { + timeout := check.Timeout + if timeout == 0 { + timeout = 5 * time.Second + } + + resultCh := make(chan ReadinessReturn, 1) + + go func() { + resultCh <- check.Fn() + }() + + select { + case result := <-resultCh: + return result + case <-time.After(timeout): + msg := "readiness check timed out" + return ReadinessReturn{ + Status: ReadinessStatusFatal, + Message: &msg, + } + } +} + +func respondWithResult(w http.ResponseWriter, result Readiness) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(result.Status.HTTPStatus()) + json.NewEncoder(w).Encode(result) +} + +// ScheduledReadiness performs periodic readiness checks and stores the latest result. +type ScheduledReadiness struct { + checks []ReadinessCheck + interval time.Duration + currentState Readiness + ticker *time.Ticker + quit chan struct{} +} + +// NewScheduledReadiness creates a new ScheduledReadiness instance. +func NewScheduledReadiness(checks []ReadinessCheck, interval time.Duration) *ScheduledReadiness { + return &ScheduledReadiness{ + checks: checks, + interval: interval, + quit: make(chan struct{}), + } +} + +// Start begins the periodic readiness checks. +func (sr *ScheduledReadiness) Start() { + sr.ticker = time.NewTicker(sr.interval) + go func() { + for { + select { + case <-sr.ticker.C: + sr.currentState = CheckReadiness(sr.checks) + case <-sr.quit: + sr.ticker.Stop() + return + } + } + }() +} + +// Stop halts the periodic readiness checks. +func (sr *ScheduledReadiness) Stop() { + close(sr.quit) +} + +// GetCurrentState returns the latest readiness state. +func (sr *ScheduledReadiness) GetCurrentState() Readiness { + return sr.currentState +} + +// Creates a handler function for scheduled readiness HTTP requests. +func (sr *ScheduledReadiness) ReadinessHandler() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + respondWithResult(w, sr.GetCurrentState()) + } +}