watchdog: migrate to Cobra and Viper for config management; search /etc for configs

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I65dbf466cb030dccc7025585d6282bd26a6a6964
This commit is contained in:
raf 2026-03-01 18:09:34 +03:00
commit f988174145
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
9 changed files with 187 additions and 61 deletions

View file

@ -1,15 +1,92 @@
package watchdog
import (
"flag"
"log"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"notashelf.dev/watchdog/internal/config"
)
func Main() {
configPath := flag.String("config", "config.yaml", "path to config file")
flag.Parse()
var (
cfgFile string
cfg *config.Config
)
if err := Run(*configPath); err != nil {
log.Fatalf("Error: %v", err)
var rootCmd = &cobra.Command{
Use: "watchdog",
Short: "Privacy-first web analytics with Prometheus metrics",
Long: `Watchdog is a lightweight, privacy-first analytics system that aggregates web traffic data.`,
RunE: func(cmd *cobra.Command, args []string) error {
return Run(cfg)
},
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().
StringVar(&cfgFile, "config", "", "config file (default: config.yaml)")
rootCmd.PersistentFlags().String("listen-addr", "", "server listen address (overrides config)")
rootCmd.PersistentFlags().String("metrics-path", "", "metrics endpoint path (overrides config)")
rootCmd.PersistentFlags().
String("ingestion-path", "", "ingestion endpoint path (overrides config)")
// Bind flags to viper
viper.BindPFlag("server.listen_addr", rootCmd.PersistentFlags().Lookup("listen-addr"))
viper.BindPFlag("server.metrics_path", rootCmd.PersistentFlags().Lookup("metrics-path"))
viper.BindPFlag("server.ingestion_path", rootCmd.PersistentFlags().Lookup("ingestion-path"))
}
func initConfig() {
if cfgFile != "" {
// Use config file from the flag
viper.SetConfigFile(cfgFile)
} else {
// Search for config in current directory
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/watchdog")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
}
// Environment variables
viper.SetEnvPrefix("WATCHDOG")
viper.AutomaticEnv()
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Read config file
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
fmt.Fprintf(
os.Stderr,
"Config file not found, using defaults and environment variables\n",
)
} else {
fmt.Fprintf(os.Stderr, "Error reading config file: %v\n", err)
os.Exit(1)
}
}
// Unmarshal into config struct
cfg = &config.Config{}
if err := viper.Unmarshal(cfg); err != nil {
fmt.Fprintf(os.Stderr, "Failed to parse config: %v\n", err)
os.Exit(1)
}
// Validate config
if err := cfg.Validate(); err != nil {
fmt.Fprintf(os.Stderr, "Config validation failed: %v\n", err)
os.Exit(1)
}
}
func Main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View file

@ -21,13 +21,7 @@ import (
"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)
}
func Run(cfg *config.Config) error {
log.Printf("Loaded config for domains: %v", cfg.Site.Domains)
// Initialize components
@ -35,7 +29,7 @@ func Run(configPath string) error {
pathRegistry := aggregate.NewPathRegistry(cfg.Limits.MaxPaths)
refRegistry := normalize.NewReferrerRegistry(cfg.Limits.MaxSources)
eventRegistry := aggregate.NewCustomEventRegistry(cfg.Limits.MaxCustomEvents)
metricsAgg := aggregate.NewMetricsAggregator(pathRegistry, eventRegistry, *cfg)
metricsAgg := aggregate.NewMetricsAggregator(pathRegistry, eventRegistry, cfg)
// HLL state persistence is handled automatically if salt_rotation is configured
if cfg.Site.SaltRotation != "" {
@ -48,7 +42,7 @@ func Run(configPath string) error {
// Create HTTP handlers
ingestionHandler := api.NewIngestionHandler(
*cfg,
cfg,
pathNormalizer,
pathRegistry,
refRegistry,