various: HTTP server; migrate to cobra pattern for repository

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ifac6e992b77dfaf92e3059944aa871f16a6a6964
This commit is contained in:
raf 2026-03-01 05:09:01 +03:00
commit b894833ac7
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
6 changed files with 182 additions and 0 deletions

15
cmd/watchdog/main.go Normal file
View file

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

68
cmd/watchdog/root.go Normal file
View file

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

56
config.example.yaml Normal file
View file

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

14
go.mod
View file

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

22
go.sum
View file

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

7
main.go Normal file
View file

@ -0,0 +1,7 @@
package main
import "notashelf.dev/watchdog/cmd/watchdog"
func main() {
watchdog.Main()
}