fleet-manager: checkpoint 4 — InfluxDB metrics gateway (write + Flux read)
The manager is the ONLY thing that talks to InfluxDB, both directions; hosts hold
no token and never reach it.
- influx.go: influxSink implements MetricsSink — each agent HealthSnapshot is
written as a fleet_health point, identity (serial/hostnqn/starship) as TAGS
from the AUTHENTICATED stream (a host can only write its own series), telemetry
as fields. QueryField answers the SPA's read: a Flux query built with a strict
FIELD WHITELIST + hex-serial guard + parsed-duration range, so no caller input
is interpolated raw (injection-safe). Server-side influxdb-client-go/v2.
- admin.go: GET /api/hosts/{serial}/metrics?field=&range= -> time-series JSON;
503 when InfluxDB isn't wired, 400 on a bad range, 404 for unknown host.
- main.go: sink created when FLEET_MGR_INFLUX_URL+_TOKEN set, fed to the agent
hub; nil -> no-op sink so the manager runs without metrics.
Tests: snapshotPoint tags/fields/time, fluxQuery contents + %q quoting, and
QueryField rejects a non-hex serial + non-whitelisted field. build/test/vet/gofmt
clean. Live InfluxDB path validated in the compose bench (ckpt 6).
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
@@ -35,7 +36,8 @@ func (s *server) routes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("POST /api/enrol/{id}/approve", s.handleEnrolApprove)
|
||||
mux.HandleFunc("POST /api/enrol/{id}/deny", s.handleEnrolDeny)
|
||||
|
||||
// METRICS routes (/api/hosts/{serial}/metrics) — checkpoint 4.
|
||||
// METRICS — time-series for one host/field, read from InfluxDB via the manager.
|
||||
mux.HandleFunc("GET /api/hosts/{serial}/metrics", s.handleMetrics)
|
||||
}
|
||||
|
||||
func (s *server) handleListHosts(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -141,6 +143,41 @@ func (s *server) handleEnrolApprove(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, born)
|
||||
}
|
||||
|
||||
// ---- METRICS (InfluxDB time-series via the manager gateway) ---------------
|
||||
|
||||
func (s *server) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
if s.influx == nil {
|
||||
writeErr(w, http.StatusServiceUnavailable, "metrics not configured (no InfluxDB wired)")
|
||||
return
|
||||
}
|
||||
serial := r.PathValue("serial")
|
||||
if _, ok := s.store.Get(serial); !ok {
|
||||
writeErr(w, http.StatusNotFound, "no such host")
|
||||
return
|
||||
}
|
||||
field := r.URL.Query().Get("field")
|
||||
if field == "" {
|
||||
field = "tmpfs_used_pct"
|
||||
}
|
||||
rng := time.Hour
|
||||
if rs := r.URL.Query().Get("range"); rs != "" {
|
||||
d, err := time.ParseDuration(rs)
|
||||
if err != nil || d <= 0 {
|
||||
writeErr(w, http.StatusBadRequest, "invalid range (use e.g. 30m, 1h, 24h)")
|
||||
return
|
||||
}
|
||||
rng = d
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
pts, err := s.influx.QueryField(ctx, serial, field, rng)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadGateway, "metrics query: "+err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"serial": serial, "field": field, "range": rng.String(), "points": pts})
|
||||
}
|
||||
|
||||
func (s *server) handleEnrolDeny(w http.ResponseWriter, r *http.Request) {
|
||||
if s.broker == nil {
|
||||
writeErr(w, http.StatusServiceUnavailable, "authorise plane not configured (no enrol-broker wired)")
|
||||
|
||||
Reference in New Issue
Block a user