153 lines
4.7 KiB
Go
153 lines
4.7 KiB
Go
package manager
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Handler exposes the manager over HTTP:
|
|
//
|
|
// /healthz the process is alive (use this for a container health check)
|
|
// /readyz every repo has synced successfully at least once
|
|
// /status JSON snapshot of every repo
|
|
// /metrics Prometheus text format
|
|
func (m *Manager) Handler(version string) http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
|
writeText(w, http.StatusOK, "ok\n")
|
|
})
|
|
|
|
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
|
|
var pending []string
|
|
for _, s := range m.Statuses() {
|
|
if s.LastSuccess.IsZero() {
|
|
pending = append(pending, s.Name)
|
|
}
|
|
}
|
|
if len(pending) > 0 {
|
|
writeText(w, http.StatusServiceUnavailable,
|
|
"awaiting first successful sync: "+strings.Join(pending, ", ")+"\n")
|
|
return
|
|
}
|
|
writeText(w, http.StatusOK, "ready\n")
|
|
})
|
|
|
|
mux.HandleFunc("GET /status", func(w http.ResponseWriter, r *http.Request) {
|
|
statuses := m.Statuses()
|
|
body := struct {
|
|
Version string `json:"version"`
|
|
Uptime string `json:"uptime"`
|
|
Repos []Status `json:"repos"`
|
|
Healthy bool `json:"healthy"`
|
|
Failing int `json:"failing"`
|
|
Reported string `json:"reported_at"`
|
|
}{
|
|
Version: version,
|
|
Uptime: m.Uptime().Round(time.Second).String(),
|
|
Repos: statuses,
|
|
Healthy: true,
|
|
Reported: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
for _, s := range statuses {
|
|
if s.Failures > 0 {
|
|
body.Failing++
|
|
body.Healthy = false
|
|
}
|
|
}
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
_ = enc.Encode(body)
|
|
})
|
|
|
|
mux.HandleFunc("GET /metrics", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# HELP syncbot_build_info Version of the running binary.\n")
|
|
fmt.Fprintf(&b, "# TYPE syncbot_build_info gauge\n")
|
|
fmt.Fprintf(&b, "syncbot_build_info{version=%q} 1\n", version)
|
|
fmt.Fprintf(&b, "# HELP syncbot_uptime_seconds Time since start.\n")
|
|
fmt.Fprintf(&b, "# TYPE syncbot_uptime_seconds gauge\n")
|
|
fmt.Fprintf(&b, "syncbot_uptime_seconds %s\n", seconds(m.Uptime()))
|
|
|
|
metric(&b, "syncbot_sync_total", "counter", "Sync cycles started.")
|
|
for _, s := range m.Statuses() {
|
|
fmt.Fprintf(&b, "syncbot_sync_total{repo=%q} %d\n", s.Name, s.Syncs)
|
|
}
|
|
metric(&b, "syncbot_push_total", "counter", "Cycles that pushed to dst.")
|
|
for _, s := range m.Statuses() {
|
|
fmt.Fprintf(&b, "syncbot_push_total{repo=%q} %d\n", s.Name, s.Pushes)
|
|
}
|
|
metric(&b, "syncbot_consecutive_failures", "gauge", "Failed cycles since the last success.")
|
|
for _, s := range m.Statuses() {
|
|
fmt.Fprintf(&b, "syncbot_consecutive_failures{repo=%q} %d\n", s.Name, s.Failures)
|
|
}
|
|
metric(&b, "syncbot_refs", "gauge", "Mirrored refs.")
|
|
for _, s := range m.Statuses() {
|
|
fmt.Fprintf(&b, "syncbot_refs{repo=%q} %d\n", s.Name, s.Refs)
|
|
}
|
|
metric(&b, "syncbot_last_success_timestamp_seconds", "gauge", "Unix time of the last successful sync.")
|
|
for _, s := range m.Statuses() {
|
|
fmt.Fprintf(&b, "syncbot_last_success_timestamp_seconds{repo=%q} %d\n", s.Name, unix(s.LastSuccess))
|
|
}
|
|
metric(&b, "syncbot_last_duration_seconds", "gauge", "Duration of the last sync cycle.")
|
|
for _, s := range m.Statuses() {
|
|
fmt.Fprintf(&b, "syncbot_last_duration_seconds{repo=%q} %s\n", s.Name,
|
|
strconv.FormatFloat(float64(s.LastDurMS)/1000, 'f', 3, 64))
|
|
}
|
|
_, _ = w.Write([]byte(b.String()))
|
|
})
|
|
|
|
return mux
|
|
}
|
|
|
|
// Serve runs the HTTP endpoint until ctx is cancelled.
|
|
func (m *Manager) Serve(ctx context.Context, addr, version string, log *slog.Logger) error {
|
|
srv := &http.Server{
|
|
Addr: addr,
|
|
Handler: m.Handler(version),
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
go func() {
|
|
<-ctx.Done()
|
|
shutdown, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_ = srv.Shutdown(shutdown)
|
|
}()
|
|
|
|
log.Info("http listening", "addr", addr,
|
|
"endpoints", "/healthz /readyz /status /metrics")
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func metric(b *strings.Builder, name, kind, help string) {
|
|
fmt.Fprintf(b, "# HELP %s %s\n# TYPE %s %s\n", name, help, name, kind)
|
|
}
|
|
|
|
func seconds(d time.Duration) string {
|
|
return strconv.FormatFloat(d.Seconds(), 'f', 3, 64)
|
|
}
|
|
|
|
func unix(t time.Time) int64 {
|
|
if t.IsZero() {
|
|
return 0
|
|
}
|
|
return t.Unix()
|
|
}
|
|
|
|
func writeText(w http.ResponseWriter, code int, body string) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.WriteHeader(code)
|
|
_, _ = w.Write([]byte(body))
|
|
}
|