From 2ef2dabf9327b4a2b34554dd5d2c13716cb82b5d Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 1 Mar 2026 19:23:19 +0300 Subject: [PATCH] internal/aggergate: make HLL state path configurable Signed-off-by: NotAShelf Change-Id: I8ff8ef25ad945aae918bea97ee39d7ea6a6a6964 --- config.example.yaml | 3 + internal/aggregate/metrics.go | 2 +- internal/aggregate/metrics_test.go | 88 +++++++++++++++++++ internal/config/config.go | 4 + web/beacon.test.html | 133 +++++++++++++++-------------- 5 files changed, 167 insertions(+), 63 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 3afbd2d..c3ad980 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -98,3 +98,6 @@ server: metrics_path: "/metrics" # Event ingestion endpoint ingestion_path: "/api/event" + # Path to persist HyperLogLog state (default: /var/lib/watchdog/hll.state) + # Ensures unique visitor counts survive restarts + state_path: "/var/lib/watchdog/hll.state" diff --git a/internal/aggregate/metrics.go b/internal/aggregate/metrics.go index cfe2cdb..4472a40 100644 --- a/internal/aggregate/metrics.go +++ b/internal/aggregate/metrics.go @@ -223,7 +223,7 @@ func (m *MetricsAggregator) Shutdown(ctx context.Context) error { m.Stop() // Persist HLL state if configured if m.cfg.Site.SaltRotation != "" { - return m.estimator.Save("/tmp/watchdog-hll.state") + return m.estimator.Save(m.cfg.Server.StatePath) } return nil } diff --git a/internal/aggregate/metrics_test.go b/internal/aggregate/metrics_test.go index 485140c..f04d725 100644 --- a/internal/aggregate/metrics_test.go +++ b/internal/aggregate/metrics_test.go @@ -1,6 +1,8 @@ package aggregate import ( + "context" + "os" "strings" "testing" @@ -190,3 +192,89 @@ func TestMetricsAggregator_MustRegister(t *testing.T) { t.Errorf("expected at least 2 metric families, got %d", len(metrics)) } } + +func TestMetricsAggregator_Shutdown_ConfigurableStatePath(t *testing.T) { + registry := NewPathRegistry(100) + + // Create temp directory for test + tmpDir := t.TempDir() + statePath := tmpDir + "/custom-hll.state" + + cfg := config.Config{ + Site: config.SiteConfig{ + SaltRotation: "daily", + Collect: config.CollectConfig{ + Pageviews: true, + }, + }, + Server: config.ServerConfig{ + StatePath: statePath, + }, + } + + agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) + + // Add some unique visitors so there's state to save + agg.AddUnique("192.168.1.1", "Mozilla/5.0") + + // Shutdown should save to configured path + ctx := context.Background() + if err := agg.Shutdown(ctx); err != nil { + t.Fatalf("Shutdown failed: %v", err) + } + + // Verify file was created at configured path + if _, err := os.Stat(statePath); os.IsNotExist(err) { + t.Errorf("state file was not created at configured path: %s", statePath) + } +} + +func TestMetricsAggregator_Shutdown_DefaultStatePath(t *testing.T) { + registry := NewPathRegistry(100) + + cfg := config.Config{ + Site: config.SiteConfig{ + Domains: []string{"example.com"}, // Required for validation + SaltRotation: "daily", + Collect: config.CollectConfig{ + Pageviews: true, + }, + }, + Limits: config.LimitsConfig{ + MaxPaths: 1000, + MaxSources: 500, + }, + Server: config.ServerConfig{ + // StatePath not set - validation should set default + StatePath: "", + }, + } + + // Validate to apply defaults + if err := cfg.Validate(); err != nil { + t.Fatalf("config validation failed: %v", err) + } + + // Verify default was applied + expectedDefault := "/var/lib/watchdog/hll.state" + if cfg.Server.StatePath != expectedDefault { + t.Errorf( + "expected default StatePath %q, got %q", + expectedDefault, + cfg.Server.StatePath, + ) + } + + agg := NewMetricsAggregator(registry, NewCustomEventRegistry(100), &cfg) + + // Add some unique visitors + agg.AddUnique("192.168.1.1", "Mozilla/5.0") + + // Shutdown should save to default path + ctx := context.Background() + if err := agg.Shutdown(ctx); err != nil { + // Might fail due to permissions on /var/lib, which is OK for this test + // We're just verifying the code path works + t.Logf("Shutdown returned error (might be expected): %v", err) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 1714196..595306e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -83,6 +83,7 @@ type ServerConfig struct { ListenAddr string `yaml:"listen_addr"` MetricsPath string `yaml:"metrics_path"` IngestionPath string `yaml:"ingestion_path"` + StatePath string `yaml:"state_path"` // path to persist HLL state } // YAML configuration file @@ -170,6 +171,9 @@ func (c *Config) Validate() error { if c.Server.IngestionPath == "" { c.Server.IngestionPath = "/api/event" } + if c.Server.StatePath == "" { + c.Server.StatePath = "/var/lib/watchdog/hll.state" + } if c.Security.MetricsAuth.Enabled { if c.Security.MetricsAuth.Username == "" || c.Security.MetricsAuth.Password == "" { diff --git a/web/beacon.test.html b/web/beacon.test.html index ffda638..c0e82b1 100644 --- a/web/beacon.test.html +++ b/web/beacon.test.html @@ -1,69 +1,78 @@ - + - - - - Watchdog Beacon Test - - - -

Watchdog Analytics Beacon Test

+ + + + Watchdog Beacon Test + + + +

Watchdog Analytics Beacon Test

-
-

Instructions: - This page automatically tracks a pageview on load. - Click the buttons below to test custom event tracking.

-
+
+

+ Instructions: This page automatically tracks a pageview + on load. Click the buttons below to test custom event tracking. +

+
-

Custom Events

- - - - +

Custom Events

+ + + + -
-

API Usage:

-

Track custom events: watchdog.track('event_name')

-

Manual pageview: watchdog.trackPageview()

-
+
+

API Usage:

+

Track custom events: watchdog.track('event_name')

+

Manual pageview: watchdog.trackPageview()

+
-

Console

-

Open your browser's developer console (F12) and network tab to see beacon requests.

+

Console

+

+ Open your browser's developer console (F12) and network tab to see beacon + requests. +

- - - + + +