Files
knox/internal/db/metrics.go
T
david 876d2aa45f 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
2026-08-29 06:06:49 -07:00

108 lines
3.0 KiB
Go

package db
// MetricsSnapshot holds the scrape-time gauges derived from the database.
type MetricsSnapshot struct {
Observations int
ObservationsLast24h int
BySource map[string]int
Entries int
Projects int
Sessions int
PendingReflections int
ThreadsByStatus map[string]int
Peers int
ByOriginNode map[string]int // node_id → observation count
KnowledgeVector map[string]int64 // node_id → max hcl
EarliestObservation string
}
// MetricsSnapshot computes database-derived gauges for Prometheus scraping.
// All queries are cheap aggregations; nothing is stored or mutated.
func (k *KnoxDB) MetricsSnapshot() (*MetricsSnapshot, error) {
s := &MetricsSnapshot{
BySource: make(map[string]int),
ThreadsByStatus: make(map[string]int),
ByOriginNode: make(map[string]int),
}
if err := k.db.QueryRow("SELECT COUNT(*) FROM observations").Scan(&s.Observations); err != nil {
return nil, err
}
if err := k.db.QueryRow("SELECT COUNT(*) FROM observations WHERE collected_at > datetime('now', '-1 day')").Scan(&s.ObservationsLast24h); err != nil {
return nil, err
}
if err := k.db.QueryRow("SELECT COUNT(*) FROM entries").Scan(&s.Entries); err != nil {
return nil, err
}
if err := k.db.QueryRow("SELECT COUNT(DISTINCT project) FROM entries").Scan(&s.Projects); err != nil {
return nil, err
}
if err := k.db.QueryRow("SELECT COUNT(*) FROM sessions").Scan(&s.Sessions); err != nil {
return nil, err
}
if err := k.db.QueryRow("SELECT COUNT(*) FROM sessions WHERE indexed=0").Scan(&s.PendingReflections); err != nil {
return nil, err
}
if err := k.db.QueryRow("SELECT COUNT(*) FROM peers").Scan(&s.Peers); err != nil {
return nil, err
}
if err := k.db.QueryRow("SELECT COALESCE(MIN(collected_at),'') FROM observations").Scan(&s.EarliestObservation); err != nil {
return nil, err
}
rows, err := k.db.Query("SELECT COALESCE(source_id,'unknown'), COUNT(*) FROM observations GROUP BY source_id")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var src string
var n int
if err := rows.Scan(&src, &n); err != nil {
return nil, err
}
s.BySource[src] = n
}
if err := rows.Err(); err != nil {
return nil, err
}
rows2, err := k.db.Query("SELECT COALESCE(status,'active'), COUNT(*) FROM threads GROUP BY status")
if err != nil {
return nil, err
}
defer rows2.Close()
for rows2.Next() {
var st string
var n int
if err := rows2.Scan(&st, &n); err != nil {
return nil, err
}
s.ThreadsByStatus[st] = n
}
if err := rows2.Err(); err != nil {
return nil, err
}
rows3, err := k.db.Query("SELECT node_id, COUNT(*) FROM observations WHERE node_id<>'' GROUP BY node_id")
if err != nil {
return nil, err
}
defer rows3.Close()
for rows3.Next() {
var nid string
var n int
if err := rows3.Scan(&nid, &n); err != nil {
return nil, err
}
s.ByOriginNode[nid] = n
}
if err := rows3.Err(); err != nil {
return nil, err
}
if s.KnowledgeVector, err = k.KnowledgeVector(); err != nil {
return nil, err
}
return s, nil
}