mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-06 16:01:28 +00:00
109 lines
2.7 KiB
Go
109 lines
2.7 KiB
Go
// Package sysstatus runs cheap liveness probes against the platform's backing
|
|
// services (Postgres, Redis, Kafka, schema registry, realtime, ...) so the
|
|
// admin dashboard can show component health at a glance. Checks are wired as
|
|
// closures in cmd/backend/main.go where the concrete clients live; this
|
|
// package only knows how to run them in parallel with a bounded timeout.
|
|
package sysstatus
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// perCheckTimeout bounds each probe so one dead dependency can't stall the
|
|
// whole status response.
|
|
const perCheckTimeout = 3 * time.Second
|
|
|
|
type check struct {
|
|
name string
|
|
fn func(ctx context.Context) error
|
|
}
|
|
|
|
// Checker holds the registered probes.
|
|
type Checker struct {
|
|
checks []check
|
|
}
|
|
|
|
// New creates an empty checker.
|
|
func New() *Checker {
|
|
return &Checker{}
|
|
}
|
|
|
|
// Add registers a named probe. Nil funcs are ignored.
|
|
func (c *Checker) Add(name string, fn func(ctx context.Context) error) {
|
|
if fn == nil {
|
|
return
|
|
}
|
|
c.checks = append(c.checks, check{name: name, fn: fn})
|
|
}
|
|
|
|
// Result is one probe outcome.
|
|
type Result struct {
|
|
Name string `json:"name"`
|
|
OK bool `json:"ok"`
|
|
LatencyMS int64 `json:"latency_ms"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// Run executes every probe in parallel and returns results in registration
|
|
// order.
|
|
func (c *Checker) Run(ctx context.Context) []Result {
|
|
results := make([]Result, len(c.checks))
|
|
var wg sync.WaitGroup
|
|
for i, ch := range c.checks {
|
|
wg.Add(1)
|
|
go func(i int, ch check) {
|
|
defer wg.Done()
|
|
cctx, cancel := context.WithTimeout(ctx, perCheckTimeout)
|
|
defer cancel()
|
|
start := time.Now()
|
|
err := ch.fn(cctx)
|
|
r := Result{Name: ch.name, OK: err == nil, LatencyMS: time.Since(start).Milliseconds()}
|
|
if err != nil {
|
|
r.Error = err.Error()
|
|
}
|
|
results[i] = r
|
|
}(i, ch)
|
|
}
|
|
wg.Wait()
|
|
return results
|
|
}
|
|
|
|
// HTTPCheck probes a URL and treats any response below 500 as healthy (auth
|
|
// walls still prove the service is up).
|
|
func HTTPCheck(url string) func(ctx context.Context) error {
|
|
return func(ctx context.Context) error {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode >= 500 {
|
|
return fmt.Errorf("status %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// TCPCheck probes the first address of a comma-separated host:port list.
|
|
func TCPCheck(addrs string) func(ctx context.Context) error {
|
|
addr := strings.TrimSpace(strings.Split(addrs, ",")[0])
|
|
return func(ctx context.Context) error {
|
|
var d net.Dialer
|
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Close()
|
|
}
|
|
}
|