rdev/internal/handlers/health.go
jordan 72d16929ca feat: Implement hexagonal architecture with services, webhooks, queue, and telemetry
Major refactoring to hexagonal (ports & adapters) architecture:

- Add service layer (apikey_service, project_service) for business logic
- Add webhook system with dispatcher and delivery tracking
- Add command queue with priority-based processing
- Add rate limiting with sliding window algorithm
- Add audit logging for command execution
- Add OpenTelemetry integration (traces, metrics, spans)
- Add circuit breaker for fault tolerance
- Add cached repository wrapper for performance
- Add comprehensive validation package
- Add Kubernetes client integration for pod management
- Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks)
- Add network policy and PodDisruptionBudget for k8s
- Remove legacy executor and projects/registry packages
- Untrack secrets.yaml (now managed via envault)
- Add coverage.out to .gitignore
- Add e2e test infrastructure with docker-compose
- Add comprehensive documentation (API, architecture, operations, plans)
- Add golangci-lint config and pre-commit hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:57:46 -07:00

156 lines
3.8 KiB
Go

// Package handlers provides HTTP handlers for the rdev API.
package handlers
import (
"context"
"database/sql"
"net/http"
"strings"
"time"
"github.com/orchard9/rdev/pkg/api"
k8sclient "k8s.io/client-go/kubernetes"
)
// HealthHandler handles health and readiness checks.
type HealthHandler struct {
serviceName string
db *sql.DB
k8sClient *k8sclient.Clientset
}
// NewHealthHandler creates a new health handler with dependencies.
func NewHealthHandler(serviceName string, db *sql.DB, k8sClient *k8sclient.Clientset) *HealthHandler {
return &HealthHandler{
serviceName: serviceName,
db: db,
k8sClient: k8sClient,
}
}
// Health returns a simple liveness check.
// This should be lightweight and only fail if the process is unhealthy.
// GET /health
func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) {
api.WriteSuccess(w, r, map[string]string{
"status": "ok",
"service": h.serviceName,
})
}
// Ready returns a readiness check with dependency health.
// This checks all required dependencies (database, k8s) and returns
// 503 if any are unhealthy.
// GET /ready
func (h *HealthHandler) Ready(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
checks := make(map[string]CheckResult)
allHealthy := true
// Database check
if h.db != nil {
dbCheck := h.checkDatabase(ctx)
checks["database"] = dbCheck
if !dbCheck.Healthy {
allHealthy = false
}
}
// Kubernetes check
if h.k8sClient != nil {
k8sCheck := h.checkKubernetes(ctx)
checks["kubernetes"] = k8sCheck
if !k8sCheck.Healthy {
allHealthy = false
}
}
response := ReadinessResponse{
Status: "ready",
Service: h.serviceName,
Checks: checks,
}
if !allHealthy {
response.Status = "not_ready"
api.WriteError(w, r, http.StatusServiceUnavailable, "NOT_READY",
"Service not ready - one or more checks failed")
return
}
api.WriteSuccess(w, r, response)
}
// checkDatabase performs a database health check.
func (h *HealthHandler) checkDatabase(ctx context.Context) CheckResult {
start := time.Now()
err := h.db.PingContext(ctx)
latency := time.Since(start)
if err != nil {
return CheckResult{
Healthy: false,
Message: "connection failed: " + err.Error(),
Latency: latency.String(),
LastCheck: time.Now().UTC(),
}
}
return CheckResult{
Healthy: true,
Message: "connected",
Latency: latency.String(),
LastCheck: time.Now().UTC(),
}
}
// checkKubernetes performs a Kubernetes API health check.
func (h *HealthHandler) checkKubernetes(ctx context.Context) CheckResult {
start := time.Now()
// Try to get server version - lightweight API call
_, err := h.k8sClient.Discovery().ServerVersion()
latency := time.Since(start)
if err != nil {
// Check if it's a timeout or connection error
msg := err.Error()
if strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline") {
msg = "connection timeout"
} else if strings.Contains(msg, "refused") {
msg = "connection refused"
}
return CheckResult{
Healthy: false,
Message: msg,
Latency: latency.String(),
LastCheck: time.Now().UTC(),
}
}
return CheckResult{
Healthy: true,
Message: "connected",
Latency: latency.String(),
LastCheck: time.Now().UTC(),
}
}
// CheckResult represents the result of a health check.
type CheckResult struct {
Healthy bool `json:"healthy"`
Message string `json:"message"`
Latency string `json:"latency,omitempty"`
LastCheck time.Time `json:"last_check"`
}
// ReadinessResponse is the response for the /ready endpoint.
type ReadinessResponse struct {
Status string `json:"status"`
Service string `json:"service"`
Checks map[string]CheckResult `json:"checks,omitempty"`
}