config: data structures; basic tests
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ia7d6f19a46ec8a4987ea429ec6502f676a6a6964
This commit is contained in:
parent
3fca34dd6f
commit
4c84393286
6 changed files with 185 additions and 0 deletions
2
go.mod
2
go.mod
|
|
@ -1,3 +1,5 @@
|
||||||
module notashelf.dev/watchdog
|
module notashelf.dev/watchdog
|
||||||
|
|
||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
|
||||||
4
go.sum
Normal file
4
go.sum
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
104
internal/config/config.go
Normal file
104
internal/config/config.go
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration structure
|
||||||
|
type Config struct {
|
||||||
|
Site SiteConfig `yaml:"site"`
|
||||||
|
Limits LimitsConfig `yaml:"limits"`
|
||||||
|
Server ServerConfig `yaml:"server"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Site-specific settings
|
||||||
|
type SiteConfig struct {
|
||||||
|
Domain string `yaml:"domain"`
|
||||||
|
SaltRotation string `yaml:"salt_rotation"`
|
||||||
|
Collect CollectConfig `yaml:"collect"`
|
||||||
|
CustomEvents []string `yaml:"custom_events"`
|
||||||
|
Path PathConfig `yaml:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which dimensions to collect
|
||||||
|
type CollectConfig struct {
|
||||||
|
Pageviews bool `yaml:"pageviews"`
|
||||||
|
Country bool `yaml:"country"`
|
||||||
|
Device bool `yaml:"device"`
|
||||||
|
Referrer string `yaml:"referrer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path normalization options
|
||||||
|
type PathConfig struct {
|
||||||
|
StripQuery bool `yaml:"strip_query"`
|
||||||
|
StripFragment bool `yaml:"strip_fragment"`
|
||||||
|
CollapseNumericSegments bool `yaml:"collapse_numeric_segments"`
|
||||||
|
MaxSegments int `yaml:"max_segments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cardinality limits
|
||||||
|
type LimitsConfig struct {
|
||||||
|
MaxPaths int `yaml:"max_paths"`
|
||||||
|
MaxEventsPerMinute int `yaml:"max_events_per_minute"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server endpoints
|
||||||
|
type ServerConfig struct {
|
||||||
|
ListenAddr string `yaml:"listen_addr"`
|
||||||
|
MetricsPath string `yaml:"metrics_path"`
|
||||||
|
IngestionPath string `yaml:"ingestion_path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// YAML configuration file
|
||||||
|
func Load(path string) (*Config, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("config validation failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required fields and sets defaults
|
||||||
|
// FIXME: in the future we need to validate in the config parser
|
||||||
|
func (c *Config) Validate() error {
|
||||||
|
// Validate site domain is required
|
||||||
|
if c.Site.Domain == "" {
|
||||||
|
return fmt.Errorf("site.domain is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate salt_rotation if provided
|
||||||
|
if c.Site.SaltRotation != "" && c.Site.SaltRotation != "daily" && c.Site.SaltRotation != "hourly" {
|
||||||
|
return fmt.Errorf("site.salt_rotation must be 'daily' or 'hourly'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate max_paths is positive
|
||||||
|
if c.Limits.MaxPaths <= 0 {
|
||||||
|
return fmt.Errorf("limits.max_paths must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set server defaults if not provided
|
||||||
|
if c.Server.ListenAddr == "" {
|
||||||
|
c.Server.ListenAddr = ":8080"
|
||||||
|
}
|
||||||
|
if c.Server.MetricsPath == "" {
|
||||||
|
c.Server.MetricsPath = "/metrics"
|
||||||
|
}
|
||||||
|
if c.Server.IngestionPath == "" {
|
||||||
|
c.Server.IngestionPath = "/api/event"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
44
internal/config/config_test.go
Normal file
44
internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadConfig_ValidFile(t *testing.T) {
|
||||||
|
cfg, err := Load("../../testdata/config.valid.yaml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Site.Domain != "example.com" {
|
||||||
|
t.Errorf("expected domain 'example.com', got '%s'", cfg.Site.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Site.SaltRotation != "daily" {
|
||||||
|
t.Errorf("expected salt_rotation 'daily', got '%s'", cfg.Site.SaltRotation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_MissingFile(t *testing.T) {
|
||||||
|
_, err := Load("nonexistent.yaml")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for missing file, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_MaxPathsRequired(t *testing.T) {
|
||||||
|
cfg := &Config{
|
||||||
|
Site: SiteConfig{
|
||||||
|
Domain: "example.com",
|
||||||
|
SaltRotation: "daily",
|
||||||
|
},
|
||||||
|
Limits: LimitsConfig{
|
||||||
|
MaxPaths: 0, // invalid
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cfg.Validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected validation error for max_paths=0, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
6
testdata/config.invalid.yaml
vendored
Normal file
6
testdata/config.invalid.yaml
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
site:
|
||||||
|
domain: ""
|
||||||
|
salt_rotation: daily
|
||||||
|
|
||||||
|
limits:
|
||||||
|
max_paths: 0
|
||||||
25
testdata/config.valid.yaml
vendored
Normal file
25
testdata/config.valid.yaml
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
site:
|
||||||
|
domain: example.com
|
||||||
|
salt_rotation: daily
|
||||||
|
collect:
|
||||||
|
pageviews: true
|
||||||
|
country: true
|
||||||
|
device: true
|
||||||
|
referrer: classify
|
||||||
|
custom_events:
|
||||||
|
- signup
|
||||||
|
- purchase
|
||||||
|
path:
|
||||||
|
strip_query: true
|
||||||
|
strip_fragment: true
|
||||||
|
collapse_numeric_segments: true
|
||||||
|
max_segments: 5
|
||||||
|
|
||||||
|
limits:
|
||||||
|
max_paths: 1000
|
||||||
|
max_events_per_minute: 10000
|
||||||
|
|
||||||
|
server:
|
||||||
|
listen_addr: :8080
|
||||||
|
metrics_path: /metrics
|
||||||
|
ingestion_path: /api/event
|
||||||
Loading…
Add table
Add a link
Reference in a new issue