Initial commit: stats-gateway gRPC service + dashboard + CLI
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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