139 lines
3.7 KiB
Go
139 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/bergabruh/stats-gateway/internal/storage"
|
|
)
|
|
|
|
type sample struct {
|
|
TS string `json:"ts"`
|
|
HTTPMs float64 `json:"http_ms"`
|
|
TLSMs float64 `json:"tls_ms"`
|
|
TCPMs float64 `json:"tcp_ms"`
|
|
}
|
|
|
|
type nodeData struct {
|
|
Recent []sample `json:"recent"`
|
|
}
|
|
|
|
// SpeedtestSnapshot exposes recent speedtest rows + day/week/month median
|
|
// aggregates per node. Recent uses storage.SpeedTest's capitalized field
|
|
// names (no JSON tags on the struct); the frontend reads them as-is.
|
|
type SpeedtestSnapshot struct {
|
|
Recent []storage.SpeedTest `json:"recent"`
|
|
AggDay map[string]storage.Agg `json:"agg_day"`
|
|
AggWeek map[string]storage.Agg `json:"agg_week"`
|
|
AggMonth map[string]storage.Agg `json:"agg_month"`
|
|
GeneratedAt int64 `json:"generated_at"`
|
|
}
|
|
|
|
type snapshot struct {
|
|
GeneratedAt string `json:"generated_at"`
|
|
Nodes map[string]nodeData `json:"nodes"`
|
|
Speedtest *SpeedtestSnapshot `json:"speedtest,omitempty"`
|
|
}
|
|
|
|
// buildSpeedtestSnapshot collects the last 200 speedtest rows + per-node
|
|
// medians over 24h/7d/30d windows. Aggregate errors are logged but
|
|
// non-fatal — the rest of the snapshot still ships.
|
|
func buildSpeedtestSnapshot(ctx context.Context, st *storage.Store) (*SpeedtestSnapshot, error) {
|
|
recent, err := st.ListRecentSpeedTests(ctx, 200)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now().Unix()
|
|
day, week, month := now-86400, now-7*86400, now-30*86400
|
|
aggDay, errDay := st.AggregateSpeedTestsSince(ctx, day)
|
|
if errDay != nil {
|
|
log.Printf("aggregate day: %v", errDay)
|
|
}
|
|
aggWeek, errWeek := st.AggregateSpeedTestsSince(ctx, week)
|
|
if errWeek != nil {
|
|
log.Printf("aggregate week: %v", errWeek)
|
|
}
|
|
aggMonth, errMonth := st.AggregateSpeedTestsSince(ctx, month)
|
|
if errMonth != nil {
|
|
log.Printf("aggregate month: %v", errMonth)
|
|
}
|
|
return &SpeedtestSnapshot{
|
|
Recent: recent,
|
|
AggDay: aggDay,
|
|
AggWeek: aggWeek,
|
|
AggMonth: aggMonth,
|
|
GeneratedAt: now,
|
|
}, nil
|
|
}
|
|
|
|
func main() {
|
|
dbPath := flag.String("db", "/var/lib/stats-gateway/stats.db", "")
|
|
out := flag.String("out", "/var/www/stats-site/data/snapshot.json", "")
|
|
limitPerNode := flag.Int("limit", 288, "max samples per node (288 = ~24h at 5min interval)")
|
|
flag.Parse()
|
|
|
|
st, err := storage.Open(*dbPath)
|
|
if err != nil {
|
|
log.Fatalf("open db: %v", err)
|
|
}
|
|
defer st.Close()
|
|
|
|
s := snapshot{
|
|
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
|
Nodes: map[string]nodeData{},
|
|
}
|
|
ctx := context.Background()
|
|
for _, node := range []string{"fl", "pl1"} {
|
|
samples, err := st.QueryRecent(ctx, node, *limitPerNode, time.Time{})
|
|
if err != nil {
|
|
log.Printf("query %s: %v", node, err)
|
|
continue
|
|
}
|
|
nd := nodeData{Recent: make([]sample, 0, len(samples))}
|
|
for _, sm := range samples {
|
|
nd.Recent = append(nd.Recent, sample{
|
|
TS: sm.Timestamp.Format(time.RFC3339Nano),
|
|
HTTPMs: sm.HTTPLatencyMs,
|
|
TLSMs: sm.TLSHandshakeMs,
|
|
TCPMs: sm.TCPRttMs,
|
|
})
|
|
}
|
|
s.Nodes[node] = nd
|
|
}
|
|
|
|
if stSnap, err := buildSpeedtestSnapshot(ctx, st); err != nil {
|
|
log.Printf("build speedtest snapshot: %v", err)
|
|
} else {
|
|
s.Speedtest = stSnap
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(*out), 0755); err != nil {
|
|
log.Fatalf("mkdir %s: %v", filepath.Dir(*out), err)
|
|
}
|
|
|
|
// Atomic write: write to .tmp then rename.
|
|
tmp := *out + ".tmp"
|
|
f, err := os.Create(tmp)
|
|
if err != nil {
|
|
log.Fatalf("create %s: %v", tmp, err)
|
|
}
|
|
enc := json.NewEncoder(f)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(s); err != nil {
|
|
f.Close()
|
|
os.Remove(tmp)
|
|
log.Fatalf("encode: %v", err)
|
|
}
|
|
f.Close()
|
|
if err := os.Rename(tmp, *out); err != nil {
|
|
log.Fatalf("rename: %v", err)
|
|
}
|
|
|
|
log.Printf("snapshot: nodes=%d → %s", len(s.Nodes), *out)
|
|
}
|