138 lines
3.6 KiB
Go
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()
|
|
}}
|
|
}
|