Implement radar-telemt-dcs aggregation endpoint and UI integration
- Added new API route `/api/agg/radar-telemt-dcs` to aggregate DC status data from multiple upstreams, including metrics like coverage percentage and RTT. - Implemented handler logic in `handlers.go` and corresponding tests in `handlers_test.go` to ensure correct data retrieval and response formatting. - Updated the frontend to fetch and display radar DC data, enhancing the user interface with a new section for Telemt ME snapshots. - Enhanced documentation in `AGGREGATE.md` and `README.md` to reflect the new functionality and usage details.
This commit is contained in:
@@ -113,6 +113,8 @@ func (h *Handler) dispatch(w http.ResponseWriter, r *http.Request, sub string) {
|
||||
h.handleUsers(w, r)
|
||||
case sub == "fleet-status":
|
||||
h.handleFleetStatus(w, r)
|
||||
case sub == "radar-telemt-dcs":
|
||||
h.handleRadarTelemtDcs(w, r)
|
||||
case sub == "incidents":
|
||||
h.handleIncidents(w, r)
|
||||
case strings.HasPrefix(sub, "user/"):
|
||||
@@ -275,6 +277,25 @@ func (h *Handler) handleUsers(w http.ResponseWriter, r *http.Request) {
|
||||
writeAggOK(w, partial, data)
|
||||
}
|
||||
|
||||
func (h *Handler) handleRadarTelemtDcs(w http.ResponseWriter, r *http.Request) {
|
||||
aliases, err := h.resolveAliases(r)
|
||||
if err != nil {
|
||||
writeBadRequest(w, err)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
|
||||
defer cancel()
|
||||
data := FetchRadarTelemtDcs(ctx, h.Client, h.Parsed, aliases)
|
||||
partial := false
|
||||
for _, s := range data.Servers {
|
||||
if !s.OK {
|
||||
partial = true
|
||||
break
|
||||
}
|
||||
}
|
||||
writeAggOK(w, partial, data)
|
||||
}
|
||||
|
||||
func (h *Handler) handleFleetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
aliases, err := h.resolveAliases(r)
|
||||
if err != nil {
|
||||
|
||||
@@ -113,6 +113,65 @@ func TestHandlerFleetStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerRadarTelemtDcs(t *testing.T) {
|
||||
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/stats/dcs" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"ok": true,
|
||||
"data": map[string]any{
|
||||
"middle_proxy_enabled": true,
|
||||
"generated_at_epoch_secs": 1,
|
||||
"dcs": []map[string]any{{
|
||||
"dc": 1, "rtt_ms": 42.5, "coverage_pct": 100.0,
|
||||
"alive_writers": 3, "required_writers": 3, "load": 0,
|
||||
}},
|
||||
},
|
||||
"revision": "rd",
|
||||
})
|
||||
}))
|
||||
defer up.Close()
|
||||
|
||||
cfg := &config.Config{
|
||||
Servers: []config.Server{
|
||||
{Alias: "test", BaseURL: up.URL, PathPrefix: "/v1"},
|
||||
},
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
parsed, err := cfg.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := NewHandler(parsed, up.Client(), nil, 0)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/agg/radar-telemt-dcs?aliases=test", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var env struct {
|
||||
OK bool `json:"ok"`
|
||||
Data RadarTelemtDcsData `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &env); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !env.OK || len(env.Data.Servers) != 1 {
|
||||
t.Fatalf("envelope: %+v", env)
|
||||
}
|
||||
row := env.Data.Servers[0]
|
||||
if row.Alias != "test" || !row.OK || row.Data == nil || !row.Data.MiddleProxyEnabled {
|
||||
t.Fatalf("row: %+v", row)
|
||||
}
|
||||
if len(row.Data.Dcs) != 1 || row.Data.Dcs[0].DC != 1 {
|
||||
t.Fatalf("dcs: %+v", row.Data.Dcs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerUserOne(t *testing.T) {
|
||||
up := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/stats/users" {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package aggregate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/telemt/telemt-api/internal/config"
|
||||
)
|
||||
|
||||
const statsDcsPath = "stats/dcs"
|
||||
|
||||
// FetchRadarTelemtDcs calls GET /v1/stats/dcs on each Telemt in parallel.
|
||||
func FetchRadarTelemtDcs(ctx context.Context, client *http.Client, parsed *config.Parsed, aliases []string) RadarTelemtDcsData {
|
||||
if len(aliases) == 0 {
|
||||
return RadarTelemtDcsData{}
|
||||
}
|
||||
rows := make([]RadarTelemtDcsServer, len(aliases))
|
||||
var wg sync.WaitGroup
|
||||
for i, alias := range aliases {
|
||||
i, alias := i, alias
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
data, meta := FetchTelemtGET[DcStatusPayload](ctx, client, parsed, alias, statsDcsPath)
|
||||
row := RadarTelemtDcsServer{
|
||||
Alias: alias,
|
||||
OK: meta.OK,
|
||||
HTTPStatus: meta.HTTPStatus,
|
||||
LatencyMs: meta.LatencyMs,
|
||||
Error: meta.Error,
|
||||
Revision: meta.Revision,
|
||||
}
|
||||
if meta.OK {
|
||||
row.Data = &data
|
||||
}
|
||||
rows[i] = row
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].Alias < rows[j].Alias })
|
||||
return RadarTelemtDcsData{Servers: rows}
|
||||
}
|
||||
@@ -206,3 +206,37 @@ type TopUserByUniqueIPs struct {
|
||||
Username string `json:"username"`
|
||||
UniqueIPs uint64 `json:"unique_ips"`
|
||||
}
|
||||
|
||||
// DcStatusRow mirrors Telemt GET /v1/stats/dcs data.dcs[] (subset for radar UI).
|
||||
type DcStatusRow struct {
|
||||
DC int `json:"dc"`
|
||||
RttMs *float64 `json:"rtt_ms"`
|
||||
CoveragePct float64 `json:"coverage_pct"`
|
||||
AliveWriters int `json:"alive_writers"`
|
||||
RequiredWriters int `json:"required_writers"`
|
||||
Load int `json:"load"`
|
||||
}
|
||||
|
||||
// DcStatusPayload mirrors Telemt GET /v1/stats/dcs data object.
|
||||
type DcStatusPayload struct {
|
||||
MiddleProxyEnabled bool `json:"middle_proxy_enabled"`
|
||||
Reason *string `json:"reason"`
|
||||
GeneratedAtEpochSecs uint64 `json:"generated_at_epoch_secs"`
|
||||
Dcs []DcStatusRow `json:"dcs"`
|
||||
}
|
||||
|
||||
// RadarTelemtDcsServer is one upstream stats/dcs outcome for /api/agg/radar-telemt-dcs.
|
||||
type RadarTelemtDcsServer struct {
|
||||
Alias string `json:"alias"`
|
||||
OK bool `json:"ok"`
|
||||
HTTPStatus int `json:"http_status,omitempty"`
|
||||
LatencyMs int64 `json:"latency_ms,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Revision string `json:"revision,omitempty"`
|
||||
Data *DcStatusPayload `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// RadarTelemtDcsData is aggregate payload for /api/agg/radar-telemt-dcs.
|
||||
type RadarTelemtDcsData struct {
|
||||
Servers []RadarTelemtDcsServer `json:"servers"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user