package main import ( "flag" "fmt" "log" "os" "reflect" "strconv" "strings" "time" "github.com/hashicorp/logutils" "github.com/prometheus/client_golang/prometheus" ) type config struct { // time configuration TimeFormat string `env:"TIME_FORMAT" default:"2006-01-02 15:04:05"` TimeZoneLocal string `env:"TIME_ZONE" default:"America/Chicago"` TZoneLocal *time.Location `ignored:"true"` TZoneUTC *time.Location `ignored:"true"` // logging LogLevel int `env:"LOG_LEVEL" default:"20"` Log *logutils.LevelFilter `ignored:"true"` // webserver WebSrvPort int `env:"WEBSRV_PORT" default:"8080"` WebSrvIP string `env:"WEBSRV_IP" default:"0.0.0.0"` WebSrvReadTimeout int `env:"WEBSRV_TO_READ" default:"5"` WebSrvWriteTimeout int `env:"WEBSRV_TO_WRITE" default:"2"` WebSrvIdleTimeout int `env:"WEBSRV_TO_READ" default:"2"` // devices Hosts []string `ignored:"true"` // prometheus Prometheus configPrometheus `ignored:"true"` } type hostStruct []string func (i *hostStruct) String() string { return "dunno" } func (i *hostStruct) Set(value string) error { *i = append(*i, value) return nil } type structInfo struct { Name string Alt string Key string Field reflect.Value Tags reflect.StructTag Type reflect.Type DefaultValue interface{} } type configPrometheus struct { CurrentMa *prometheus.GaugeVec VoltageMv *prometheus.GaugeVec PowerMw *prometheus.GaugeVec TotalWh *prometheus.CounterVec } func defaultConfig() *config { return &config{ Log: &logutils.LevelFilter{ Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARNING", "ERROR"}, Writer: os.Stderr, }, Prometheus: configPrometheus{ CurrentMa: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "tplink", Name: "unknown", Help: "unknown", }, []string{"device"}), VoltageMv: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "tplink", Name: "volts", Help: "input voltage", }, []string{"device"}), PowerMw: prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "tplink", Name: "watts", Help: "current wattage", }, []string{"device"}), TotalWh: prometheus.NewCounterVec(prometheus.CounterOpts{ Namespace: "tplink", Name: "watt_hours", Help: "total watt hours", }, []string{"device"}), }, } } func (cfg *config) setLogLevel() { switch { case cfg.LogLevel <= 20: cfg.Log.SetMinLevel(logutils.LogLevel("ERROR")) case cfg.LogLevel > 20 && cfg.LogLevel <= 40: cfg.Log.SetMinLevel(logutils.LogLevel("WARNING")) case cfg.LogLevel > 40 && cfg.LogLevel <= 60: cfg.Log.SetMinLevel(logutils.LogLevel("INFO")) case cfg.LogLevel > 60 && cfg.LogLevel <= 80: cfg.Log.SetMinLevel(logutils.LogLevel("DEBUG")) case cfg.LogLevel > 80: cfg.Log.SetMinLevel(logutils.LogLevel("TRACE")) } log.SetOutput(cfg.Log) } func (cfg *config) printRunningConfig(cfgInfo []structInfo) { log.Printf("[DEBUG] Current Running configuration Values:") for _, info := range cfgInfo { switch info.Type.String() { case "string": p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string) log.Printf("[DEBUG]\t%s\t\t= %s\n", info.Alt, *p) case "bool": p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool) log.Printf("[DEBUG]\t%s\t\t= %s\n", info.Alt, strconv.FormatBool(*p)) case "int": p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int) log.Printf("[DEBUG]\t%s\t\t= %s\n", info.Alt, strconv.FormatInt(int64(*p), 10)) } } } func getStructInfo(spec interface{}) ([]structInfo, error) { s := reflect.ValueOf(spec) if s.Kind() != reflect.Pointer { return []structInfo{}, fmt.Errorf("getstructInfo() was sent a %s instead of a pointer to a struct.\n", s.Kind()) } s = s.Elem() if s.Kind() != reflect.Struct { return []structInfo{}, fmt.Errorf("getstructInfo() was sent a %s instead of a struct.\n", s.Kind()) } typeOfSpec := s.Type() infos := make([]structInfo, 0, s.NumField()) for i := 0; i < s.NumField(); i++ { f := s.Field(i) ftype := typeOfSpec.Field(i) ignored, _ := strconv.ParseBool(ftype.Tag.Get("ignored")) if !f.CanSet() || ignored { continue } for f.Kind() == reflect.Pointer { if f.IsNil() { if f.Type().Elem().Kind() != reflect.Struct { break } f.Set(reflect.New(f.Type().Elem())) } f = f.Elem() } info := structInfo{ Name: ftype.Name, Alt: strings.ToUpper(ftype.Tag.Get("env")), Key: ftype.Name, Field: f, Tags: ftype.Tag, Type: ftype.Type, } if info.Alt != "" { info.Key = info.Alt } info.Key = strings.ToUpper(info.Key) if ftype.Tag.Get("default") != "" { v, err := typeConversion(ftype.Type.String(), ftype.Tag.Get("default")) if err != nil { return []structInfo{}, err } info.DefaultValue = v } infos = append(infos, info) } return infos, nil } func typeConversion(t, v string) (interface{}, error) { switch t { case "string": return v, nil case "int": return strconv.ParseInt(v, 10, 0) case "int8": return strconv.ParseInt(v, 10, 8) case "int16": return strconv.ParseInt(v, 10, 16) case "int32": return strconv.ParseInt(v, 10, 32) case "int64": return strconv.ParseInt(v, 10, 64) case "uint": return strconv.ParseUint(v, 10, 0) case "uint16": return strconv.ParseUint(v, 10, 16) case "uint32": return strconv.ParseUint(v, 10, 32) case "uint64": return strconv.ParseUint(v, 10, 64) case "float32": return strconv.ParseFloat(v, 32) case "float64": return strconv.ParseFloat(v, 64) case "complex64": return strconv.ParseComplex(v, 64) case "complex128": return strconv.ParseComplex(v, 128) case "bool": return strconv.ParseBool(v) } return nil, fmt.Errorf("Unable to identify type.") } // getEnvString returns string from environment variable func getEnvString(env, def string) (val string) { //nolint:deadcode val = os.Getenv(env) if val == "" { return def } return } // getEnvInt returns int from environment variable func getEnvInt(env string, def int) (ret int) { val := os.Getenv(env) if val == "" { return def } ret, err := strconv.Atoi(val) if err != nil { log.Fatalf("[ERROR] Environment variable is not numeric: %v\n", env) } return } // getEnvBool returns boolean from environment variable func getEnvBool(env string, def bool) bool { var ( err error retVal bool val = os.Getenv(env) ) if len(val) == 0 { return def } else { retVal, err = strconv.ParseBool(val) if err != nil { log.Fatalf("[ERROR] Environment variable is not boolean: %v\n", env) } } return retVal } // Init initializes the application configuration by reading default values from the struct's tags // and environment variables. Tags processed by this process are as follows: // `ignored:"true" env:"ENVIRONMENT_VARIABLE" default:"default value"` func initialize() *config { cfg := defaultConfig() var hosts hostStruct cfgInfo, err := getStructInfo(cfg) if err != nil { log.Fatalf("[FATAL] %v", err) } for _, info := range cfgInfo { switch info.Type.String() { case "string": var dv string if info.DefaultValue != nil { dv = info.DefaultValue.(string) } p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string) flag.StringVar(p, info.Name, getEnvString(info.Name, dv), "("+info.Key+")") case "bool": var dv bool if info.DefaultValue != nil { dv = info.DefaultValue.(bool) } p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool) flag.BoolVar(p, info.Name, getEnvBool(info.Name, dv), "("+info.Key+")") case "int": var dv int if info.DefaultValue != nil { dv = int(info.DefaultValue.(int64)) } p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int) flag.IntVar(p, info.Name, getEnvInt(info.Name, dv), "("+info.Key+")") } } flag.Var(&hosts, "host", "Host to monitor") flag.Parse() cfg.Hosts = hosts // set logging level cfg.setLogLevel() // timezone & format configuration cfg.TZoneUTC, _ = time.LoadLocation("UTC") if err != nil { log.Fatalf("[ERROR] Unable to parse timezone string. Please use one of the timezone database values listed here: %s", "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones") } cfg.TZoneLocal, err = time.LoadLocation(cfg.TimeZoneLocal) if err != nil { log.Fatalf("[ERROR] Unable to parse timezone string. Please use one of the timezone database values listed here: %s", "https://en.wikipedia.org/wiki/List_of_tz_database_time_zones") } time.Now().Format(cfg.TimeFormat) prometheus.MustRegister(cfg.Prometheus.CurrentMa, cfg.Prometheus.VoltageMv, cfg.Prometheus.PowerMw, cfg.Prometheus.TotalWh) // print running config cfg.printRunningConfig(cfgInfo) log.Println("[INFO] initialization complete") return cfg }