Files
relata/internal/observability/metrics.go
T
2026-08-11 10:40:10 +01:00

145 lines
5.0 KiB
Go

package observability
import (
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
)
// Metrics holds Relata's application metrics.
//
// These describe the health of the *service* — request rates, job outcomes,
// provider latency. They deliberately contain no stored-knowledge cardinality:
// no item IDs, no entity names, no query strings ever become label values.
type Metrics struct {
Registry *prometheus.Registry
HTTPRequests *prometheus.CounterVec
HTTPDuration *prometheus.HistogramVec
HTTPInFlight prometheus.Gauge
JobsProcessed *prometheus.CounterVec
JobDuration *prometheus.HistogramVec
JobsQueued *prometheus.GaugeVec
AIRequests *prometheus.CounterVec
AIDuration *prometheus.HistogramVec
StorageOps *prometheus.CounterVec
StorageDuration *prometheus.HistogramVec
IngestItems *prometheus.CounterVec
ModelInvalid *prometheus.CounterVec
AuthAttempts *prometheus.CounterVec
SearchOps *prometheus.CounterVec
SearchLatency prometheus.Histogram
}
var (
metricsOnce sync.Once
metricsInst *Metrics
)
// NewMetrics returns the process-wide metrics registry, creating it once.
func NewMetrics() *Metrics {
metricsOnce.Do(func() {
reg := prometheus.NewRegistry()
reg.MustRegister(collectors.NewGoCollector())
reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
m := &Metrics{Registry: reg}
m.HTTPRequests = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "relata_http_requests_total",
Help: "HTTP requests by route pattern, method and status class.",
}, []string{"route", "method", "status"})
m.HTTPDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "relata_http_request_duration_seconds",
Help: "HTTP request duration in seconds.",
Buckets: []float64{0.005, 0.025, 0.1, 0.25, 1, 2.5, 10, 30},
}, []string{"route", "method"})
m.HTTPInFlight = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "relata_http_in_flight_requests",
Help: "HTTP requests currently being served.",
})
m.JobsProcessed = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "relata_jobs_processed_total",
Help: "Background jobs finished, by type and outcome.",
}, []string{"type", "outcome"})
m.JobDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "relata_job_duration_seconds",
Help: "Background job execution duration in seconds.",
Buckets: []float64{0.05, 0.25, 1, 5, 15, 60, 180, 600},
}, []string{"type"})
m.JobsQueued = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "relata_jobs_queued",
Help: "Jobs currently in each status.",
}, []string{"status"})
m.AIRequests = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "relata_ai_requests_total",
Help: "AI provider calls by provider, operation and outcome.",
}, []string{"provider", "operation", "outcome"})
m.AIDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "relata_ai_request_duration_seconds",
Help: "AI provider call duration in seconds.",
Buckets: []float64{0.1, 0.5, 1, 2.5, 5, 15, 60, 180},
}, []string{"provider", "operation"})
m.StorageOps = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "relata_storage_operations_total",
Help: "Storage operations by store, operation and outcome.",
}, []string{"store", "operation", "outcome"})
m.StorageDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
Name: "relata_storage_operation_duration_seconds",
Help: "Storage operation duration in seconds.",
Buckets: []float64{0.001, 0.005, 0.025, 0.1, 0.5, 2, 10},
}, []string{"store", "operation"})
m.IngestItems = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "relata_ingest_items_total",
Help: "Items accepted for ingestion by source type and outcome.",
}, []string{"source_type", "outcome"})
m.ModelInvalid = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "relata_model_invalid_output_total",
Help: "Model responses rejected by schema validation, by task and reason.",
}, []string{"task", "reason"})
m.AuthAttempts = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "relata_auth_attempts_total",
Help: "Authentication attempts by outcome.",
}, []string{"outcome"})
m.SearchOps = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "relata_search_operations_total",
Help: "Search operations by mode and outcome.",
}, []string{"mode", "outcome"})
m.SearchLatency = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "relata_search_duration_seconds",
Help: "End-to-end search duration in seconds.",
Buckets: []float64{0.01, 0.05, 0.15, 0.5, 1.5, 5, 20},
})
reg.MustRegister(
m.HTTPRequests, m.HTTPDuration, m.HTTPInFlight,
m.JobsProcessed, m.JobDuration, m.JobsQueued,
m.AIRequests, m.AIDuration,
m.StorageOps, m.StorageDuration,
m.IngestItems, m.ModelInvalid, m.AuthAttempts,
m.SearchOps, m.SearchLatency,
)
metricsInst = m
})
return metricsInst
}