Initial commit: stats-gateway gRPC service + dashboard + CLI

This commit is contained in:
2026-05-05 21:04:32 +05:00
commit 4059e94759
44 changed files with 2513 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"context"
"flag"
"log"
"time"
"github.com/bergabruh/stats-gateway/internal/probes"
"github.com/bergabruh/stats-gateway/internal/storage"
)
func main() {
dbPath := flag.String("db", "/var/lib/stats-gateway/stats.db", "sqlite path")
flag.Parse()
st, err := storage.Open(*dbPath)
if err != nil {
log.Fatalf("open db: %v", err)
}
defer st.Close()
targets := []struct {
Node string
URL string
}{
{"fl", "https://fl.speedtest.projectshitpost.fun/"},
{"pl1", "https://speedtest.projectshitpost.fun/"},
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
for _, t := range targets {
r, err := probes.ProbeHTTP(ctx, t.URL)
if err != nil {
log.Printf("probe %s: %v", t.Node, err)
continue
}
if err := st.InsertSample(ctx, t.Node, r); err != nil {
log.Printf("store %s: %v", t.Node, err)
continue
}
log.Printf("probe %s ok: http=%.1fms tls=%.1fms",
t.Node, r.HTTPLatencyMs, r.TLSHandshakeMs)
}
}
+38
View File
@@ -0,0 +1,38 @@
package main
import (
"flag"
"log"
"net"
"google.golang.org/grpc"
statsv1 "github.com/bergabruh/stats-gateway/gen/stats/v1"
"github.com/bergabruh/stats-gateway/internal/service"
"github.com/bergabruh/stats-gateway/internal/storage"
)
func main() {
addr := flag.String("addr", "127.0.0.1:50051", "listen address")
dbPath := flag.String("db", "/var/lib/stats-gateway/stats.db", "sqlite path")
flag.Parse()
st, err := storage.Open(*dbPath)
if err != nil {
log.Fatalf("storage: %v", err)
}
defer st.Close()
lis, err := net.Listen("tcp", *addr)
if err != nil {
log.Fatalf("listen: %v", err)
}
srv := grpc.NewServer()
statsv1.RegisterSpeedStatusServer(srv, service.New(st))
log.Printf("stats-gateway listening on %s", *addr)
if err := srv.Serve(lis); err != nil {
log.Fatalf("serve: %v", err)
}
}
+89
View File
@@ -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)
}