Initial commit: stats-gateway gRPC service + dashboard + CLI
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user