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
+32 -4
View File
@@ -19,14 +19,16 @@ import (
"time"
"github.com/david/knox/internal/db"
"github.com/david/knox/internal/metrics"
)
const defaultPort = "8931"
type Node struct {
Kdb *db.KnoxDB
Name string
Addr string // advertised base URL, e.g. http://192.168.1.20:8931
Kdb *db.KnoxDB
Name string
Addr string // advertised base URL, e.g. http://192.168.1.20:8931
Metrics *metrics.Metrics
}
// pingResponse is the anti-entropy summary returned by /v1/ping.
@@ -269,7 +271,9 @@ func (c *Client) Diff() (*DiffSummary, error) {
// with any one node reveals who else is in the swarm, and those nodes are then
// swept too. There is no relay of observations — only membership is shared; each
// node pulls/pushes directly with every other node it learns about.
func Run(kdb *db.KnoxDB, static []string) {
//
// m, when non-nil, receives gossip event counters (nil for one-shot CLI runs).
func Run(kdb *db.KnoxDB, m *metrics.Metrics, static []string) {
myID := kdb.NodeID()
// Seed the work queue with static config plus persisted discoveries.
@@ -292,6 +296,9 @@ func Run(kdb *db.KnoxDB, static []string) {
p, err := c.Ping()
if err != nil {
log.Printf("[gossip] ping %s: %v", addr, err)
if m != nil {
m.IncrementErrors()
}
continue
}
if p.NodeID == myID {
@@ -323,13 +330,22 @@ func Run(kdb *db.KnoxDB, static []string) {
n, err := c.Pull(remoteNode, localHCL, kdb)
if err != nil {
log.Printf("[gossip] pull %s@%s: %v", remoteNode, addr, err)
if m != nil {
m.IncrementErrors()
}
continue
}
pulled += n
}
}
if m != nil {
m.IncrementPull(pulled)
}
pushed, _ := c.Push(kdb, p.Vector)
if m != nil {
m.IncrementPush(pushed)
}
if err := kdb.UpsertPeer(p.NodeID, addr, p.Name, vectorMax(p.Vector)); err != nil {
log.Printf("[gossip] peer upsert: %v", err)
@@ -368,6 +384,18 @@ func ListenAddr() (addr string) {
return "localhost:" + defaultPort
}
const defaultMetricsPort = "8932"
// MetricsAddr returns the Prometheus scrape address (KNOX_METRICS_ADDR or
// default). It is a separate port from gossip so scraping never contends with
// the peer protocol.
func MetricsAddr() (addr string) {
if addr = os.Getenv("KNOX_METRICS_ADDR"); addr != "" {
return addr
}
return "localhost:" + defaultMetricsPort
}
// PeerAddrs returns the configured peer list (KNOX_PEERS, comma-separated).
func PeerAddrs() []string {
raw := os.Getenv("KNOX_PEERS")