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": ""}. 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) }