mirror of
https://github.com/luxfi/monitoring.git
synced 2026-10-30 15:24:48 +00:00
The validator-health exporter and the rpc-tip-gate were both stdlib-only
Python scripts living in ConfigMaps on a stock python image. That choice
bought one thing — no build step — and it cost the backend standard: the
serving side of this stack is Go, and these two are serving processes.
internal/luxd — what one node says it believes (tip, finalized, peers,
bootstrapped, health-with-check-names). /v1 only.
internal/kube — the four apiserver calls fleet health makes. Not
client-go: a scheme, a codec and an informer machinery
to unmarshal four fields is not a dependency, it is a
liability.
internal/fleet — what the numbers MEAN, separated from the process that
serves them, so a metric's meaning can be tested
without a cluster.
Proven, not asserted. Both implementations were pointed at one fake
cluster and diffed:
exporter : 234 lines, 22 metric families — byte-for-byte identical.
tip-gate : same 5 decisions, same 3 merge-patches, byte-for-byte.
The differential earned its keep: the gate had been ported to
eth_getBlockByNumber when the deployed one calls eth_blockNumber. Same
answer today, a different call tomorrow. Fixed to match.
Two deliberate improvements over the Python, neither a scrape-visible
change: exposition series are sorted (Python's thread-completion order
made two scrapes of a steady fleet textually different), and the gate
refuses to start on a missing setting rather than defaulting quietly.
Co-authored-by: Hanzo Dev <[email protected]>
76 lines
2.2 KiB
Go
76 lines
2.2 KiB
Go
// Lux validator-health exporter.
|
|
//
|
|
// luxd serves an empty metrics endpoint, so this sources validator/consensus
|
|
// health from the JSON-RPC + info + health APIs and the Kubernetes API instead,
|
|
// and exposes a Prometheus endpoint that a sidecar vmagent scrapes and
|
|
// remote-writes to the central Hanzo VictoriaMetrics.
|
|
//
|
|
// The process is a poll loop and a text handler. Everything that decides what a
|
|
// number means lives in internal/fleet, so it can be tested without a cluster.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/luxfi/monitoring/internal/fleet"
|
|
)
|
|
|
|
func main() {
|
|
e := fleet.NewExporter(fleet.Config{
|
|
Replicas: envInt("REPLICAS", 5),
|
|
PollInterval: envSeconds("POLL_INTERVAL", 15*time.Second),
|
|
RPCTimeout: envSeconds("RPC_TIMEOUT", 3*time.Second),
|
|
Headless: env("HEADLESS_SVC", "luxd-headless"),
|
|
})
|
|
go e.Run(context.Background())
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/metrics", func(w http.ResponseWriter, _ *http.Request) {
|
|
body := e.Exposition()
|
|
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
|
|
_, _ = w.Write([]byte(body))
|
|
})
|
|
// "/" answers too: a readiness probe and a human curl should not have to
|
|
// agree on a spelling. Anything else is a 404, so a typo'd probe path fails
|
|
// loudly instead of passing forever.
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/", "/health", "/healthz":
|
|
_, _ = w.Write([]byte("ok"))
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
})
|
|
|
|
addr := ":" + env("LISTEN_PORT", "9101")
|
|
log.Fatal((&http.Server{Addr: addr, Handler: mux, ReadHeaderTimeout: 5 * time.Second}).ListenAndServe())
|
|
}
|
|
|
|
func env(k, def string) string {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func envInt(k string, def int) int {
|
|
if n, err := strconv.Atoi(os.Getenv(k)); err == nil {
|
|
return n
|
|
}
|
|
return def
|
|
}
|
|
|
|
// envSeconds reads a bare number of seconds, which is what the deployment sets.
|
|
func envSeconds(k string, def time.Duration) time.Duration {
|
|
if f, err := strconv.ParseFloat(os.Getenv(k), 64); err == nil && f > 0 {
|
|
return time.Duration(f * float64(time.Second))
|
|
}
|
|
return def
|
|
}
|