package main import ( "log" "os" "os/user" "path" "path/filepath" "time" "github.com/kkyr/fig" ) // Config is the service-mode configuration, loaded from YAML (via fig) and // overridable by flags. It only governs the output exporters; drive discovery // and SMART collection auto-detect their tools and need no configuration. type Config struct { // Hostname is used as the host tag/label on every metric. When empty it is // resolved from the system hostname. Hostname string `fig:"hostname"` // Metric outputs. HTTP HTTPOutputConfig `fig:"http_output"` Influx InfluxOutputConfig `fig:"influx_output"` } // HTTPOutputConfig configures the Prometheus HTTP exporter. type HTTPOutputConfig struct { Enabled bool `fig:"enabled"` BindAddr string `fig:"bind_addr"` Port uint `fig:"port"` MetricsPath string `fig:"metrics_path"` } // InfluxOutputConfig configures the scheduled InfluxDB output. Metrics are // pushed every Frequency to InfluxDB's v2 API and/or to Kafka. A zero Frequency // (or no destination configured) disables the output. type InfluxOutputConfig struct { Frequency time.Duration `fig:"frequency"` KafkaBrokers []string `fig:"kafka_brokers"` KafkaTopic string `fig:"kafka_topic"` KafkaUsername string `fig:"kafka_username"` KafkaPassword string `fig:"kafka_password"` KafkaInsecureSkipVerify bool `fig:"kafka_insecure_skip_verify"` KafkaOutputFormat string `fig:"kafka_output_format"` // lineprotocol (default) or json. InfluxServer string `fig:"influx_server"` Token string `fig:"token"` Org string `fig:"org"` Bucket string `fig:"bucket"` } // defaultConfig returns the configuration with all defaults applied, used as the // base before a file (if any) is loaded over it. func defaultConfig() *Config { return &Config{ HTTP: HTTPOutputConfig{ Enabled: true, Port: 9101, MetricsPath: "/metrics", }, Influx: InfluxOutputConfig{ KafkaOutputFormat: "lineprotocol", }, } } // findConfigFile returns the first configuration file that exists, preferring // the -config flag (configPath), then a local file, the user config dir, and // finally /etc. It returns "" when none is found — configuration is optional. func findConfigFile(configPath string) string { if configPath != "" { if _, err := os.Stat(configPath); err == nil { return configPath } log.Printf("Configured config path %q not found, falling back to defaults", configPath) } candidates := []string{} if local, err := filepath.Abs("./config.yaml"); err == nil { candidates = append(candidates, local) } if usr, err := user.Current(); err == nil { candidates = append(candidates, usr.HomeDir+"/.config/drive-health-metrics/config.yaml") } candidates = append(candidates, "/etc/drive-health-metrics.yaml") for _, c := range candidates { if _, err := os.Stat(c); err == nil { return c } } return "" } // ReadConfig loads the configuration into app.config. It always succeeds with a // usable config: a file is loaded over the defaults when present, flag overrides // are applied, and the host tag is resolved when unset. func (a *App) ReadConfig() { config := defaultConfig() // Load a configuration file over the defaults when one is available. if configFile := findConfigFile(a.flags.ConfigPath); configFile != "" { dir, name := path.Split(configFile) if dir == "" { dir = "." } if err := fig.Load(config, fig.File(name), fig.Dirs(dir)); err != nil { log.Printf("Error parsing configuration %q: %s", configFile, err) } } // Resolve the host tag from the system when not configured. if config.Hostname == "" { config.Hostname = hostname() } // Flag overrides for the HTTP output. if a.flags.HTTPBind != "" { config.HTTP.BindAddr = a.flags.HTTPBind } if a.flags.HTTPPort != 0 { config.HTTP.Port = a.flags.HTTPPort } if a.flags.HTTPMetricsPath != "" { config.HTTP.MetricsPath = a.flags.HTTPMetricsPath } a.config = config }