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:
2026-08-29 06:06:49 -07:00
parent 25a7112d8a
commit 876d2aa45f
10 changed files with 494 additions and 20 deletions
+16 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/david/knox/internal/db"
"github.com/david/knox/internal/ingest"
"github.com/david/knox/internal/metrics"
"github.com/fsnotify/fsnotify"
)
@@ -24,6 +25,7 @@ type Watcher struct {
vault string
debounce time.Duration
fileIngesters []ingest.Ingester
metrics *metrics.Metrics
}
func New(kdb *db.KnoxDB, dirs []string) *Watcher {
@@ -40,6 +42,7 @@ func New(kdb *db.KnoxDB, dirs []string) *Watcher {
ingest.NewLogIngester(),
ingest.NewSkillsIngester(),
},
metrics: metrics.New(kdb, "knox"),
}
}
@@ -53,7 +56,7 @@ func (w *Watcher) Start() error {
// Start the gossip server first so peers can reach us while the initial
// seed is still ingesting. Vault/dirs are logged after the seed below.
gossipAddr := ListenAddr()
node := &Node{Kdb: w.knoxDB, Name: "knox", Addr: gossipAddr}
node := &Node{Kdb: w.knoxDB, Name: "knox", Addr: gossipAddr, Metrics: w.metrics}
srv := &http.Server{Addr: gossipAddr, Handler: node.Handler()}
go func() {
log.Printf("[knox] gossip listening on %s", gossipAddr)
@@ -61,6 +64,17 @@ func (w *Watcher) Start() error {
log.Printf("[knox] gossip server: %v", err)
}
}()
// Prometheus scraping on a dedicated port (KNOX_METRICS_ADDR).
metricsAddr := MetricsAddr()
metricsSrv := &http.Server{Addr: metricsAddr, Handler: node.Metrics.Handler()}
go func() {
log.Printf("[knox] metrics listening on %s", metricsAddr)
if err := metricsSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("[knox] metrics server: %v", err)
}
}()
if peers := PeerAddrs(); len(peers) > 0 {
log.Printf("[knox] gossip peers: %v", peers)
}
@@ -447,7 +461,7 @@ func (w *Watcher) syncGossip() {
before = n
}
Run(w.knoxDB, peers)
Run(w.knoxDB, w.metrics, peers)
// If new observations arrived, reconcile to pick up entries/threads they
// imply (deterministic log → derived rebuild).