feat: Prometheus metrics endpoint for knox nodes
Refs #2 - /metrics served on a dedicated port (KNOX_METRICS_ADDR, default localhost:8932) via prometheus/client_golang, with Go runtime + process collectors - DB-derived gauges refreshed per scrape: observations by source, last-24h observations, entries, projects, sessions, pending reflections, threads by status, peers, observations by origin node, knowledge vector (max hcl per node) - live gossip counters (pulls/pushes, observations pulled/pushed, errors) incremented during the anti-entropy sweep; Run accepts an optional metrics handle (nil for one-shot CLI) - knox_node_info{node_id,name} for scrape identification - internal/metrics package + db MetricsSnapshot; tests for snapshot, scrape output, and counter increments
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/david/knox/internal/db"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
)
|
||||
|
||||
func tmpKdb(t *testing.T) *db.KnoxDB {
|
||||
t.Helper()
|
||||
k, err := db.Open(filepath.Join(t.TempDir(), "index.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { k.Close() })
|
||||
return k
|
||||
}
|
||||
|
||||
func seed(t *testing.T, k *db.KnoxDB, src string, n int) {
|
||||
t.Helper()
|
||||
for i := 0; i < n; i++ {
|
||||
_, _, err := k.RecordObservation(db.ObservationRecord{
|
||||
Fingerprint: "fp-" + src + "-" + string(rune('a'+i)),
|
||||
SourceID: src,
|
||||
SourcePath: src,
|
||||
Project: "test",
|
||||
ContentType: "test",
|
||||
Title: src,
|
||||
Summary: "s",
|
||||
CreatedAt: "2026-08-29T00:00:00Z",
|
||||
Confidence: 0.9,
|
||||
IngesterVersion: "itest/v1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsSnapshot(t *testing.T) {
|
||||
k := tmpKdb(t)
|
||||
seed(t, k, "git", 2)
|
||||
seed(t, k, "browser-history", 3)
|
||||
|
||||
s, err := k.MetricsSnapshot()
|
||||
if err != nil {
|
||||
t.Fatalf("snapshot: %v", err)
|
||||
}
|
||||
if s.Observations != 5 {
|
||||
t.Errorf("observations = %d, want 5", s.Observations)
|
||||
}
|
||||
if s.BySource["git"] != 2 || s.BySource["browser-history"] != 3 {
|
||||
t.Errorf("by source = %v", s.BySource)
|
||||
}
|
||||
if s.KnowledgeVector[k.NodeID()] == 0 {
|
||||
t.Errorf("knowledge vector missing own node")
|
||||
}
|
||||
if s.ByOriginNode[k.NodeID()] != 5 {
|
||||
t.Errorf("by origin node = %v", s.ByOriginNode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsScrape(t *testing.T) {
|
||||
k := tmpKdb(t)
|
||||
seed(t, k, "git", 2)
|
||||
|
||||
m := New(k, "testnode")
|
||||
h := m.Handler()
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest("GET", "/metrics", nil))
|
||||
|
||||
body, _ := io.ReadAll(rec.Body)
|
||||
out := string(body)
|
||||
for _, want := range []string{
|
||||
`knox_node_info{name="testnode"`,
|
||||
`knox_observations_total{source_id="git"} 2`,
|
||||
`knox_gossip_pulls_total 0`,
|
||||
"go_goroutines",
|
||||
"process_cpu_seconds_total",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("scrape output missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsCounters(t *testing.T) {
|
||||
k := tmpKdb(t)
|
||||
m := New(k, "t")
|
||||
|
||||
// Manually drive counters through the Metrics API.
|
||||
m.IncrementPull(3)
|
||||
m.IncrementPush(7)
|
||||
m.IncrementErrors()
|
||||
|
||||
if got := testutil.ToFloat64(m.pullsTotal); got != 1 {
|
||||
t.Errorf("pullsTotal = %v, want 1", got)
|
||||
}
|
||||
if got := testutil.ToFloat64(m.obsPulledTotal); got != 3 {
|
||||
t.Errorf("obsPulled = %v, want 3", got)
|
||||
}
|
||||
if got := testutil.ToFloat64(m.obsPushedTotal); got != 7 {
|
||||
t.Errorf("obsPushed = %v, want 7", got)
|
||||
}
|
||||
if got := testutil.ToFloat64(m.errorsTotal); got != 1 {
|
||||
t.Errorf("errorsTotal = %v, want 1", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user