diff --git a/config.go b/config.go index 58eae756fb38b887f65adaa4fd2e0d1f8d240c1c..f9b667851f6fdfc8eef19875a6c38e38558ccb43 100644 --- a/config.go +++ b/config.go @@ -11,18 +11,34 @@ type Config struct { LibvirtURI string ListenAddr string MetricsPath string + ConfigFile string + FileConfig *FileConfig } // ParseConfig parses command line flags and returns the configuration func ParseConfig() (*Config, error) { var config Config - flag.StringVar(&config.LibvirtURI, "libvirt.uri", "qemu:///system", "Libvirt connection URI") - flag.StringVar(&config.ListenAddr, "web.listen-address", ":9177", "Address to listen on for web interface and telemetry") - flag.StringVar(&config.MetricsPath, "web.telemetry-path", "/metrics", "Path under which to expose metrics") + // String parameters + flag.StringVar(&config.LibvirtURI, "libvirt.uri", "", "Libvirt connection URI") + flag.StringVar(&config.ListenAddr, "web.listen-address", "", "Address to listen on for web interface and telemetry") + flag.StringVar(&config.MetricsPath, "web.telemetry-path", "", "Path under which to expose metrics") + flag.StringVar(&config.ConfigFile, "config.file", "", "Path to configuration file") flag.Parse() + // Load configuration from file if specified + if config.ConfigFile != "" { + fileConfig, err := LoadConfigFromFile(config.ConfigFile) + if err != nil { + return nil, fmt.Errorf("failed to load config file: %w", err) + } + config.FileConfig = fileConfig + } + + // Merge configuration (command line args take precedence over file config) + config.mergeConfig() + // Validate configuration if err := config.Validate(); err != nil { return nil, fmt.Errorf("invalid configuration: %w", err) @@ -31,6 +47,35 @@ func ParseConfig() (*Config, error) { return &config, nil } +// mergeConfig merges configuration from file and command line arguments +// Command line arguments take precedence over file configuration +func (c *Config) mergeConfig() { + // If no file config, use defaults for empty command line values + if c.FileConfig == nil { + if c.LibvirtURI == "" { + c.LibvirtURI = "qemu:///system" + } + if c.ListenAddr == "" { + c.ListenAddr = ":9177" + } + if c.MetricsPath == "" { + c.MetricsPath = "/metrics" + } + return + } + + // Use file config as base, override with command line args if provided + if c.LibvirtURI == "" { + c.LibvirtURI = c.FileConfig.Libvirt.URI + } + if c.ListenAddr == "" { + c.ListenAddr = c.FileConfig.Web.ListenAddress + } + if c.MetricsPath == "" { + c.MetricsPath = c.FileConfig.Web.TelemetryPath + } +} + // Validate validates the configuration func (c *Config) Validate() error { if c.LibvirtURI == "" { @@ -47,13 +92,19 @@ func (c *Config) Validate() error { // Log logs the configuration values func (c *Config) Log() { - log.Printf("Libvirt URI: %s", c.LibvirtURI) - log.Printf("Listening on: %s", c.ListenAddr) - log.Printf("Metrics path: %s", c.MetricsPath) log.Println("--------------------------------------------------------------------") log.Println("UOS Libvirt Exporter Configuration:") - log.Printf(" Libvirt URI : %s", c.LibvirtURI) - log.Printf(" Listen Address : %s", c.ListenAddr) - log.Printf(" Metrics Path : %s", c.MetricsPath) + + if c.ConfigFile != "" { + log.Printf(" Config File : %s", c.ConfigFile) + if c.FileConfig != nil { + c.FileConfig.Log() + } + } else { + log.Printf(" Libvirt URI : %s", c.LibvirtURI) + log.Printf(" Listen Address : %s", c.ListenAddr) + log.Printf(" Metrics Path : %s", c.MetricsPath) + } + log.Println("--------------------------------------------------------------------") } diff --git a/config_file.go b/config_file.go new file mode 100644 index 0000000000000000000000000000000000000000..51e62654dcc6a0ba9a613e8eed4adb9cb5394560 --- /dev/null +++ b/config_file.go @@ -0,0 +1,188 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + + "go.yaml.in/yaml/v2" +) + +// FileConfig represents the configuration structure from YAML file +type FileConfig struct { + Libvirt LibvirtConfig `yaml:"libvirt"` + Web WebConfig `yaml:"web"` + Logging LoggingConfig `yaml:"logging"` + Collection CollectionConfig `yaml:"collection"` + Metrics MetricsConfig `yaml:"metrics"` +} + +// LibvirtConfig holds libvirt connection settings +type LibvirtConfig struct { + URI string `yaml:"uri"` + Timeout int `yaml:"timeout"` + ReconnectInterval int `yaml:"reconnect_interval"` +} + +// WebConfig holds HTTP server settings +type WebConfig struct { + ListenAddress string `yaml:"listen_address"` + TelemetryPath string `yaml:"telemetry_path"` + EnablePprof bool `yaml:"enable_pprof"` + PprofAddress string `yaml:"pprof_address"` +} + +// LoggingConfig holds logging settings +type LoggingConfig struct { + Level string `yaml:"level"` + Format string `yaml:"format"` +} + +// CollectionConfig holds metrics collection settings +type CollectionConfig struct { + Interval int `yaml:"interval"` + Timeout int `yaml:"timeout"` + MaxConcurrent int `yaml:"max_concurrent"` +} + +// MetricsConfig holds metric filtering settings +type MetricsConfig struct { + Enabled []string `yaml:"enabled"` + ExtraLabels map[string]string `yaml:"extra_labels"` +} + +// LoadConfigFromFile loads configuration from YAML file +func LoadConfigFromFile(configFile string) (*FileConfig, error) { + if configFile == "" { + return nil, fmt.Errorf("config file path cannot be empty") + } + + // Read config file + data, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("failed to read config file %s: %w", configFile, err) + } + + // Parse YAML + var config FileConfig + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to parse YAML config: %w", err) + } + + // Apply defaults if not specified + config.applyDefaults() + + // Validate configuration + if err := config.Validate(); err != nil { + return nil, fmt.Errorf("invalid configuration: %w", err) + } + + log.Printf("Configuration loaded from file: %s", configFile) + return &config, nil +} + +// applyDefaults sets default values for missing configuration +func (c *FileConfig) applyDefaults() { + // Libvirt defaults + if c.Libvirt.URI == "" { + c.Libvirt.URI = "qemu:///system" + } + if c.Libvirt.Timeout == 0 { + c.Libvirt.Timeout = 30 + } + if c.Libvirt.ReconnectInterval == 0 { + c.Libvirt.ReconnectInterval = 10 + } + + // Web defaults + if c.Web.ListenAddress == "" { + c.Web.ListenAddress = ":9177" + } + if c.Web.TelemetryPath == "" { + c.Web.TelemetryPath = "/metrics" + } + if c.Web.PprofAddress == "" { + c.Web.PprofAddress = ":6060" + } + + // Logging defaults + if c.Logging.Level == "" { + c.Logging.Level = "info" + } + if c.Logging.Format == "" { + c.Logging.Format = "text" + } + + // Collection defaults + if c.Collection.Interval == 0 { + c.Collection.Interval = 15 + } + if c.Collection.Timeout == 0 { + c.Collection.Timeout = 10 + } + if c.Collection.MaxConcurrent == 0 { + c.Collection.MaxConcurrent = 10 + } + + // Metrics defaults + if len(c.Metrics.Enabled) == 0 { + c.Metrics.Enabled = []string{ + "vm_status", + "vm_cpu", + "vm_memory", + "vm_disk", + "vm_network", + "vm_uptime", + } + } + if c.Metrics.ExtraLabels == nil { + c.Metrics.ExtraLabels = make(map[string]string) + } +} + +// Validate validates the file configuration +func (c *FileConfig) Validate() error { + if c.Libvirt.URI == "" { + return fmt.Errorf("libvirt URI cannot be empty") + } + if c.Web.ListenAddress == "" { + return fmt.Errorf("web listen address cannot be empty") + } + if c.Web.TelemetryPath == "" { + return fmt.Errorf("web telemetry path cannot be empty") + } + if c.Collection.Interval <= 0 { + return fmt.Errorf("collection interval must be positive") + } + if c.Collection.Timeout <= 0 { + return fmt.Errorf("collection timeout must be positive") + } + if c.Collection.MaxConcurrent <= 0 { + return fmt.Errorf("max concurrent must be positive") + } + return nil +} + +// Log logs the file configuration +func (c *FileConfig) Log() { + log.Println("Configuration from file:") + log.Printf(" Libvirt:") + log.Printf(" URI: %s", c.Libvirt.URI) + log.Printf(" Timeout: %d", c.Libvirt.Timeout) + log.Printf(" Reconnect Interval: %d", c.Libvirt.ReconnectInterval) + log.Printf(" Web:") + log.Printf(" Listen Address: %s", c.Web.ListenAddress) + log.Printf(" Telemetry Path: %s", c.Web.TelemetryPath) + log.Printf(" Enable Pprof: %t", c.Web.EnablePprof) + log.Printf(" Pprof Address: %s", c.Web.PprofAddress) + log.Printf(" Logging:") + log.Printf(" Level: %s", c.Logging.Level) + log.Printf(" Format: %s", c.Logging.Format) + log.Printf(" Collection:") + log.Printf(" Interval: %d", c.Collection.Interval) + log.Printf(" Timeout: %d", c.Collection.Timeout) + log.Printf(" Max Concurrent: %d", c.Collection.MaxConcurrent) + log.Printf(" Metrics:") + log.Printf(" Enabled: %v", c.Metrics.Enabled) + log.Printf(" Extra Labels: %v", c.Metrics.ExtraLabels) +} \ No newline at end of file