Files
silo-server/internal/api/handlers/health.go
T

133 lines
4.0 KiB
Go

// Package handlers provides HTTP handler functions for the Silo API.
package handlers
import (
"context"
"encoding/json"
"net/http"
)
// PGPinger is the interface used to check PostgreSQL connectivity.
// *pgxpool.Pool satisfies this interface.
type PGPinger interface {
Ping(ctx context.Context) error
}
// S3HealthChecker is the interface used to check S3 bucket accessibility.
// *s3client.Client satisfies this interface.
type S3HealthChecker interface {
HeadBucket(ctx context.Context, bucket string) error
Bucket() string
}
// healthStatus represents the JSON response for the health endpoint.
//
// ServerName and ServerID identify this Silo instance. They are
// populated from server configuration and are stable across restarts,
// which allows clients (notably the iOS/tvOS multi-server picker) to
// display a friendly name and detect the same server reached via
// different URLs.
type healthStatus struct {
Status string `json:"status"`
ServerName string `json:"server_name,omitempty"`
ServerID string `json:"server_id,omitempty"`
}
// readyStatus represents the JSON response for the readiness endpoint.
type readyStatus struct {
Status string `json:"status"`
Postgres *bool `json:"postgres,omitempty"`
S3 *bool `json:"s3,omitempty"`
}
// HealthHandler responds to liveness probes and advertises the server's
// identity. Identity fields (ServerName, ServerID) are injected at
// construction time and reused for every request.
type HealthHandler struct {
serverName string
serverID string
}
// NewHealthHandler creates a HealthHandler with the given identity
// fields. Empty strings are allowed: the corresponding JSON fields are
// omitted from the response.
func NewHealthHandler(serverName, serverID string) *HealthHandler {
return &HealthHandler{
serverName: serverName,
serverID: serverID,
}
}
// ServeHTTP responds with 200 OK and a JSON body indicating the service
// is alive, along with server identity fields. This endpoint does not
// check any dependencies.
func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(healthStatus{
Status: "ok",
ServerName: h.serverName,
ServerID: h.serverID,
})
}
// ReadyHandler checks the health of PostgreSQL and S3 dependencies.
type ReadyHandler struct {
pg PGPinger
s3 S3HealthChecker
}
// NewReadyHandler creates a ReadyHandler with the given PG and S3 dependencies.
// Either dependency may be nil: a nil PG pinger means PG is unavailable,
// and a nil S3 checker means S3 is not configured (treated as healthy).
func NewReadyHandler(pg PGPinger, s3 S3HealthChecker) *ReadyHandler {
return &ReadyHandler{pg: pg, s3: s3}
}
// ServeHTTP checks both PostgreSQL and S3 health and responds with the
// combined status. Returns 200 if all checks pass, 503 if any fail.
func (h *ReadyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
pgOK := h.checkPostgres(ctx)
s3OK := h.checkS3(ctx)
status := readyStatus{
Status: "ok",
}
if !pgOK || !s3OK {
status.Status = "error"
status.Postgres = new(pgOK)
status.S3 = new(s3OK)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
_ = json.NewEncoder(w).Encode(status)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(status)
}
// checkPostgres pings the PG pool. Returns false if the pool is nil or
// the ping fails.
func (h *ReadyHandler) checkPostgres(ctx context.Context) bool {
if h.pg == nil {
return false
}
return h.pg.Ping(ctx) == nil
}
// checkS3 performs a HeadBucket call on the S3 client. Returns true if
// no S3 client is configured (S3 is optional). Returns false if the
// HeadBucket call fails.
func (h *ReadyHandler) checkS3(ctx context.Context) bool {
if h.s3 == nil {
return true
}
return h.s3.HeadBucket(ctx, h.s3.Bucket()) == nil
}