Prevent poller from dying from an error

This commit is contained in:
davidnewhall2 2019-12-09 00:45:46 -08:00
parent b64179cc07
commit d1a1de9bcc
7 changed files with 50 additions and 64 deletions

View File

@ -103,7 +103,7 @@
name = "golang.org/x/sys" name = "golang.org/x/sys"
packages = ["windows"] packages = ["windows"]
pruneopts = "UT" pruneopts = "UT"
revision = "ce4227a45e2eb77e5c847278dcc6a626742e2945" revision = "eeba5f6aabab6d6594a9191d6bfeaca5fa6a8248"
[[projects]] [[projects]]
digest = "1:87738e338f505d3e3be1f80d36b53f3c4e73be9b7ad4ccae46abbe9ef04f3f71" digest = "1:87738e338f505d3e3be1f80d36b53f3c4e73be9b7ad4ccae46abbe9ef04f3f71"

View File

@ -1,5 +1,13 @@
package poller package poller
/*
I consider this file the pinacle example of how to allow a Go application to be configured from a file.
You can put your configuration into any file format: XML, YAML, JSON, TOML, and you can override any
struct member using an environment variable. The Duration type is also supported. All of the Config{}
and Duration{} types and methods are reusable in other projects. Just adjust the data in the struct to
meet your app's needs. See the New() procedure and Start() method in start.go for example usage.
*/
import ( import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
@ -41,12 +49,11 @@ const ENVConfigPrefix = "UP_"
// UnifiPoller contains the application startup data, and auth info for UniFi & Influx. // UnifiPoller contains the application startup data, and auth info for UniFi & Influx.
type UnifiPoller struct { type UnifiPoller struct {
Influx *influxunifi.InfluxUnifi Influx *influxunifi.InfluxUnifi
Unifi *unifi.Unifi Unifi *unifi.Unifi
Flag *Flag Flag *Flag
Config *Config Config *Config
errorCount int LastCheck time.Time
LastCheck time.Time
} }
// Flag represents the CLI args available and their settings. // Flag represents the CLI args available and their settings.
@ -110,44 +117,50 @@ func (c *Config) ParseFile(configFile string) error {
// ParseENV copies environment variables into configuration values. // ParseENV copies environment variables into configuration values.
// This is useful for Docker users that find it easier to pass ENV variables // This is useful for Docker users that find it easier to pass ENV variables
// than a specific configuration file. Uses reflection to find struct tags. // than a specific configuration file. Uses reflection to find struct tags.
// This method uses the json struct tag member to match environment variables.
// Use a custom tag name by changing "json" below, but that's overkill for this app.
func (c *Config) ParseENV() error { func (c *Config) ParseENV() error {
t := reflect.TypeOf(Config{}) // Get tag names from the Config struct. t := reflect.TypeOf(*c) // Get "types" from the Config struct.
// Loop each Config struct member; get reflect tag & env var value; update config. for i := 0; i < t.NumField(); i++ { // Loop each Config struct member
for i := 0; i < t.NumField(); i++ {
tag := t.Field(i).Tag.Get("json") // Get the ENV variable name from "json" struct tag tag := t.Field(i).Tag.Get("json") // Get the ENV variable name from "json" struct tag
tag = strings.Split(strings.ToUpper(tag), ",")[0] // Capitalize and remove ,omitempty suffix tag = strings.Split(strings.ToUpper(tag), ",")[0] // Capitalize and remove ,omitempty suffix
env := os.Getenv(ENVConfigPrefix + tag) // Then pull value from OS. env := os.Getenv(ENVConfigPrefix + tag) // Then pull value from OS.
if tag == "" || env == "" { if tag == "" || env == "" { // Skip if either are empty.
continue // Skip if either are empty. continue
} }
// Reflect and update the u.Config struct member at position i. // Reflect and update the u.Config struct member at position i.
switch c := reflect.ValueOf(c).Elem().Field(i); c.Type().String() { switch field := reflect.ValueOf(c).Elem().Field(i); field.Type().String() {
// Handle each member type appropriately (differently). // Handle each member type appropriately (differently).
case "string": case "string":
// This is a reflect package method to update a struct member by index. // This is a reflect package method to update a struct member by index.
c.SetString(env) field.SetString(env)
case "int": case "int":
val, err := strconv.Atoi(env) val, err := strconv.Atoi(env)
if err != nil { if err != nil {
return fmt.Errorf("%s: %v", tag, err) return fmt.Errorf("%s: %v", tag, err)
} }
c.Set(reflect.ValueOf(val)) field.Set(reflect.ValueOf(val))
case "[]string": case "[]string":
c.Set(reflect.ValueOf(strings.Split(env, ","))) field.Set(reflect.ValueOf(strings.Split(env, ",")))
case path.Base(t.PkgPath()) + ".Duration": case path.Base(t.PkgPath()) + ".Duration":
val, err := time.ParseDuration(env) val, err := time.ParseDuration(env)
if err != nil { if err != nil {
return fmt.Errorf("%s: %v", tag, err) return fmt.Errorf("%s: %v", tag, err)
} }
c.Set(reflect.ValueOf(Duration{val})) field.Set(reflect.ValueOf(Duration{val}))
case "bool": case "bool":
val, err := strconv.ParseBool(env) val, err := strconv.ParseBool(env)
if err != nil { if err != nil {
return fmt.Errorf("%s: %v", tag, err) return fmt.Errorf("%s: %v", tag, err)
} }
c.SetBool(val) field.SetBool(val)
} }
// Add more types here if more types are added to the config struct.
} }
return nil return nil

View File

@ -8,15 +8,6 @@ import (
const callDepth = 2 const callDepth = 2
// LogError logs an error and increments the error counter.
// Should be used in the poller loop.
func (u *UnifiPoller) LogError(err error, prefix string) {
if err != nil {
u.errorCount++
_ = log.Output(callDepth, fmt.Sprintf("[ERROR] %v: %v", prefix, err))
}
}
// StringInSlice returns true if a string is in a slice. // StringInSlice returns true if a string is in a slice.
func StringInSlice(str string, slice []string) bool { func StringInSlice(str string, slice []string) bool {
for _, s := range slice { for _, s := range slice {
@ -41,7 +32,7 @@ func (u *UnifiPoller) LogDebugf(m string, v ...interface{}) {
} }
} }
// LogErrorf prints an error log entry. This is used for external library logging. // LogErrorf prints an error log entry.
func (u *UnifiPoller) LogErrorf(m string, v ...interface{}) { func (u *UnifiPoller) LogErrorf(m string, v ...interface{}) {
_ = log.Output(callDepth, fmt.Sprintf("[ERROR] "+m, v...)) _ = log.Output(callDepth, fmt.Sprintf("[ERROR] "+m, v...))
} }

View File

@ -42,7 +42,7 @@ func (u *UnifiPoller) CollectAndProcess() error {
u.AugmentMetrics(metrics) u.AugmentMetrics(metrics)
report, err := u.Influx.ReportMetrics(metrics) report, err := u.Influx.ReportMetrics(metrics)
if err != nil { if err != nil {
u.LogError(err, "processing metrics") u.LogErrorf("processing metrics: %v", err)
return err return err
} }
u.LogInfluxReport(report) u.LogInfluxReport(report)

View File

@ -15,7 +15,7 @@ const oneDecimalPoint = 10
// RunPrometheus starts the web server and registers the collector. // RunPrometheus starts the web server and registers the collector.
func (u *UnifiPoller) RunPrometheus() error { func (u *UnifiPoller) RunPrometheus() error {
u.Logf("Exporting Measurements at https://%s/metrics for Prometheus", u.Config.HTTPListen) u.Logf("Exporting Measurements for Prometheus at https://%s/metrics", u.Config.HTTPListen)
http.Handle("/metrics", promhttp.Handler()) http.Handle("/metrics", promhttp.Handler())
prometheus.MustRegister(promunifi.NewUnifiCollector(promunifi.UnifiCollectorCnfg{ prometheus.MustRegister(promunifi.NewUnifiCollector(promunifi.UnifiCollectorCnfg{
Namespace: strings.Replace(u.Config.Namespace, "-", "", -1), Namespace: strings.Replace(u.Config.Namespace, "-", "", -1),
@ -34,8 +34,9 @@ func (u *UnifiPoller) ExportMetrics() (*metrics.Metrics, error) {
if err != nil { if err != nil {
u.LogErrorf("collecting metrics: %v", err) u.LogErrorf("collecting metrics: %v", err)
u.Logf("Re-authenticating to UniFi Controller") u.Logf("Re-authenticating to UniFi Controller")
if err := u.Unifi.Login(); err != nil { if err := u.Unifi.Login(); err != nil {
u.LogError(err, "re-authenticating") u.LogErrorf("re-authenticating: %v", err)
return nil, err return nil, err
} }

View File

@ -100,55 +100,36 @@ func (u *UnifiPoller) Run() error {
switch strings.ToLower(u.Config.Mode) { switch strings.ToLower(u.Config.Mode) {
default: default:
return u.PollController() u.PollController()
return nil
case "influxlambda", "lambdainflux", "lambda_influx", "influx_lambda": case "influxlambda", "lambdainflux", "lambda_influx", "influx_lambda":
u.LastCheck = time.Now() u.LastCheck = time.Now()
return u.CollectAndProcess() return u.CollectAndProcess()
case "both":
go u.PollController()
fallthrough
case "prometheus", "exporter": case "prometheus", "exporter":
return u.RunPrometheus() return u.RunPrometheus()
case "both":
return u.RunBoth()
} }
} }
// RunBoth starts the prometheus exporter and influxdb exporter at the same time.
// This will likely double the amount of polls your controller receives.
func (u *UnifiPoller) RunBoth() error {
e := make(chan error)
defer close(e)
go func() {
e <- u.RunPrometheus()
}()
go func() {
e <- u.PollController()
}()
// If either method returns an error (even nil), bail out.
return <-e
}
// PollController runs forever, polling UniFi and pushing to InfluxDB // PollController runs forever, polling UniFi and pushing to InfluxDB
// This is started by Run() or RunBoth() after everything checks out. // This is started by Run() or RunBoth() after everything checks out.
func (u *UnifiPoller) PollController() error { func (u *UnifiPoller) PollController() {
interval := u.Config.Interval.Round(time.Second) interval := u.Config.Interval.Round(time.Second)
log.Printf("[INFO] Everything checks out! Poller started, interval: %v", interval) log.Printf("[INFO] Everything checks out! Poller started, InfluxDB interval: %v", interval)
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop()
for u.LastCheck = range ticker.C { for u.LastCheck = range ticker.C {
// Some users need to re-auth every interval because the cookie times out. // Some users need to re-auth every interval because the cookie times out.
if u.Config.ReAuth { if u.Config.ReAuth {
u.LogDebugf("Re-authenticating to UniFi Controller") u.LogDebugf("Re-authenticating to UniFi Controller")
if err := u.Unifi.Login(); err != nil { if err := u.Unifi.Login(); err != nil {
return err u.LogErrorf("%v", err)
continue
} }
} }
if err := u.CollectAndProcess(); err != nil { if err := u.CollectAndProcess(); err != nil {
return err u.LogErrorf("%v", err)
}
// check for errors from the unifi polls.
if u.errorCount > 0 {
return fmt.Errorf("too many errors, stopping poller")
} }
} }
return nil
} }

View File

@ -67,16 +67,16 @@ func (u *UnifiPoller) CollectMetrics() (*metrics.Metrics, error) {
var err error var err error
// Get the sites we care about. // Get the sites we care about.
m.Sites, err = u.GetFilteredSites() m.Sites, err = u.GetFilteredSites()
u.LogError(err, "unifi.GetSites()") u.LogErrorf("unifi.GetSites(): %v", err)
if u.Config.SaveIDS { if u.Config.SaveIDS {
m.IDSList, err = u.Unifi.GetIDS(m.Sites, time.Now().Add(u.Config.Interval.Duration), time.Now()) m.IDSList, err = u.Unifi.GetIDS(m.Sites, time.Now().Add(u.Config.Interval.Duration), time.Now())
u.LogError(err, "unifi.GetIDS()") u.LogErrorf("unifi.GetIDS(): %v", err)
} }
// Get all the points. // Get all the points.
m.Clients, err = u.Unifi.GetClients(m.Sites) m.Clients, err = u.Unifi.GetClients(m.Sites)
u.LogError(err, "unifi.GetClients()") u.LogErrorf("unifi.GetClients(): %v", err)
m.Devices, err = u.Unifi.GetDevices(m.Sites) m.Devices, err = u.Unifi.GetDevices(m.Sites)
u.LogError(err, "unifi.GetDevices()") u.LogErrorf("unifi.GetDevices(): %v", err)
return m, err return m, err
} }