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"` } type snapshot struct { GeneratedAt string `json:"generated_at"` Nodes map[string]nodeData `json:"nodes"` } 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 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) }