diff --git a/Gopkg.lock b/Gopkg.lock index 08130d43..615d1159 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -27,7 +27,7 @@ [[projects]] branch = "master" - digest = "1:50708c8fc92aec981df5c446581cf9f90ba9e2a5692118e0ce75d4534aaa14a2" + digest = "1:00e5ad58045d6d2a6c9e65d1809ff2594bc396e911712ae892a93976fdece115" name = "github.com/influxdata/influxdb1-client" packages = [ "models", @@ -35,7 +35,7 @@ "v2", ] pruneopts = "UT" - revision = "fc22c7df067eefd070157f157893fbce961d6359" + revision = "8bf82d3c094dc06be9da8e5bf9d3589b6ea032ae" [[projects]] digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc" @@ -103,15 +103,15 @@ name = "golang.org/x/sys" packages = ["windows"] pruneopts = "UT" - revision = "ce4227a45e2eb77e5c847278dcc6a626742e2945" + revision = "ac6580df4449443a05718fd7858c1f91ad5f8d20" [[projects]] - digest = "1:87738e338f505d3e3be1f80d36b53f3c4e73be9b7ad4ccae46abbe9ef04f3f71" + digest = "1:2883cea734f2766f41ff9c9d4aefccccc53e3d44f5c8b08893b9c218cf666722" name = "golift.io/unifi" packages = ["."] pruneopts = "UT" - revision = "ba857a3a04311fed362cb43fa7bf4066bc3a7e55" - version = "v4.1.5" + revision = "a607fe940c6a563c6994f2c945394b19d2183b1c" + version = "v4.1.6" [[projects]] digest = "1:b75b3deb2bce8bc079e16bb2aecfe01eb80098f5650f9e93e5643ca8b7b73737" diff --git a/pkg/influxunifi/uap.go b/pkg/influxunifi/uap.go index f6f8e8ad..46c25c32 100644 --- a/pkg/influxunifi/uap.go +++ b/pkg/influxunifi/uap.go @@ -7,8 +7,8 @@ import ( // batchUAP generates Wireless-Access-Point datapoints for InfluxDB. // These points can be passed directly to influx. func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) { - if s.Stat.Ap == nil { - s.Stat.Ap = &unifi.Ap{} + if !s.Adopted.Val || s.Locating.Val { + return } tags := map[string]string{ "mac": s.Mac, @@ -36,6 +36,9 @@ func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) { } func (u *InfluxUnifi) processUAPstats(ap *unifi.Ap) map[string]interface{} { + if ap == nil { + return map[string]interface{}{} + } // Accumulative Statistics. return map[string]interface{}{ "stat_user-rx_packets": ap.UserRxPackets.Val, diff --git a/pkg/influxunifi/udm.go b/pkg/influxunifi/udm.go index 2e93ce8e..af42e5d0 100644 --- a/pkg/influxunifi/udm.go +++ b/pkg/influxunifi/udm.go @@ -33,11 +33,8 @@ func (u *InfluxUnifi) batchSysStats(s unifi.SysStats, ss unifi.SystemStats) map[ // batchUDM generates Unifi Gateway datapoints for InfluxDB. // These points can be passed directly to influx. func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) { - if s.Stat.Sw == nil { - s.Stat.Sw = &unifi.Sw{} - } - if s.Stat.Gw == nil { - s.Stat.Gw = &unifi.Gw{} + if !s.Adopted.Val || s.Locating.Val { + return } tags := map[string]string{ "mac": s.Mac, @@ -81,28 +78,18 @@ func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) { "serial": s.Serial, "type": s.Type, } - fields = map[string]interface{}{ - "guest-num_sta": s.GuestNumSta.Val, - "ip": s.IP, - "bytes": s.Bytes.Val, - "last_seen": s.LastSeen.Val, - "rx_bytes": s.RxBytes.Val, - "tx_bytes": s.TxBytes.Val, - "uptime": s.Uptime.Val, - "state": s.State.Val, - "stat_bytes": s.Stat.Sw.Bytes.Val, - "stat_rx_bytes": s.Stat.Sw.RxBytes.Val, - "stat_rx_crypts": s.Stat.Sw.RxCrypts.Val, - "stat_rx_dropped": s.Stat.Sw.RxDropped.Val, - "stat_rx_errors": s.Stat.Sw.RxErrors.Val, - "stat_rx_frags": s.Stat.Sw.RxFrags.Val, - "stat_rx_packets": s.Stat.Sw.TxPackets.Val, - "stat_tx_bytes": s.Stat.Sw.TxBytes.Val, - "stat_tx_dropped": s.Stat.Sw.TxDropped.Val, - "stat_tx_errors": s.Stat.Sw.TxErrors.Val, - "stat_tx_packets": s.Stat.Sw.TxPackets.Val, - "stat_tx_retries": s.Stat.Sw.TxRetries.Val, - } + fields = Combine( + u.batchUSWstat(s.Stat.Sw), + map[string]interface{}{ + "guest-num_sta": s.GuestNumSta.Val, + "ip": s.IP, + "bytes": s.Bytes.Val, + "last_seen": s.LastSeen.Val, + "rx_bytes": s.RxBytes.Val, + "tx_bytes": s.TxBytes.Val, + "uptime": s.Uptime.Val, + "state": s.State.Val, + }) r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) u.batchPortTable(r, tags, s.PortTable) diff --git a/pkg/influxunifi/usg.go b/pkg/influxunifi/usg.go index 0993be1a..221e0e40 100644 --- a/pkg/influxunifi/usg.go +++ b/pkg/influxunifi/usg.go @@ -7,8 +7,8 @@ import ( // batchUSG generates Unifi Gateway datapoints for InfluxDB. // These points can be passed directly to influx. func (u *InfluxUnifi) batchUSG(r report, s *unifi.USG) { - if s.Stat.Gw == nil { - s.Stat.Gw = &unifi.Gw{} + if !s.Adopted.Val || s.Locating.Val { + return } tags := map[string]string{ "mac": s.Mac, @@ -74,6 +74,9 @@ func (u *InfluxUnifi) batchUSG(r report, s *unifi.USG) { */ } func (u *InfluxUnifi) batchUSGstat(ss unifi.SpeedtestStatus, gw *unifi.Gw, ul unifi.Uplink) map[string]interface{} { + if gw == nil { + return map[string]interface{}{} + } return map[string]interface{}{ "uplink_latency": ul.Latency.Val, "uplink_speed": ul.Speed.Val, diff --git a/pkg/influxunifi/usw.go b/pkg/influxunifi/usw.go index a62f5f81..7bc31c37 100644 --- a/pkg/influxunifi/usw.go +++ b/pkg/influxunifi/usw.go @@ -7,9 +7,10 @@ import ( // batchUSW generates Unifi Switch datapoints for InfluxDB. // These points can be passed directly to influx. func (u *InfluxUnifi) batchUSW(r report, s *unifi.USW) { - if s.Stat.Sw == nil { - s.Stat.Sw = &unifi.Sw{} + if !s.Adopted.Val || s.Locating.Val { + return } + tags := map[string]string{ "mac": s.Mac, "site_name": s.SiteName, @@ -19,35 +20,45 @@ func (u *InfluxUnifi) batchUSW(r report, s *unifi.USW) { "serial": s.Serial, "type": s.Type, } - fields := Combine(map[string]interface{}{ - "guest-num_sta": s.GuestNumSta.Val, - "ip": s.IP, - "bytes": s.Bytes.Val, - "fan_level": s.FanLevel.Val, - "general_temperature": s.GeneralTemperature.Val, - "last_seen": s.LastSeen.Val, - "rx_bytes": s.RxBytes.Val, - "tx_bytes": s.TxBytes.Val, - "uptime": s.Uptime.Val, - "state": s.State.Val, - "user-num_sta": s.UserNumSta.Val, - "stat_bytes": s.Stat.Sw.Bytes.Val, - "stat_rx_bytes": s.Stat.Sw.RxBytes.Val, - "stat_rx_crypts": s.Stat.Sw.RxCrypts.Val, - "stat_rx_dropped": s.Stat.Sw.RxDropped.Val, - "stat_rx_errors": s.Stat.Sw.RxErrors.Val, - "stat_rx_frags": s.Stat.Sw.RxFrags.Val, - "stat_rx_packets": s.Stat.Sw.TxPackets.Val, - "stat_tx_bytes": s.Stat.Sw.TxBytes.Val, - "stat_tx_dropped": s.Stat.Sw.TxDropped.Val, - "stat_tx_errors": s.Stat.Sw.TxErrors.Val, - "stat_tx_packets": s.Stat.Sw.TxPackets.Val, - "stat_tx_retries": s.Stat.Sw.TxRetries.Val, - }, u.batchSysStats(s.SysStats, s.SystemStats)) + fields := Combine( + u.batchUSWstat(s.Stat.Sw), + u.batchSysStats(s.SysStats, s.SystemStats), + map[string]interface{}{ + "guest-num_sta": s.GuestNumSta.Val, + "ip": s.IP, + "bytes": s.Bytes.Val, + "fan_level": s.FanLevel.Val, + "general_temperature": s.GeneralTemperature.Val, + "last_seen": s.LastSeen.Val, + "rx_bytes": s.RxBytes.Val, + "tx_bytes": s.TxBytes.Val, + "uptime": s.Uptime.Val, + "state": s.State.Val, + "user-num_sta": s.UserNumSta.Val, + }) r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) u.batchPortTable(r, tags, s.PortTable) } +func (u *InfluxUnifi) batchUSWstat(sw *unifi.Sw) map[string]interface{} { + if sw == nil { + return map[string]interface{}{} + } + return map[string]interface{}{ + "stat_bytes": sw.Bytes.Val, + "stat_rx_bytes": sw.RxBytes.Val, + "stat_rx_crypts": sw.RxCrypts.Val, + "stat_rx_dropped": sw.RxDropped.Val, + "stat_rx_errors": sw.RxErrors.Val, + "stat_rx_frags": sw.RxFrags.Val, + "stat_rx_packets": sw.TxPackets.Val, + "stat_tx_bytes": sw.TxBytes.Val, + "stat_tx_dropped": sw.TxDropped.Val, + "stat_tx_errors": sw.TxErrors.Val, + "stat_tx_packets": sw.TxPackets.Val, + "stat_tx_retries": sw.TxRetries.Val, + } +} func (u *InfluxUnifi) batchPortTable(r report, t map[string]string, pt []unifi.Port) { for _, p := range pt { if !p.Up.Val || !p.Enable.Val { diff --git a/pkg/poller/config.go b/pkg/poller/config.go index fd914f9f..1725b03a 100644 --- a/pkg/poller/config.go +++ b/pkg/poller/config.go @@ -1,5 +1,13 @@ 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 ( "encoding/json" "encoding/xml" @@ -10,6 +18,7 @@ import ( "reflect" "strconv" "strings" + "sync" "time" "github.com/BurntSushi/toml" @@ -45,8 +54,8 @@ type UnifiPoller struct { Unifi *unifi.Unifi Flag *Flag Config *Config - errorCount int LastCheck time.Time + sync.Mutex // locks the Unifi struct member when re-authing to unifi. } // Flag represents the CLI args available and their settings. @@ -110,44 +119,50 @@ func (c *Config) ParseFile(configFile string) error { // ParseENV copies environment variables into configuration values. // 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. +// 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 { - t := reflect.TypeOf(Config{}) // Get tag names from the Config struct. - // Loop each Config struct member; get reflect tag & env var value; update config. - for i := 0; i < t.NumField(); i++ { + t := reflect.TypeOf(*c) // Get "types" from the Config struct. + for i := 0; i < t.NumField(); i++ { // Loop each Config struct member 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 env := os.Getenv(ENVConfigPrefix + tag) // Then pull value from OS. - if tag == "" || env == "" { - continue // Skip if either are empty. + if tag == "" || env == "" { // Skip if either are empty. + continue } // 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). case "string": // This is a reflect package method to update a struct member by index. - c.SetString(env) + field.SetString(env) + case "int": val, err := strconv.Atoi(env) if err != nil { return fmt.Errorf("%s: %v", tag, err) } - c.Set(reflect.ValueOf(val)) + field.Set(reflect.ValueOf(val)) + case "[]string": - c.Set(reflect.ValueOf(strings.Split(env, ","))) + field.Set(reflect.ValueOf(strings.Split(env, ","))) + case path.Base(t.PkgPath()) + ".Duration": val, err := time.ParseDuration(env) if err != nil { return fmt.Errorf("%s: %v", tag, err) } - c.Set(reflect.ValueOf(Duration{val})) + field.Set(reflect.ValueOf(Duration{val})) + case "bool": val, err := strconv.ParseBool(env) if err != nil { 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 diff --git a/pkg/poller/dumper.go b/pkg/poller/dumper.go index da825485..09514e57 100644 --- a/pkg/poller/dumper.go +++ b/pkg/poller/dumper.go @@ -35,9 +35,9 @@ func (u *UnifiPoller) DumpJSONPayload() (err error) { case err != nil: return err case StringInSlice(u.Flag.DumpJSON, []string{"d", "device", "devices"}): - return u.dumpSitesJSON(unifi.DevicePath, "Devices", sites) + return u.dumpSitesJSON(unifi.APIDevicePath, "Devices", sites) case StringInSlice(u.Flag.DumpJSON, []string{"client", "clients", "c"}): - return u.dumpSitesJSON(unifi.ClientPath, "Clients", sites) + return u.dumpSitesJSON(unifi.APIClientPath, "Clients", sites) case strings.HasPrefix(u.Flag.DumpJSON, "other "): apiPath := strings.SplitN(u.Flag.DumpJSON, " ", 2)[1] _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", apiPath) diff --git a/pkg/poller/helpers.go b/pkg/poller/helpers.go index 71421b16..92acd223 100644 --- a/pkg/poller/helpers.go +++ b/pkg/poller/helpers.go @@ -8,15 +8,6 @@ import ( 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. func StringInSlice(str string, slice []string) bool { 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{}) { _ = log.Output(callDepth, fmt.Sprintf("[ERROR] "+m, v...)) } diff --git a/pkg/poller/influx.go b/pkg/poller/influx.go index 2abd83ad..ff2384e8 100644 --- a/pkg/poller/influx.go +++ b/pkg/poller/influx.go @@ -12,6 +12,7 @@ func (u *UnifiPoller) GetInfluxDB() (err error) { if u.Influx != nil { return nil } + u.Influx, err = influxunifi.New(&influxunifi.Config{ Database: u.Config.InfluxDB, User: u.Config.InfluxUser, @@ -22,7 +23,9 @@ func (u *UnifiPoller) GetInfluxDB() (err error) { if err != nil { return fmt.Errorf("influxdb: %v", err) } + u.Logf("Logging Measurements to InfluxDB at %s as user %s", u.Config.InfluxURL, u.Config.InfluxUser) + return nil } @@ -35,16 +38,19 @@ func (u *UnifiPoller) CollectAndProcess() error { if err := u.GetInfluxDB(); err != nil { return err } + metrics, err := u.CollectMetrics() if err != nil { return err } + u.AugmentMetrics(metrics) + report, err := u.Influx.ReportMetrics(metrics) if err != nil { - u.LogError(err, "processing metrics") return err } + u.LogInfluxReport(report) return nil } @@ -52,9 +58,11 @@ func (u *UnifiPoller) CollectAndProcess() error { // LogInfluxReport writes a log message after exporting to influxdb. func (u *UnifiPoller) LogInfluxReport(r *influxunifi.Report) { idsMsg := "" + if u.Config.SaveIDS { idsMsg = fmt.Sprintf("IDS Events: %d, ", len(r.Metrics.IDSList)) } + u.Logf("UniFi Metrics Recorded. Sites: %d, Clients: %d, "+ "UAP: %d, USG/UDM: %d, USW: %d, %sPoints: %d, Fields: %d, Errs: %d, Elapsed: %v", len(r.Metrics.Sites), len(r.Metrics.Clients), len(r.Metrics.UAPs), diff --git a/pkg/poller/prometheus.go b/pkg/poller/prometheus.go index 45dd1176..59aba109 100644 --- a/pkg/poller/prometheus.go +++ b/pkg/poller/prometheus.go @@ -15,7 +15,7 @@ const oneDecimalPoint = 10 // RunPrometheus starts the web server and registers the collector. 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()) prometheus.MustRegister(promunifi.NewUnifiCollector(promunifi.UnifiCollectorCnfg{ Namespace: strings.Replace(u.Config.Namespace, "-", "", -1), @@ -23,6 +23,7 @@ func (u *UnifiPoller) RunPrometheus() error { LoggingFn: u.LogExportReport, ReportErrors: true, // XXX: Does this need to be configurable? })) + return http.ListenAndServe(u.Config.HTTPListen, nil) } @@ -34,8 +35,9 @@ func (u *UnifiPoller) ExportMetrics() (*metrics.Metrics, error) { if err != nil { u.LogErrorf("collecting metrics: %v", err) u.Logf("Re-authenticating to UniFi Controller") - if err := u.Unifi.Login(); err != nil { - u.LogError(err, "re-authenticating") + + if err := u.GetUnifi(); err != nil { + u.LogErrorf("re-authenticating: %v", err) return nil, err } diff --git a/pkg/poller/start.go b/pkg/poller/start.go index 1e1719c8..3f1a4d74 100644 --- a/pkg/poller/start.go +++ b/pkg/poller/start.go @@ -28,7 +28,10 @@ func New() *UnifiPoller { SaveSites: true, HTTPListen: defaultHTTPListen, Namespace: appName, - }, Flag: &Flag{ConfigFile: DefaultConfFile}, + }, + Flag: &Flag{ + ConfigFile: DefaultConfFile, + }, } } @@ -36,6 +39,7 @@ func New() *UnifiPoller { // Parses cli flags, parses config file, parses env vars, sets up logging, then: // - dumps a json payload OR - executes Run(). func (u *UnifiPoller) Start() error { + log.SetOutput(os.Stdout) log.SetFlags(log.LstdFlags) u.Flag.Parse(os.Args[1:]) @@ -92,63 +96,44 @@ func (f *Flag) Parse(args []string) { // 2. Run the collector one time and report the metrics to influxdb. (lambda) // 3. Start a web server and wait for Prometheus to poll the application for metrics. func (u *UnifiPoller) Run() error { - if err := u.GetUnifi(); err != nil { - return err + switch err := u.GetUnifi(); err { + case nil: + u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v", + u.Config.UnifiBase, u.Unifi.ServerVersion, u.Config.UnifiUser, u.Config.Sites) + default: + u.LogErrorf("Controller Auth or Connection failed, but continuing to retry! %v", err) } - u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v", - u.Config.UnifiBase, u.Unifi.ServerVersion, u.Config.UnifiUser, u.Config.Sites) switch strings.ToLower(u.Config.Mode) { default: - return u.PollController() + u.PollController() + return nil case "influxlambda", "lambdainflux", "lambda_influx", "influx_lambda": u.LastCheck = time.Now() return u.CollectAndProcess() + case "both": + go u.PollController() + fallthrough case "prometheus", "exporter": 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 // 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) - log.Printf("[INFO] Everything checks out! Poller started, interval: %v", interval) - ticker := time.NewTicker(interval) - defer ticker.Stop() + log.Printf("[INFO] Everything checks out! Poller started, InfluxDB interval: %v", interval) + ticker := time.NewTicker(interval) for u.LastCheck = range ticker.C { - // Some users need to re-auth every interval because the cookie times out. - if u.Config.ReAuth { - u.LogDebugf("Re-authenticating to UniFi Controller") - if err := u.Unifi.Login(); err != nil { - return err + if err := u.CollectAndProcess(); err != nil { + u.LogErrorf("%v", err) + + if u.Unifi != nil { + u.Unifi.CloseIdleConnections() + u.Unifi = nil // trigger re-auth in unifi.go. } } - if err := u.CollectAndProcess(); err != nil { - return err - } - // check for errors from the unifi polls. - if u.errorCount > 0 { - return fmt.Errorf("too many errors, stopping poller") - } } - return nil } diff --git a/pkg/poller/unifi.go b/pkg/poller/unifi.go index 73708243..beeacec5 100644 --- a/pkg/poller/unifi.go +++ b/pkg/poller/unifi.go @@ -11,6 +11,13 @@ import ( // GetUnifi returns a UniFi controller interface. func (u *UnifiPoller) GetUnifi() (err error) { + u.Lock() + defer u.Unlock() + + if u.Unifi != nil { + u.Unifi.CloseIdleConnections() + } + // Create an authenticated session to the Unifi Controller. u.Unifi, err = unifi.NewUnifi(&unifi.Config{ User: u.Config.UnifiUser, @@ -21,8 +28,10 @@ func (u *UnifiPoller) GetUnifi() (err error) { DebugLog: u.LogDebugf, // Log debug messages. }) if err != nil { + u.Unifi = nil return fmt.Errorf("unifi controller: %v", err) } + u.LogDebugf("Authenticated with controller successfully") return u.CheckSites() @@ -34,20 +43,26 @@ func (u *UnifiPoller) CheckSites() error { if strings.Contains(strings.ToLower(u.Config.Mode), "lambda") { return nil // Skip this in lambda mode. } + u.LogDebugf("Checking Controller Sites List") + sites, err := u.Unifi.GetSites() if err != nil { return err } + msg := []string{} + for _, site := range sites { msg = append(msg, site.Name+" ("+site.Desc+")") } u.Logf("Found %d site(s) on controller: %v", len(msg), strings.Join(msg, ", ")) + if StringInSlice("all", u.Config.Sites) { u.Config.Sites = []string{"all"} return nil } + FIRST: for _, s := range u.Config.Sites { for _, site := range sites { @@ -58,26 +73,44 @@ FIRST: // This is fine, it may get added later. u.LogErrorf("configured site not found on controller: %v", s) } + return nil } // CollectMetrics grabs all the measurements from a UniFi controller and returns them. func (u *UnifiPoller) CollectMetrics() (*metrics.Metrics, error) { - m := &metrics.Metrics{TS: u.LastCheck} // At this point, it's the Current Check. var err error + + if u.Unifi == nil || u.Config.ReAuth { + // Some users need to re-auth every interval because the cookie times out. + // Sometimes we hit this path when the controller dies. + u.Logf("Re-authenticating to UniFi Controller") + if err := u.GetUnifi(); err != nil { + return nil, err + } + } + + m := &metrics.Metrics{TS: u.LastCheck} // At this point, it's the Current Check. // Get the sites we care about. - m.Sites, err = u.GetFilteredSites() - u.LogError(err, "unifi.GetSites()") + if m.Sites, err = u.GetFilteredSites(); err != nil { + return m, fmt.Errorf("unifi.GetSites(): %v", err) + } + if u.Config.SaveIDS { m.IDSList, err = u.Unifi.GetIDS(m.Sites, time.Now().Add(u.Config.Interval.Duration), time.Now()) - u.LogError(err, "unifi.GetIDS()") + return m, fmt.Errorf("unifi.GetIDS(): %v", err) } + // Get all the points. - m.Clients, err = u.Unifi.GetClients(m.Sites) - u.LogError(err, "unifi.GetClients()") - m.Devices, err = u.Unifi.GetDevices(m.Sites) - u.LogError(err, "unifi.GetDevices()") - return m, err + if m.Clients, err = u.Unifi.GetClients(m.Sites); err != nil { + return m, fmt.Errorf("unifi.GetClients(): %v", err) + } + + if m.Devices, err = u.Unifi.GetDevices(m.Sites); err != nil { + return m, fmt.Errorf("unifi.GetDevices(): %v", err) + } + + return m, nil } // AugmentMetrics is our middleware layer between collecting metrics and writing them. @@ -87,23 +120,29 @@ func (u *UnifiPoller) AugmentMetrics(metrics *metrics.Metrics) { if metrics == nil || metrics.Devices == nil || metrics.Clients == nil { return } + devices := make(map[string]string) bssdIDs := make(map[string]string) + for _, r := range metrics.UAPs { devices[r.Mac] = r.Name for _, v := range r.VapTable { bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName) } } + for _, r := range metrics.USGs { devices[r.Mac] = r.Name } + for _, r := range metrics.USWs { devices[r.Mac] = r.Name } + for _, r := range metrics.UDMs { devices[r.Mac] = r.Name } + // These come blank, so set them here. for i, c := range metrics.Clients { metrics.Clients[i].SwName = devices[c.SwMac] @@ -111,6 +150,7 @@ func (u *UnifiPoller) AugmentMetrics(metrics *metrics.Metrics) { metrics.Clients[i].GwName = devices[c.GwMac] metrics.Clients[i].RadioDescription = bssdIDs[metrics.Clients[i].Bssid] + metrics.Clients[i].RadioProto } + if !u.Config.SaveSites { metrics.Sites = nil } @@ -120,13 +160,15 @@ func (u *UnifiPoller) AugmentMetrics(metrics *metrics.Metrics) { // Omits requested but unconfigured sites. Grabs the full list from the // controller and returns the sites provided in the config file. func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) { + var i int + sites, err := u.Unifi.GetSites() if err != nil { return nil, err } else if len(u.Config.Sites) < 1 || StringInSlice("all", u.Config.Sites) { return sites, nil } - var i int + for _, s := range sites { // Only include valid sites in the request filter. if StringInSlice(s.Name, u.Config.Sites) { @@ -134,5 +176,6 @@ func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) { i++ } } + return sites[:i], nil } diff --git a/pkg/promunifi/uap.go b/pkg/promunifi/uap.go index 2dba5a6f..9998df30 100644 --- a/pkg/promunifi/uap.go +++ b/pkg/promunifi/uap.go @@ -159,6 +159,9 @@ func descUAP(ns string) *uap { } func (u *promUnifi) exportUAP(r report, d *unifi.UAP) { + if !d.Adopted.Val || d.Locating.Val { + return + } labels := []string{d.Type, d.SiteName, d.Name} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} u.exportUAPstats(r, labels, d.Stat.Ap, d.BytesD, d.TxBytesD, d.RxBytesD, d.BytesR) @@ -175,6 +178,9 @@ func (u *promUnifi) exportUAP(r report, d *unifi.UAP) { // udm doesn't have these stats exposed yet, so pass 2 or 6 metrics. func (u *promUnifi) exportUAPstats(r report, labels []string, ap *unifi.Ap, bytes ...unifi.FlexInt) { + if ap == nil { + return + } labelU := []string{"user", labels[1], labels[2]} labelG := []string{"guest", labels[1], labels[2]} r.send([]*metric{ diff --git a/pkg/promunifi/udm.go b/pkg/promunifi/udm.go index 83f27526..3ce95117 100644 --- a/pkg/promunifi/udm.go +++ b/pkg/promunifi/udm.go @@ -60,6 +60,9 @@ func descDevice(ns string) *unifiDevice { // UDM is a collection of stats from USG, USW and UAP. It has no unique stats. func (u *promUnifi) exportUDM(r report, d *unifi.UDM) { + if !d.Adopted.Val || d.Locating.Val { + return + } labels := []string{d.Type, d.SiteName, d.Name} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} // Shared data (all devices do this). diff --git a/pkg/promunifi/usg.go b/pkg/promunifi/usg.go index 6c2ffd01..94a43f80 100644 --- a/pkg/promunifi/usg.go +++ b/pkg/promunifi/usg.go @@ -69,6 +69,9 @@ func descUSG(ns string) *usg { } func (u *promUnifi) exportUSG(r report, d *unifi.USG) { + if !d.Adopted.Val || d.Locating.Val { + return + } labels := []string{d.Type, d.SiteName, d.Name} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} // Gateway System Data. @@ -85,6 +88,9 @@ func (u *promUnifi) exportUSG(r report, d *unifi.USG) { // Gateway States func (u *promUnifi) exportUSGstats(r report, labels []string, gw *unifi.Gw, st unifi.SpeedtestStatus, ul unifi.Uplink) { + if gw == nil { + return + } labelLan := []string{"lan", labels[1], labels[2]} labelWan := []string{"all", labels[1], labels[2]} r.send([]*metric{ diff --git a/pkg/promunifi/usw.go b/pkg/promunifi/usw.go index 831e71ee..fd089ea7 100644 --- a/pkg/promunifi/usw.go +++ b/pkg/promunifi/usw.go @@ -91,6 +91,9 @@ func descUSW(ns string) *usw { } func (u *promUnifi) exportUSW(r report, d *unifi.USW) { + if !d.Adopted.Val || d.Locating.Val { + return + } labels := []string{d.Type, d.SiteName, d.Name} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} u.exportUSWstats(r, labels, d.Stat.Sw) @@ -116,6 +119,9 @@ func (u *promUnifi) exportUSW(r report, d *unifi.USW) { // Switch Stats func (u *promUnifi) exportUSWstats(r report, labels []string, sw *unifi.Sw) { + if sw == nil { + return + } labelS := labels[1:] r.send([]*metric{ {u.USW.SwRxPackets, counter, sw.RxPackets, labelS},