90 lines
2.4 KiB
Go
90 lines
2.4 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"net/http"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/jobs"
|
||
|
|
)
|
||
|
|
|
||
|
|
const healthServiceName = "api"
|
||
|
|
|
||
|
|
// dbPinger is satisfied by *pgxpool.Pool; kept narrow for unit tests.
|
||
|
|
type dbPinger interface {
|
||
|
|
Ping(ctx context.Context) error
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleHealthz(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
JSON(w, http.StatusOK, map[string]any{
|
||
|
|
"status": "ok",
|
||
|
|
"service": healthServiceName,
|
||
|
|
"maintenance": s.Config.MaintenanceMode,
|
||
|
|
"read_only": s.Config.ReadOnlyMode,
|
||
|
|
"hypercare": s.Config.HypercareMode,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) handleReadyz(w http.ResponseWriter, r *http.Request) {
|
||
|
|
var pinger dbPinger
|
||
|
|
var prober jobs.HeartbeatQuerier
|
||
|
|
if s.Pool != nil {
|
||
|
|
pinger = s.Pool
|
||
|
|
prober = s.Pool
|
||
|
|
}
|
||
|
|
s.writeReadyz(w, r, pinger, prober)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *Server) writeReadyz(w http.ResponseWriter, r *http.Request, pinger dbPinger, prober jobs.HeartbeatQuerier) {
|
||
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
ok, check, errMsg := databaseReady(ctx, pinger)
|
||
|
|
checks := map[string]string{"database": check}
|
||
|
|
body := map[string]any{
|
||
|
|
"status": "ready",
|
||
|
|
"service": healthServiceName,
|
||
|
|
"maintenance": s.Config.MaintenanceMode,
|
||
|
|
"read_only": s.Config.ReadOnlyMode,
|
||
|
|
"hypercare": s.Config.HypercareMode,
|
||
|
|
"checks": checks,
|
||
|
|
}
|
||
|
|
if !ok {
|
||
|
|
body["status"] = "not_ready"
|
||
|
|
body["error"] = errMsg
|
||
|
|
JSON(w, http.StatusServiceUnavailable, body)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
probe := jobs.ProbeWorkerReadiness(ctx, prober, jobs.DefaultHeartbeatStaleAfter)
|
||
|
|
checks["worker"] = probe.WorkerCheck
|
||
|
|
checks["queue"] = probe.QueueCheck
|
||
|
|
body["queue_pending"] = probe.PendingJobs
|
||
|
|
if probe.LastSeenAgeS >= 0 {
|
||
|
|
body["worker_last_seen_age_s"] = probe.LastSeenAgeS
|
||
|
|
}
|
||
|
|
if !probe.OK {
|
||
|
|
body["status"] = "not_ready"
|
||
|
|
body["error"] = probe.ErrMsg
|
||
|
|
if probe.Reason != "" {
|
||
|
|
body["reason"] = probe.Reason
|
||
|
|
}
|
||
|
|
JSON(w, http.StatusServiceUnavailable, body)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
JSON(w, http.StatusOK, body)
|
||
|
|
}
|
||
|
|
|
||
|
|
// databaseReady pings Postgres for readiness. check is "ok", "unavailable", or "fail".
|
||
|
|
// errMsg is empty when ok; never includes driver detail (safe for public probes).
|
||
|
|
func databaseReady(ctx context.Context, p dbPinger) (ok bool, check string, errMsg string) {
|
||
|
|
if p == nil {
|
||
|
|
return false, "unavailable", "database pool unavailable"
|
||
|
|
}
|
||
|
|
if err := p.Ping(ctx); err != nil {
|
||
|
|
return false, "fail", "database ping failed"
|
||
|
|
}
|
||
|
|
return true, "ok", ""
|
||
|
|
}
|