130 lines
3.8 KiB
Go
130 lines
3.8 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// handleHealthz reports liveness. It does NOT check dependencies — by design,
|
|
// a process that can answer this is "alive" even if its DB is wedged. Use
|
|
// /readyz for dependency health.
|
|
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"status": "alive",
|
|
"time": time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// handleReadyz reports readiness. Returns 503 if either:
|
|
// - MarkReady has not been called, OR
|
|
// - the SQLite database cannot be pinged within 2s.
|
|
//
|
|
// Distinguishing these cases in the response body helps operators triage.
|
|
func (s *Server) handleReadyz(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
if !s.ready.Load() {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]any{
|
|
"status": "not_ready",
|
|
"reason": "daemon not marked ready",
|
|
})
|
|
return
|
|
}
|
|
if err := s.db.PingContext(ctx); err != nil {
|
|
s.log.Warn("readyz db ping failed",
|
|
slog.String("component", "daemon"),
|
|
slog.String("error", err.Error()))
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]any{
|
|
"status": "not_ready",
|
|
"reason": "db ping failed",
|
|
})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"status": "ready",
|
|
"db": "ok",
|
|
})
|
|
}
|
|
|
|
// handleStatus returns a small diagnostic JSON blob. Cheap to call; does
|
|
// NOT touch the database unless we want a DB status check, in which case
|
|
// the ping is bounded by 2s.
|
|
func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
dbStatus := "ok"
|
|
if err := s.db.PingContext(ctx); err != nil {
|
|
dbStatus = "error"
|
|
s.log.Warn("status db ping failed",
|
|
slog.String("component", "daemon"),
|
|
slog.String("error", err.Error()))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"version": Version,
|
|
"phase": "5-health-checks",
|
|
"milestone": "v0.1",
|
|
"db": dbStatus,
|
|
"ready": s.ready.Load(),
|
|
"time": time.Now().UTC().Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// writeJSON encodes body as JSON with the given status code.
|
|
// Errors during encoding are logged but not surfaced — we cannot write
|
|
// another header after the response has started.
|
|
func writeJSON(w http.ResponseWriter, code int, body any) {
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(code)
|
|
_ = json.NewEncoder(w).Encode(body)
|
|
}
|
|
|
|
// writeError emits a uniform error envelope: {"error": "<message>"}.
|
|
func writeError(w http.ResponseWriter, code int, msg string) {
|
|
writeJSON(w, code, map[string]string{"error": msg})
|
|
}
|
|
|
|
// loggingMiddleware wraps the mux with a structured access log. It does
|
|
// NOT log request/response bodies (could contain secrets); just method,
|
|
// path, status, and duration.
|
|
func loggingMiddleware(log *slog.Logger, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
start := time.Now()
|
|
ww := &statusRecorder{ResponseWriter: w, status: 200}
|
|
next.ServeHTTP(ww, r)
|
|
log.Info("http",
|
|
slog.String("method", r.Method),
|
|
slog.String("path", r.URL.Path),
|
|
slog.Int("status", ww.status),
|
|
slog.Duration("dur", time.Since(start)),
|
|
slog.String("remote", r.RemoteAddr),
|
|
)
|
|
})
|
|
}
|
|
|
|
type statusRecorder struct {
|
|
http.ResponseWriter
|
|
status int
|
|
}
|
|
|
|
func (s *statusRecorder) WriteHeader(code int) {
|
|
s.status = code
|
|
s.ResponseWriter.WriteHeader(code)
|
|
}
|