Files
Assistant/runtime/health/server.go
m 8aa0123047
CI / test (push) Successful in 31s
CI / test (pull_request) Successful in 35s
chore: format readiness health server
2026-08-22 20:55:48 +03:00

138 lines
3.6 KiB
Go

package health
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"time"
)
type Handler struct {
Runtime string
SemanticGeneration int
ArchitectureGeneration int
}
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
"runtime": h.Runtime,
"semantic_generation": h.SemanticGeneration,
"architecture_generation": h.ArchitectureGeneration,
})
}
type Probe struct {
Name string
Check func(context.Context) error
Optional bool
Timeout time.Duration
}
type Result struct {
Name string `json:"name"`
State string `json:"state"`
Reason string `json:"reason"`
Detail string `json:"detail,omitempty"`
}
type ReadinessHandler struct {
Runtime string
Probes []Probe
Timeout time.Duration
}
func (h ReadinessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
timeout := h.Timeout
if timeout <= 0 {
timeout = 2 * time.Second
}
results := make([]Result, 0, len(h.Probes))
ready := true
for _, probe := range h.Probes {
result := Result{Name: probe.Name, State: "READY", Reason: "AVAILABLE"}
if probe.Check == nil {
result.State, result.Reason, result.Detail = "DEGRADED", "PROBE_NOT_CONFIGURED", "probe is not configured"
} else {
probeTimeout := timeout
if probe.Timeout > 0 {
probeTimeout = probe.Timeout
}
ctx, cancel := context.WithTimeout(r.Context(), probeTimeout)
err := probe.Check(ctx)
cancel()
if err != nil {
result.State, result.Reason, result.Detail = "DEGRADED", "DEPENDENCY_UNAVAILABLE", err.Error()
if !probe.Optional {
ready = false
}
}
}
if result.State != "READY" && !probe.Optional {
ready = false
}
results = append(results, result)
}
status := "ready"
code := http.StatusOK
if !ready {
status, code = "degraded", http.StatusServiceUnavailable
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]any{"status": status, "runtime": h.Runtime, "checks": results})
}
func HTTPProbe(client *http.Client, endpoint string) Probe {
return HTTPProbeWithHeaders(client, endpoint, nil)
}
func HTTPProbeWithHeaders(client *http.Client, endpoint string, headers map[string]string) Probe {
return Probe{Name: endpoint, Check: func(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return err
}
for name, value := range headers {
req.Header.Set(name, value)
}
response, err := client.Do(req)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("status=%s", response.Status)
}
return nil
}}
}
// OptionalHTTPProbe reports dependency degradation without making the whole
// runtime unavailable. This is used for opportunistic fleet members when a
// qualified local fallback remains available.
func OptionalHTTPProbe(client *http.Client, endpoint string) Probe {
probe := HTTPProbe(client, endpoint)
probe.Optional = true
return probe
}
func OptionalHTTPProbeWithHeaders(client *http.Client, endpoint string, headers map[string]string) Probe {
probe := HTTPProbeWithHeaders(client, endpoint, headers)
probe.Optional = true
return probe
}
func TCPProbe(name, host, port string) Probe {
return Probe{Name: name, Check: func(ctx context.Context) error {
connection, err := (&net.Dialer{}).DialContext(ctx, "tcp", net.JoinHostPort(host, port))
if err != nil {
return err
}
return connection.Close()
}}
}