internal: initial metrics aggregator
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I9cdd6e2b33bb65182568db9db4460bc46a6a6964
This commit is contained in:
parent
ce848ed6f0
commit
bc4d3fed53
2 changed files with 293 additions and 0 deletions
101
internal/aggregate/metrics.go
Normal file
101
internal/aggregate/metrics.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package aggregate
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"notashelf.dev/watchdog/internal/config"
|
||||
)
|
||||
|
||||
// Records analytics events as Prometheus metrics
|
||||
type MetricsAggregator struct {
|
||||
registry *PathRegistry
|
||||
cfg config.Config
|
||||
pageviews *prometheus.CounterVec
|
||||
customEvents *prometheus.CounterVec
|
||||
pathOverflow prometheus.Counter
|
||||
}
|
||||
|
||||
// Creates a new metrics aggregator with dynamic labels based on config
|
||||
func NewMetricsAggregator(registry *PathRegistry, cfg config.Config) *MetricsAggregator {
|
||||
// Build label names based on what's enabled in config
|
||||
labels := []string{"path"} // path is always included
|
||||
|
||||
if cfg.Site.Collect.Country {
|
||||
labels = append(labels, "country")
|
||||
}
|
||||
|
||||
if cfg.Site.Collect.Device {
|
||||
labels = append(labels, "device")
|
||||
}
|
||||
|
||||
if cfg.Site.Collect.Referrer != "off" {
|
||||
labels = append(labels, "referrer")
|
||||
}
|
||||
|
||||
pageviews := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "web_pageviews_total",
|
||||
Help: "Total number of pageviews",
|
||||
},
|
||||
labels,
|
||||
)
|
||||
|
||||
customEvents := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "web_custom_events_total",
|
||||
Help: "Total number of custom events",
|
||||
},
|
||||
[]string{"event"},
|
||||
)
|
||||
|
||||
pathOverflow := prometheus.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "web_path_overflow_total",
|
||||
Help: "Paths rejected due to cardinality limit",
|
||||
},
|
||||
)
|
||||
|
||||
return &MetricsAggregator{
|
||||
registry: registry,
|
||||
cfg: cfg,
|
||||
pageviews: pageviews,
|
||||
customEvents: customEvents,
|
||||
pathOverflow: pathOverflow,
|
||||
}
|
||||
}
|
||||
|
||||
// Records a pageview with the configured dimensions
|
||||
func (m *MetricsAggregator) RecordPageview(path, country, device, referrer string) {
|
||||
// Build label values in the same order as label names
|
||||
labels := prometheus.Labels{"path": path}
|
||||
|
||||
if m.cfg.Site.Collect.Country {
|
||||
labels["country"] = country
|
||||
}
|
||||
|
||||
if m.cfg.Site.Collect.Device {
|
||||
labels["device"] = device
|
||||
}
|
||||
|
||||
if m.cfg.Site.Collect.Referrer != "off" {
|
||||
labels["referrer"] = referrer
|
||||
}
|
||||
|
||||
m.pageviews.With(labels).Inc()
|
||||
}
|
||||
|
||||
// Records a custom event
|
||||
func (m *MetricsAggregator) RecordCustomEvent(eventName string) {
|
||||
m.customEvents.With(prometheus.Labels{"event": eventName}).Inc()
|
||||
}
|
||||
|
||||
// Records a path that was rejected due to cardinality limits
|
||||
func (m *MetricsAggregator) RecordPathOverflow() {
|
||||
m.pathOverflow.Inc()
|
||||
}
|
||||
|
||||
// Registers all metrics with the provided Prometheus registry
|
||||
func (m *MetricsAggregator) MustRegister(reg prometheus.Registerer) {
|
||||
reg.MustRegister(m.pageviews)
|
||||
reg.MustRegister(m.customEvents)
|
||||
reg.MustRegister(m.pathOverflow)
|
||||
}
|
||||
192
internal/aggregate/metrics_test.go
Normal file
192
internal/aggregate/metrics_test.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package aggregate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/testutil"
|
||||
"notashelf.dev/watchdog/internal/config"
|
||||
)
|
||||
|
||||
func TestMetricsAggregator_RecordPageview(t *testing.T) {
|
||||
registry := NewPathRegistry(100)
|
||||
cfg := config.Config{
|
||||
Site: config.SiteConfig{
|
||||
Collect: config.CollectConfig{
|
||||
Pageviews: true,
|
||||
Country: true,
|
||||
Device: true,
|
||||
Referrer: "domain",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
agg := NewMetricsAggregator(registry, cfg)
|
||||
|
||||
// Record pageview with all dimensions
|
||||
agg.RecordPageview("/home", "US", "desktop", "google.com")
|
||||
|
||||
// Verify metric was recorded
|
||||
expected := `
|
||||
# HELP web_pageviews_total Total number of pageviews
|
||||
# TYPE web_pageviews_total counter
|
||||
web_pageviews_total{country="US",device="desktop",path="/home",referrer="google.com"} 1
|
||||
`
|
||||
if err := testutil.CollectAndCompare(agg.pageviews, strings.NewReader(expected)); err != nil {
|
||||
t.Errorf("unexpected metric value: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsAggregator_RecordPageview_MinimalDimensions(t *testing.T) {
|
||||
registry := NewPathRegistry(100)
|
||||
cfg := config.Config{
|
||||
Site: config.SiteConfig{
|
||||
Collect: config.CollectConfig{
|
||||
Pageviews: true,
|
||||
Country: false,
|
||||
Device: false,
|
||||
Referrer: "off",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
agg := NewMetricsAggregator(registry, cfg)
|
||||
|
||||
// Record pageview with only path
|
||||
agg.RecordPageview("/home", "", "", "")
|
||||
|
||||
// Verify metric was recorded
|
||||
expected := `
|
||||
# HELP web_pageviews_total Total number of pageviews
|
||||
# TYPE web_pageviews_total counter
|
||||
web_pageviews_total{path="/home"} 1
|
||||
`
|
||||
if err := testutil.CollectAndCompare(agg.pageviews, strings.NewReader(expected)); err != nil {
|
||||
t.Errorf("unexpected metric value: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsAggregator_PathOverflow(t *testing.T) {
|
||||
// Create registry with limit of 2
|
||||
registry := NewPathRegistry(2)
|
||||
cfg := config.Config{
|
||||
Site: config.SiteConfig{
|
||||
Collect: config.CollectConfig{
|
||||
Pageviews: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
agg := NewMetricsAggregator(registry, cfg)
|
||||
|
||||
// Add two paths successfully
|
||||
registry.Add("/path1")
|
||||
registry.Add("/path2")
|
||||
|
||||
// Try to add third path - should be rejected
|
||||
if registry.Add("/path3") {
|
||||
t.Error("expected path to be rejected")
|
||||
}
|
||||
|
||||
// Record overflow
|
||||
agg.RecordPathOverflow()
|
||||
|
||||
// Verify overflow metric
|
||||
expected := `
|
||||
# HELP web_path_overflow_total Paths rejected due to cardinality limit
|
||||
# TYPE web_path_overflow_total counter
|
||||
web_path_overflow_total 1
|
||||
`
|
||||
if err := testutil.CollectAndCompare(agg.pathOverflow, strings.NewReader(expected)); err != nil {
|
||||
t.Errorf("unexpected metric value: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsAggregator_RecordCustomEvent(t *testing.T) {
|
||||
registry := NewPathRegistry(100)
|
||||
cfg := config.Config{
|
||||
Site: config.SiteConfig{
|
||||
Collect: config.CollectConfig{
|
||||
Pageviews: true,
|
||||
},
|
||||
CustomEvents: []string{"signup", "purchase"},
|
||||
},
|
||||
}
|
||||
|
||||
agg := NewMetricsAggregator(registry, cfg)
|
||||
|
||||
// Record custom event
|
||||
agg.RecordCustomEvent("signup")
|
||||
|
||||
// Verify metric was recorded
|
||||
expected := `
|
||||
# HELP web_custom_events_total Total number of custom events
|
||||
# TYPE web_custom_events_total counter
|
||||
web_custom_events_total{event="signup"} 1
|
||||
`
|
||||
if err := testutil.CollectAndCompare(agg.customEvents, strings.NewReader(expected)); err != nil {
|
||||
t.Errorf("unexpected metric value: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsAggregator_RecordCustomEvent_MultipleEvents(t *testing.T) {
|
||||
registry := NewPathRegistry(100)
|
||||
cfg := config.Config{
|
||||
Site: config.SiteConfig{
|
||||
Collect: config.CollectConfig{
|
||||
Pageviews: true,
|
||||
},
|
||||
CustomEvents: []string{"signup", "purchase"},
|
||||
},
|
||||
}
|
||||
|
||||
agg := NewMetricsAggregator(registry, cfg)
|
||||
|
||||
// Record multiple events
|
||||
agg.RecordCustomEvent("signup")
|
||||
agg.RecordCustomEvent("signup")
|
||||
agg.RecordCustomEvent("purchase")
|
||||
|
||||
// Verify metrics
|
||||
if err := testutil.CollectAndCompare(agg.customEvents, strings.NewReader(`
|
||||
# HELP web_custom_events_total Total number of custom events
|
||||
# TYPE web_custom_events_total counter
|
||||
web_custom_events_total{event="purchase"} 1
|
||||
web_custom_events_total{event="signup"} 2
|
||||
`)); err != nil {
|
||||
t.Errorf("unexpected metric value: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsAggregator_MustRegister(t *testing.T) {
|
||||
registry := NewPathRegistry(100)
|
||||
cfg := config.Config{
|
||||
Site: config.SiteConfig{
|
||||
Collect: config.CollectConfig{
|
||||
Pageviews: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
promRegistry := prometheus.NewRegistry()
|
||||
agg := NewMetricsAggregator(registry, cfg)
|
||||
|
||||
// Register metrics
|
||||
agg.MustRegister(promRegistry)
|
||||
|
||||
// Record some metrics to ensure they show up
|
||||
agg.RecordPageview("/test", "", "", "")
|
||||
agg.RecordPathOverflow()
|
||||
|
||||
// Verify metrics can be gathered
|
||||
metrics, err := promRegistry.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to gather metrics: %v", err)
|
||||
}
|
||||
|
||||
// Should have at least pageviews and path overflow metrics
|
||||
if len(metrics) < 2 {
|
||||
t.Errorf("expected at least 2 metric families, got %d", len(metrics))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue