From b894833ac71de3294489137739b8784c37f2f6c1 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 1 Mar 2026 05:09:01 +0300 Subject: [PATCH] various: HTTP server; migrate to cobra pattern for repository Signed-off-by: NotAShelf Change-Id: Ifac6e992b77dfaf92e3059944aa871f16a6a6964 --- cmd/watchdog/main.go | 15 ++++++++++ cmd/watchdog/root.go | 68 ++++++++++++++++++++++++++++++++++++++++++++ config.example.yaml | 56 ++++++++++++++++++++++++++++++++++++ go.mod | 14 +++++++++ go.sum | 22 ++++++++++++++ main.go | 7 +++++ 6 files changed, 182 insertions(+) create mode 100644 cmd/watchdog/main.go create mode 100644 cmd/watchdog/root.go create mode 100644 config.example.yaml create mode 100644 main.go diff --git a/cmd/watchdog/main.go b/cmd/watchdog/main.go new file mode 100644 index 0000000..1d132a3 --- /dev/null +++ b/cmd/watchdog/main.go @@ -0,0 +1,15 @@ +package watchdog + +import ( + "flag" + "log" +) + +func Main() { + configPath := flag.String("config", "config.yaml", "path to config file") + flag.Parse() + + if err := Run(*configPath); err != nil { + log.Fatalf("Error: %v", err) + } +} diff --git a/cmd/watchdog/root.go b/cmd/watchdog/root.go new file mode 100644 index 0000000..6b50f40 --- /dev/null +++ b/cmd/watchdog/root.go @@ -0,0 +1,68 @@ +package watchdog + +import ( + "fmt" + "log" + "net/http" + "os" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "notashelf.dev/watchdog/internal/aggregate" + "notashelf.dev/watchdog/internal/api" + "notashelf.dev/watchdog/internal/config" + "notashelf.dev/watchdog/internal/normalize" +) + +func Run(configPath string) error { + // Load configuration + cfg, err := config.Load(configPath) + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + log.Printf("Loaded config for domain: %s", cfg.Site.Domain) + + // Initialize components + pathNormalizer := normalize.NewPathNormalizer(cfg.Site.Path) + pathRegistry := aggregate.NewPathRegistry(cfg.Limits.MaxPaths) + refRegistry := normalize.NewReferrerRegistry(cfg.Limits.MaxSources) + metricsAgg := aggregate.NewMetricsAggregator(pathRegistry, *cfg) + + // Register Prometheus metrics + promRegistry := prometheus.NewRegistry() + metricsAgg.MustRegister(promRegistry) + + // Create HTTP handlers + ingestionHandler := api.NewIngestionHandler(*cfg, pathNormalizer, pathRegistry, refRegistry, metricsAgg) + + // Setup routes + mux := http.NewServeMux() + + // Metrics endpoint + mux.Handle(cfg.Server.MetricsPath, promhttp.HandlerFor(promRegistry, promhttp.HandlerOpts{ + EnableOpenMetrics: true, + })) + + // Ingestion endpoint + mux.Handle(cfg.Server.IngestionPath, ingestionHandler) + + // Health check endpoint + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + // Serve static files from /web/ if the directory exists + if info, err := os.Stat("web"); err == nil && info.IsDir() { + log.Println("Serving static files from /web/") + mux.Handle("/web/", http.StripPrefix("/web/", http.FileServer(http.Dir("web")))) + } + + // Start server + log.Printf("Starting server on %s", cfg.Server.ListenAddr) + log.Printf("Metrics endpoint: %s", cfg.Server.MetricsPath) + log.Printf("Ingestion endpoint: %s", cfg.Server.IngestionPath) + + return http.ListenAndServe(cfg.Server.ListenAddr, mux) +} diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..f3a63a4 --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,56 @@ +# Watchdog Analytics Configuration Example + +site: + # Your site's primary domain (required) + domain: "example.com" + + # Salt rotation for IP hashing (optional, default: daily) + # Options: "daily", "hourly" + salt_rotation: "daily" + + # Which dimensions to collect + collect: + pageviews: true + country: true + device: true + # Referrer collection mode: "off", "domain" + # "domain" - collect only the domain part (e.g., "google.com") + # "off" - don't collect referrer data + referrer: "domain" + + # Custom events to track (optional) + custom_events: + - "signup" + - "purchase" + - "download" + + # Path normalization options + path: + # Remove query strings from paths (e.g., /page?id=1 -> /page) + strip_query: true + # Remove fragments from paths (e.g., /page#section -> /page) + strip_fragment: true + # Collapse numeric segments to :id (e.g., /user/123 -> /user/:id) + collapse_numeric_segments: true + # Maximum number of path segments to keep + max_segments: 5 + # Normalize trailing slashes (e.g., /page/ -> /page) + normalize_trailing_slash: true + +# Cardinality limits to prevent metric explosion +limits: + # Maximum number of unique paths to track + max_paths: 10000 + # Maximum number of unique referrer sources to track + max_sources: 500 + # Maximum events per minute (for rate limiting, not yet implemented) + max_events_per_minute: 1000 + +# Server configuration +server: + # Address to listen on + listen_addr: ":8080" + # Prometheus metrics endpoint + metrics_path: "/metrics" + # Event ingestion endpoint + ingestion_path: "/api/event" diff --git a/go.mod b/go.mod index ff5eb22..0dbe9d8 100644 --- a/go.mod +++ b/go.mod @@ -5,3 +5,17 @@ go 1.25.5 require gopkg.in/yaml.v3 v3.0.1 require golang.org/x/net v0.51.0 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/sys v0.41.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect +) diff --git a/go.sum b/go.sum index 00815c1..b1985a5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,27 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..ce24c8f --- /dev/null +++ b/main.go @@ -0,0 +1,7 @@ +package main + +import "notashelf.dev/watchdog/cmd/watchdog" + +func main() { + watchdog.Main() +}