package main import ( "fmt" "log" "log/slog" "time" "github.com/spf13/viper" ) type ExportConfig struct { Enabled bool `mapstructure:"enabled"` BatchSize int `mapstructure:"batch_size"` ExportInterval time.Duration `mapstructure:"export_interval"` RetryAttempts int `mapstructure:"retry_attempts"` RetryBackoff time.Duration `mapstructure:"retry_backoff"` HealthCheckInterval time.Duration `mapstructure:"health_check_interval"` } type WebConfig struct { Enabled bool `mapstructure:"enabled"` Port int `mapstructure:"port"` Host string `mapstructure:"host"` } type LogFormat struct { Name string `mapstructure:"name"` Pattern string `mapstructure:"pattern"` Fields map[string]string `mapstructure:"fields"` } type ToolConfig struct { Name string `mapstructure:"name"` LogFile string `mapstructure:"log_file"` Format LogFormat `mapstructure:"format"` Enabled bool `mapstructure:"enabled"` BufferSize int `mapstructure:"buffer_size"` } type ServiceConfig struct { Name string `mapstructure:"name"` Service string `mapstructure:"service"` Enabled bool `mapstructure:"enabled"` SinceTime string `mapstructure:"since_time"` Priority string `mapstructure:"priority"` } type ElasticsearchConfig struct { Enabled bool `mapstructure:"enabled"` URL string `mapstructure:"url"` Index string `mapstructure:"index"` Username string `mapstructure:"username"` Password string `mapstructure:"password"` APIKey string `mapstructure:"api_key"` Timeout int `mapstructure:"timeout"` } type LocalStorage struct { Enable bool `mapstructure:"enabled"` DBPath string `mapstructure:"db_path"` RotationConfig StorageRotationConfig `mapstructure:"rotation"` } type SystemMetrics struct { Enabled bool `mapstructure:"enabled"` CollectCPU bool `mapstructure:"collect_cpu"` CollectMemory bool `mapstructure:"collect_memory"` CollectDisk bool `mapstructure:"collect_disk"` CollectNetwork bool `mapstructure:"collect_network"` CollectProcesses bool `mapstructure:"collect_processes"` DiskPaths []string `mapstructure:"disk_paths"` NetworkInterfaces []string `mapstructure:"network_interfaces"` TopProcessesLimit int `mapstructure:"top_processes_limit"` CollectNetworkConnections bool `mapstructure:"collect_network_connections"` CollectLoadAverage bool `mapstructure:"collect_load_average"` CollectTCPStats bool `mapstructure:"collect_tcp_stats"` CollectFileHandles bool `mapstructure:"collect_filehandles"` CollectDiskIO bool `mapstructure:"collect_disk_io"` CollectNetworkLatency bool `mapstructure:"collect_network_latency"` CollectBandwidthUsage bool `mapstructure:"collect_bandwidth_usage"` TransferPorts []int `mapstructure:"transfer_ports"` LatencyTestHosts []string `mapstructure:"latency_test_hosts"` } type Config struct { Elasticsearch ElasticsearchConfig `mapstructure:"elasticsearch"` LocalStorage LocalStorage `mapstrucutre:"localstorage"` Export ExportConfig `mapstructure:"export"` Tools []ToolConfig `mapstructure:"tools"` Services []ServiceConfig `mapstructure:"services"` PollIntervalSeconds int `mapstructure:"poll_interval_seconds"` SystemMetrics SystemMetrics `mapstructure:"system_metrics"` WebService WebConfig `mapstructure:"web_service"` Logging struct { Level string `mapstructure:"level"` FilePath string `mapstructure:"file_path"` } `mapstructure:"logging"` } type StorageRotationConfig struct { // MaxSizeBytes is the maximum size of the database in bytes (0 = deactivated) MaxSizeBytes int64 `mapstructure:"max_size_bytes"` // MaxAgeHours is the maximum age of the database (0 = deaactivated) MaxAgeHours time.Duration `mapstructure:"max_age_hours"` // MaxFiles is the maximum count of old files, to keep MaxFiles int `mapstructure:"max_files"` // CheckIntervalMinutes is the intervall for checking rotation conditions CheckIntervalMinutes time.Duration `mapstructure:"check_interval_minutes"` // ArchiveDir is the dir to store archived files (empty = same dir as db) ArchiveDir string `mapstructure:"archive_dir"` } func (src StorageRotationConfig) GetMaxAge() time.Duration { if src.MaxAgeHours <= 0 { return 0 } return time.Duration(src.MaxAgeHours) * time.Hour } func (src StorageRotationConfig) GetCheckInterval() time.Duration { if src.CheckIntervalMinutes <= 0 { return 5 * time.Minute } return time.Duration(src.CheckIntervalMinutes) * time.Minute } func LoadConfig() (*Config, error) { viper.SetConfigName("config") viper.AddConfigPath(".") viper.AddConfigPath("/opt/tixel/tixel-watch/") viper.SetConfigType("yaml") setConfigDefaults() if err := viper.ReadInConfig(); err != nil { return nil, fmt.Errorf("error reading config: %w", err) } var cfg Config if err := viper.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("error parsing config: %w", err) } if err := validateConfig(&cfg); err != nil { return nil, fmt.Errorf("config validation failed: %w", err) } return &cfg, nil } func setConfigDefaults() { viper.SetDefault("poll_interval_seconds", 30) viper.SetDefault("elasticsearch.timeout", 30) viper.SetDefault("system_metrics.enabled", true) viper.SetDefault("system_metrics.collect_cpu", true) viper.SetDefault("system_metrics.collect_memory", true) viper.SetDefault("system_metrics.collect_disk", true) viper.SetDefault("system_metrics.collect_network", false) viper.SetDefault("system_metrics.disk_paths", []string{"/"}) viper.SetDefault("web_service.enabled", false) viper.SetDefault("web_service.port", 8080) viper.SetDefault("web_service.host", "localhost") viper.SetDefault("logging.level", "info") } func setConfigDefaultsV2() { viper.SetDefault("poll_interval_seconds", 30) viper.SetDefault("elasticsearch.timeout", 30) viper.SetDefault("system_metrics.enabled", true) viper.SetDefault("system_metrics.collect_cpu", true) viper.SetDefault("system_metrics.collect_memory", true) viper.SetDefault("system_metrics.collect_disk", true) viper.SetDefault("system_metrics.collect_network", false) viper.SetDefault("system_metrics.disk_paths", []string{"/"}) viper.SetDefault("web_service.enabled", false) viper.SetDefault("web_service.port", 8080) viper.SetDefault("web_service.host", "localhost") viper.SetDefault("logging.level", "info") viper.SetDefault("export.enabled", true) viper.SetDefault("export.batch_size", 100) viper.SetDefault("export.export_interval", "30s") viper.SetDefault("export.retry_attempts", 3) viper.SetDefault("export.retry_backoff", "5s") viper.SetDefault("export.health_check_interval", "60s") viper.SetDefault("localstorage.enabled", true) viper.SetDefault("localstorage.db_path", "./tixel_watch.db") viper.SetDefault("localstorage.rotation.max_size_bytes", int64(100*1024*1024)) viper.SetDefault("localstorage.rotation.max_age_hours", 24) viper.SetDefault("localstorage.rotation.max_files", 7) viper.SetDefault("localstorage.rotation.check_interval_minutes", 5) viper.SetDefault("localstorage.rotation.archive_dir", "") } func validateConfig(cfg *Config) error { if cfg.Elasticsearch.URL == "" { return fmt.Errorf("elasticsearch.url is required") } if cfg.Elasticsearch.Index == "" { return fmt.Errorf("elasticsearch.index is required") } if cfg.PollIntervalSeconds <= 0 { log.Printf("Warning: poll_interval_seconds is %d, setting to 30", cfg.PollIntervalSeconds) cfg.PollIntervalSeconds = 30 } for i := range cfg.Tools { if cfg.Tools[i].BufferSize <= 0 { cfg.Tools[i].BufferSize = 100 } } return nil } func LoadConfigV2() (*Config, error) { viper.SetConfigName("config") viper.AddConfigPath(".") viper.AddConfigPath("/opt/tixel/tixel-watch/") viper.SetConfigType("yaml") setConfigDefaultsV2() if err := viper.ReadInConfig(); err != nil { return nil, fmt.Errorf("error reading config: %w", err) } var cfg Config if err := viper.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("error parsing config: %w", err) } if err := validateConfigV2(&cfg); err != nil { return nil, fmt.Errorf("config validation failed: %w", err) } return &cfg, nil } func validateConfigV2(cfg *Config) error { if !cfg.LocalStorage.Enable { return fmt.Errorf("local storage must be enabled in the new architecture") } if cfg.LocalStorage.DBPath == "" { return fmt.Errorf("local storage db_path is required") } if cfg.Export.Enabled && cfg.Elasticsearch.Enabled { if cfg.Elasticsearch.URL == "" { return fmt.Errorf("elasticsearch.url is required when elasticsearch export is enabled") } if cfg.Elasticsearch.Index == "" { return fmt.Errorf("elasticsearch.index is required when elasticsearch export is enabled") } } if cfg.PollIntervalSeconds <= 0 { slog.Warn("poll_interval_seconds is invalid, setting to 30", "value", cfg.PollIntervalSeconds) cfg.PollIntervalSeconds = 30 } if cfg.Export.Enabled { if cfg.Export.BatchSize <= 0 { cfg.Export.BatchSize = 100 } if cfg.Export.ExportInterval <= 0 { cfg.Export.ExportInterval = 30 * time.Second } if cfg.Export.RetryAttempts < 0 { cfg.Export.RetryAttempts = 3 } if cfg.Export.RetryBackoff <= 0 { cfg.Export.RetryBackoff = 5 * time.Second } if cfg.Export.HealthCheckInterval <= 0 { cfg.Export.HealthCheckInterval = 60 * time.Second } } for i := range cfg.Tools { if cfg.Tools[i].BufferSize <= 0 { cfg.Tools[i].BufferSize = 100 } } if cfg.LocalStorage.RotationConfig.MaxSizeBytes < 0 { slog.Warn("Invalid rotation max_size_bytes, setting to 100MB", "value", cfg.LocalStorage.RotationConfig.MaxSizeBytes) cfg.LocalStorage.RotationConfig.MaxSizeBytes = 100 * 1024 * 1024 } if cfg.LocalStorage.RotationConfig.MaxAgeHours < 0 { slog.Warn("Invalid rotation max_age_hours, setting to 24", "value", cfg.LocalStorage.RotationConfig.MaxAgeHours) cfg.LocalStorage.RotationConfig.MaxAgeHours = 24 } if cfg.LocalStorage.RotationConfig.MaxFiles < 0 { slog.Warn("Invalid rotation max_files, setting to 7", "value", cfg.LocalStorage.RotationConfig.MaxFiles) cfg.LocalStorage.RotationConfig.MaxFiles = 7 } if cfg.LocalStorage.RotationConfig.CheckIntervalMinutes < 1 { slog.Warn("Invalid rotation check_interval_minutes, setting to 5", "value", cfg.LocalStorage.RotationConfig.CheckIntervalMinutes) cfg.LocalStorage.RotationConfig.CheckIntervalMinutes = 5 } return nil }