internal/aggergate: make HLL state path configurable
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I8ff8ef25ad945aae918bea97ee39d7ea6a6a6964
This commit is contained in:
parent
f988174145
commit
2ef2dabf93
5 changed files with 167 additions and 63 deletions
|
|
@ -98,3 +98,6 @@ server:
|
||||||
metrics_path: "/metrics"
|
metrics_path: "/metrics"
|
||||||
# Event ingestion endpoint
|
# Event ingestion endpoint
|
||||||
ingestion_path: "/api/event"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,7 @@ func (m *MetricsAggregator) Shutdown(ctx context.Context) error {
|
||||||
m.Stop()
|
m.Stop()
|
||||||
// Persist HLL state if configured
|
// Persist HLL state if configured
|
||||||
if m.cfg.Site.SaltRotation != "" {
|
if m.cfg.Site.SaltRotation != "" {
|
||||||
return m.estimator.Save("/tmp/watchdog-hll.state")
|
return m.estimator.Save(m.cfg.Server.StatePath)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package aggregate
|
package aggregate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -190,3 +192,89 @@ func TestMetricsAggregator_MustRegister(t *testing.T) {
|
||||||
t.Errorf("expected at least 2 metric families, got %d", len(metrics))
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ type ServerConfig struct {
|
||||||
ListenAddr string `yaml:"listen_addr"`
|
ListenAddr string `yaml:"listen_addr"`
|
||||||
MetricsPath string `yaml:"metrics_path"`
|
MetricsPath string `yaml:"metrics_path"`
|
||||||
IngestionPath string `yaml:"ingestion_path"`
|
IngestionPath string `yaml:"ingestion_path"`
|
||||||
|
StatePath string `yaml:"state_path"` // path to persist HLL state
|
||||||
}
|
}
|
||||||
|
|
||||||
// YAML configuration file
|
// YAML configuration file
|
||||||
|
|
@ -170,6 +171,9 @@ func (c *Config) Validate() error {
|
||||||
if c.Server.IngestionPath == "" {
|
if c.Server.IngestionPath == "" {
|
||||||
c.Server.IngestionPath = "/api/event"
|
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.Enabled {
|
||||||
if c.Security.MetricsAuth.Username == "" || c.Security.MetricsAuth.Password == "" {
|
if c.Security.MetricsAuth.Username == "" || c.Security.MetricsAuth.Password == "" {
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,78 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Watchdog Beacon Test</title>
|
<title>Watchdog Beacon Test</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
font-family:
|
||||||
max-width: 600px;
|
system-ui,
|
||||||
margin: 50px auto;
|
-apple-system,
|
||||||
padding: 20px;
|
sans-serif;
|
||||||
line-height: 1.6;
|
max-width: 600px;
|
||||||
}
|
margin: 50px auto;
|
||||||
button {
|
padding: 20px;
|
||||||
background: #4CAF50;
|
line-height: 1.6;
|
||||||
color: white;
|
}
|
||||||
padding: 12px 24px;
|
button {
|
||||||
border: none;
|
background: #4caf50;
|
||||||
border-radius: 4px;
|
color: white;
|
||||||
cursor: pointer;
|
padding: 12px 24px;
|
||||||
font-size: 16px;
|
border: none;
|
||||||
margin: 8px 4px;
|
border-radius: 4px;
|
||||||
}
|
cursor: pointer;
|
||||||
button:hover {
|
font-size: 16px;
|
||||||
background: #45a049;
|
margin: 8px 4px;
|
||||||
}
|
}
|
||||||
.info {
|
button:hover {
|
||||||
background: #f0f0f0;
|
background: #45a049;
|
||||||
padding: 12px;
|
}
|
||||||
border-radius: 4px;
|
.info {
|
||||||
margin: 16px 0;
|
background: #f0f0f0;
|
||||||
}
|
padding: 12px;
|
||||||
code {
|
border-radius: 4px;
|
||||||
background: #e8e8e8;
|
margin: 16px 0;
|
||||||
padding: 2px 6px;
|
}
|
||||||
border-radius: 3px;
|
code {
|
||||||
font-family: 'Courier New', monospace;
|
background: #e8e8e8;
|
||||||
}
|
padding: 2px 6px;
|
||||||
</style>
|
border-radius: 3px;
|
||||||
</head>
|
font-family: "Courier New", monospace;
|
||||||
<body>
|
}
|
||||||
<h1>Watchdog Analytics Beacon Test</h1>
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Watchdog Analytics Beacon Test</h1>
|
||||||
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<p><strong>Instructions:</strong>
|
<p>
|
||||||
This page automatically tracks a pageview on load.
|
<strong>Instructions:</strong> This page automatically tracks a pageview
|
||||||
Click the buttons below to test custom event tracking.</p>
|
on load. Click the buttons below to test custom event tracking.
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2>Custom Events</h2>
|
<h2>Custom Events</h2>
|
||||||
<button onclick="watchdog.track('signup')">Track Signup</button>
|
<button onclick="watchdog.track('signup')">Track Signup</button>
|
||||||
<button onclick="watchdog.track('purchase')">Track Purchase</button>
|
<button onclick="watchdog.track('purchase')">Track Purchase</button>
|
||||||
<button onclick="watchdog.track('download')">Track Download</button>
|
<button onclick="watchdog.track('download')">Track Download</button>
|
||||||
<button onclick="watchdog.track('newsletter_subscribe')">Track Newsletter</button>
|
<button onclick="watchdog.track('newsletter_subscribe')">
|
||||||
|
Track Newsletter
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<p><strong>API Usage:</strong></p>
|
<p><strong>API Usage:</strong></p>
|
||||||
<p>Track custom events: <code>watchdog.track('event_name')</code></p>
|
<p>Track custom events: <code>watchdog.track('event_name')</code></p>
|
||||||
<p>Manual pageview: <code>watchdog.trackPageview()</code></p>
|
<p>Manual pageview: <code>watchdog.trackPageview()</code></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Console</h2>
|
<h2>Console</h2>
|
||||||
<p>Open your browser's developer console (F12) and network tab to see beacon requests.</p>
|
<p>
|
||||||
|
Open your browser's developer console (F12) and network tab to see beacon
|
||||||
|
requests.
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Load the beacon script -->
|
<!-- Load the beacon script -->
|
||||||
<script src="/web/beacon.js"></script>
|
<script src="/web/beacon.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue