internal/aggergate: make HLL state path configurable

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I8ff8ef25ad945aae918bea97ee39d7ea6a6a6964
This commit is contained in:
raf 2026-03-01 19:23:19 +03:00
commit 2ef2dabf93
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
5 changed files with 167 additions and 63 deletions

View file

@ -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"

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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 == "" {

View file

@ -1,69 +1,78 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Watchdog Beacon Test</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
line-height: 1.6;
}
button {
background: #4CAF50;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 8px 4px;
}
button:hover {
background: #45a049;
}
.info {
background: #f0f0f0;
padding: 12px;
border-radius: 4px;
margin: 16px 0;
}
code {
background: #e8e8e8;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', monospace;
}
</style>
</head>
<body>
<h1>Watchdog Analytics Beacon Test</h1>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Watchdog Beacon Test</title>
<style>
body {
font-family:
system-ui,
-apple-system,
sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
line-height: 1.6;
}
button {
background: #4caf50;
color: white;
padding: 12px 24px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 8px 4px;
}
button:hover {
background: #45a049;
}
.info {
background: #f0f0f0;
padding: 12px;
border-radius: 4px;
margin: 16px 0;
}
code {
background: #e8e8e8;
padding: 2px 6px;
border-radius: 3px;
font-family: "Courier New", monospace;
}
</style>
</head>
<body>
<h1>Watchdog Analytics Beacon Test</h1>
<div class="info">
<p><strong>Instructions:</strong>
This page automatically tracks a pageview on load.
Click the buttons below to test custom event tracking.</p>
</div>
<div class="info">
<p>
<strong>Instructions:</strong> This page automatically tracks a pageview
on load. Click the buttons below to test custom event tracking.
</p>
</div>
<h2>Custom Events</h2>
<button onclick="watchdog.track('signup')">Track Signup</button>
<button onclick="watchdog.track('purchase')">Track Purchase</button>
<button onclick="watchdog.track('download')">Track Download</button>
<button onclick="watchdog.track('newsletter_subscribe')">Track Newsletter</button>
<h2>Custom Events</h2>
<button onclick="watchdog.track('signup')">Track Signup</button>
<button onclick="watchdog.track('purchase')">Track Purchase</button>
<button onclick="watchdog.track('download')">Track Download</button>
<button onclick="watchdog.track('newsletter_subscribe')">
Track Newsletter
</button>
<div class="info">
<p><strong>API Usage:</strong></p>
<p>Track custom events: <code>watchdog.track('event_name')</code></p>
<p>Manual pageview: <code>watchdog.trackPageview()</code></p>
</div>
<div class="info">
<p><strong>API Usage:</strong></p>
<p>Track custom events: <code>watchdog.track('event_name')</code></p>
<p>Manual pageview: <code>watchdog.trackPageview()</code></p>
</div>
<h2>Console</h2>
<p>Open your browser's developer console (F12) and network tab to see beacon requests.</p>
<h2>Console</h2>
<p>
Open your browser's developer console (F12) and network tab to see beacon
requests.
</p>
<!-- Load the beacon script -->
<script src="/web/beacon.js"></script>
</body>
<!-- Load the beacon script -->
<script src="/web/beacon.js"></script>
</body>
</html>