Merge pull request #155 from davidnewhall/dn2_dont_die

Prevent poller from dying from an error
This commit is contained in:
David Newhall II 2019-12-11 12:35:38 -08:00 committed by GitHub
commit 740249d1de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 211 additions and 142 deletions

12
Gopkg.lock generated
View File

@ -27,7 +27,7 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:50708c8fc92aec981df5c446581cf9f90ba9e2a5692118e0ce75d4534aaa14a2" digest = "1:00e5ad58045d6d2a6c9e65d1809ff2594bc396e911712ae892a93976fdece115"
name = "github.com/influxdata/influxdb1-client" name = "github.com/influxdata/influxdb1-client"
packages = [ packages = [
"models", "models",
@ -35,7 +35,7 @@
"v2", "v2",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "fc22c7df067eefd070157f157893fbce961d6359" revision = "8bf82d3c094dc06be9da8e5bf9d3589b6ea032ae"
[[projects]] [[projects]]
digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc" digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
@ -103,15 +103,15 @@
name = "golang.org/x/sys" name = "golang.org/x/sys"
packages = ["windows"] packages = ["windows"]
pruneopts = "UT" pruneopts = "UT"
revision = "ce4227a45e2eb77e5c847278dcc6a626742e2945" revision = "ac6580df4449443a05718fd7858c1f91ad5f8d20"
[[projects]] [[projects]]
digest = "1:87738e338f505d3e3be1f80d36b53f3c4e73be9b7ad4ccae46abbe9ef04f3f71" digest = "1:2883cea734f2766f41ff9c9d4aefccccc53e3d44f5c8b08893b9c218cf666722"
name = "golift.io/unifi" name = "golift.io/unifi"
packages = ["."] packages = ["."]
pruneopts = "UT" pruneopts = "UT"
revision = "ba857a3a04311fed362cb43fa7bf4066bc3a7e55" revision = "a607fe940c6a563c6994f2c945394b19d2183b1c"
version = "v4.1.5" version = "v4.1.6"
[[projects]] [[projects]]
digest = "1:b75b3deb2bce8bc079e16bb2aecfe01eb80098f5650f9e93e5643ca8b7b73737" digest = "1:b75b3deb2bce8bc079e16bb2aecfe01eb80098f5650f9e93e5643ca8b7b73737"

View File

@ -7,8 +7,8 @@ import (
// batchUAP generates Wireless-Access-Point datapoints for InfluxDB. // batchUAP generates Wireless-Access-Point datapoints for InfluxDB.
// These points can be passed directly to influx. // These points can be passed directly to influx.
func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) { func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) {
if s.Stat.Ap == nil { if !s.Adopted.Val || s.Locating.Val {
s.Stat.Ap = &unifi.Ap{} return
} }
tags := map[string]string{ tags := map[string]string{
"mac": s.Mac, "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{} { func (u *InfluxUnifi) processUAPstats(ap *unifi.Ap) map[string]interface{} {
if ap == nil {
return map[string]interface{}{}
}
// Accumulative Statistics. // Accumulative Statistics.
return map[string]interface{}{ return map[string]interface{}{
"stat_user-rx_packets": ap.UserRxPackets.Val, "stat_user-rx_packets": ap.UserRxPackets.Val,

View File

@ -33,11 +33,8 @@ func (u *InfluxUnifi) batchSysStats(s unifi.SysStats, ss unifi.SystemStats) map[
// batchUDM generates Unifi Gateway datapoints for InfluxDB. // batchUDM generates Unifi Gateway datapoints for InfluxDB.
// These points can be passed directly to influx. // These points can be passed directly to influx.
func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) { func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) {
if s.Stat.Sw == nil { if !s.Adopted.Val || s.Locating.Val {
s.Stat.Sw = &unifi.Sw{} return
}
if s.Stat.Gw == nil {
s.Stat.Gw = &unifi.Gw{}
} }
tags := map[string]string{ tags := map[string]string{
"mac": s.Mac, "mac": s.Mac,
@ -81,28 +78,18 @@ func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) {
"serial": s.Serial, "serial": s.Serial,
"type": s.Type, "type": s.Type,
} }
fields = map[string]interface{}{ fields = Combine(
"guest-num_sta": s.GuestNumSta.Val, u.batchUSWstat(s.Stat.Sw),
"ip": s.IP, map[string]interface{}{
"bytes": s.Bytes.Val, "guest-num_sta": s.GuestNumSta.Val,
"last_seen": s.LastSeen.Val, "ip": s.IP,
"rx_bytes": s.RxBytes.Val, "bytes": s.Bytes.Val,
"tx_bytes": s.TxBytes.Val, "last_seen": s.LastSeen.Val,
"uptime": s.Uptime.Val, "rx_bytes": s.RxBytes.Val,
"state": s.State.Val, "tx_bytes": s.TxBytes.Val,
"stat_bytes": s.Stat.Sw.Bytes.Val, "uptime": s.Uptime.Val,
"stat_rx_bytes": s.Stat.Sw.RxBytes.Val, "state": s.State.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,
}
r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) r.send(&metric{Table: "usw", Tags: tags, Fields: fields})
u.batchPortTable(r, tags, s.PortTable) u.batchPortTable(r, tags, s.PortTable)

View File

@ -7,8 +7,8 @@ import (
// batchUSG generates Unifi Gateway datapoints for InfluxDB. // batchUSG generates Unifi Gateway datapoints for InfluxDB.
// These points can be passed directly to influx. // These points can be passed directly to influx.
func (u *InfluxUnifi) batchUSG(r report, s *unifi.USG) { func (u *InfluxUnifi) batchUSG(r report, s *unifi.USG) {
if s.Stat.Gw == nil { if !s.Adopted.Val || s.Locating.Val {
s.Stat.Gw = &unifi.Gw{} return
} }
tags := map[string]string{ tags := map[string]string{
"mac": s.Mac, "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{} { 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{}{ return map[string]interface{}{
"uplink_latency": ul.Latency.Val, "uplink_latency": ul.Latency.Val,
"uplink_speed": ul.Speed.Val, "uplink_speed": ul.Speed.Val,

View File

@ -7,9 +7,10 @@ import (
// batchUSW generates Unifi Switch datapoints for InfluxDB. // batchUSW generates Unifi Switch datapoints for InfluxDB.
// These points can be passed directly to influx. // These points can be passed directly to influx.
func (u *InfluxUnifi) batchUSW(r report, s *unifi.USW) { func (u *InfluxUnifi) batchUSW(r report, s *unifi.USW) {
if s.Stat.Sw == nil { if !s.Adopted.Val || s.Locating.Val {
s.Stat.Sw = &unifi.Sw{} return
} }
tags := map[string]string{ tags := map[string]string{
"mac": s.Mac, "mac": s.Mac,
"site_name": s.SiteName, "site_name": s.SiteName,
@ -19,35 +20,45 @@ func (u *InfluxUnifi) batchUSW(r report, s *unifi.USW) {
"serial": s.Serial, "serial": s.Serial,
"type": s.Type, "type": s.Type,
} }
fields := Combine(map[string]interface{}{ fields := Combine(
"guest-num_sta": s.GuestNumSta.Val, u.batchUSWstat(s.Stat.Sw),
"ip": s.IP, u.batchSysStats(s.SysStats, s.SystemStats),
"bytes": s.Bytes.Val, map[string]interface{}{
"fan_level": s.FanLevel.Val, "guest-num_sta": s.GuestNumSta.Val,
"general_temperature": s.GeneralTemperature.Val, "ip": s.IP,
"last_seen": s.LastSeen.Val, "bytes": s.Bytes.Val,
"rx_bytes": s.RxBytes.Val, "fan_level": s.FanLevel.Val,
"tx_bytes": s.TxBytes.Val, "general_temperature": s.GeneralTemperature.Val,
"uptime": s.Uptime.Val, "last_seen": s.LastSeen.Val,
"state": s.State.Val, "rx_bytes": s.RxBytes.Val,
"user-num_sta": s.UserNumSta.Val, "tx_bytes": s.TxBytes.Val,
"stat_bytes": s.Stat.Sw.Bytes.Val, "uptime": s.Uptime.Val,
"stat_rx_bytes": s.Stat.Sw.RxBytes.Val, "state": s.State.Val,
"stat_rx_crypts": s.Stat.Sw.RxCrypts.Val, "user-num_sta": s.UserNumSta.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))
r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) r.send(&metric{Table: "usw", Tags: tags, Fields: fields})
u.batchPortTable(r, tags, s.PortTable) 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) { func (u *InfluxUnifi) batchPortTable(r report, t map[string]string, pt []unifi.Port) {
for _, p := range pt { for _, p := range pt {
if !p.Up.Val || !p.Enable.Val { if !p.Up.Val || !p.Enable.Val {

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"
@ -10,6 +18,7 @@ import (
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
@ -45,8 +54,8 @@ type UnifiPoller struct {
Unifi *unifi.Unifi Unifi *unifi.Unifi
Flag *Flag Flag *Flag
Config *Config Config *Config
errorCount int
LastCheck time.Time LastCheck time.Time
sync.Mutex // locks the Unifi struct member when re-authing to unifi.
} }
// Flag represents the CLI args available and their settings. // 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. // 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

@ -35,9 +35,9 @@ func (u *UnifiPoller) DumpJSONPayload() (err error) {
case err != nil: case err != nil:
return err return err
case StringInSlice(u.Flag.DumpJSON, []string{"d", "device", "devices"}): 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"}): 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 "): case strings.HasPrefix(u.Flag.DumpJSON, "other "):
apiPath := strings.SplitN(u.Flag.DumpJSON, " ", 2)[1] apiPath := strings.SplitN(u.Flag.DumpJSON, " ", 2)[1]
_, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", apiPath) _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", apiPath)

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

@ -12,6 +12,7 @@ func (u *UnifiPoller) GetInfluxDB() (err error) {
if u.Influx != nil { if u.Influx != nil {
return nil return nil
} }
u.Influx, err = influxunifi.New(&influxunifi.Config{ u.Influx, err = influxunifi.New(&influxunifi.Config{
Database: u.Config.InfluxDB, Database: u.Config.InfluxDB,
User: u.Config.InfluxUser, User: u.Config.InfluxUser,
@ -22,7 +23,9 @@ func (u *UnifiPoller) GetInfluxDB() (err error) {
if err != nil { if err != nil {
return fmt.Errorf("influxdb: %v", err) return fmt.Errorf("influxdb: %v", err)
} }
u.Logf("Logging Measurements to InfluxDB at %s as user %s", u.Config.InfluxURL, u.Config.InfluxUser) u.Logf("Logging Measurements to InfluxDB at %s as user %s", u.Config.InfluxURL, u.Config.InfluxUser)
return nil return nil
} }
@ -35,16 +38,19 @@ func (u *UnifiPoller) CollectAndProcess() error {
if err := u.GetInfluxDB(); err != nil { if err := u.GetInfluxDB(); err != nil {
return err return err
} }
metrics, err := u.CollectMetrics() metrics, err := u.CollectMetrics()
if err != nil { if err != nil {
return err return err
} }
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")
return err return err
} }
u.LogInfluxReport(report) u.LogInfluxReport(report)
return nil return nil
} }
@ -52,9 +58,11 @@ func (u *UnifiPoller) CollectAndProcess() error {
// LogInfluxReport writes a log message after exporting to influxdb. // LogInfluxReport writes a log message after exporting to influxdb.
func (u *UnifiPoller) LogInfluxReport(r *influxunifi.Report) { func (u *UnifiPoller) LogInfluxReport(r *influxunifi.Report) {
idsMsg := "" idsMsg := ""
if u.Config.SaveIDS { if u.Config.SaveIDS {
idsMsg = fmt.Sprintf("IDS Events: %d, ", len(r.Metrics.IDSList)) idsMsg = fmt.Sprintf("IDS Events: %d, ", len(r.Metrics.IDSList))
} }
u.Logf("UniFi Metrics Recorded. Sites: %d, Clients: %d, "+ u.Logf("UniFi Metrics Recorded. Sites: %d, Clients: %d, "+
"UAP: %d, USG/UDM: %d, USW: %d, %sPoints: %d, Fields: %d, Errs: %d, Elapsed: %v", "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), len(r.Metrics.Sites), len(r.Metrics.Clients), len(r.Metrics.UAPs),

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),
@ -23,6 +23,7 @@ func (u *UnifiPoller) RunPrometheus() error {
LoggingFn: u.LogExportReport, LoggingFn: u.LogExportReport,
ReportErrors: true, // XXX: Does this need to be configurable? ReportErrors: true, // XXX: Does this need to be configurable?
})) }))
return http.ListenAndServe(u.Config.HTTPListen, nil) return http.ListenAndServe(u.Config.HTTPListen, nil)
} }
@ -34,8 +35,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 {
u.LogError(err, "re-authenticating") if err := u.GetUnifi(); err != nil {
u.LogErrorf("re-authenticating: %v", err)
return nil, err return nil, err
} }

View File

@ -28,7 +28,10 @@ func New() *UnifiPoller {
SaveSites: true, SaveSites: true,
HTTPListen: defaultHTTPListen, HTTPListen: defaultHTTPListen,
Namespace: appName, 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: // Parses cli flags, parses config file, parses env vars, sets up logging, then:
// - dumps a json payload OR - executes Run(). // - dumps a json payload OR - executes Run().
func (u *UnifiPoller) Start() error { func (u *UnifiPoller) Start() error {
log.SetOutput(os.Stdout)
log.SetFlags(log.LstdFlags) log.SetFlags(log.LstdFlags)
u.Flag.Parse(os.Args[1:]) 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) // 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. // 3. Start a web server and wait for Prometheus to poll the application for metrics.
func (u *UnifiPoller) Run() error { func (u *UnifiPoller) Run() error {
if err := u.GetUnifi(); err != nil { switch err := u.GetUnifi(); err {
return 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) { 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)
defer ticker.Stop()
ticker := time.NewTicker(interval)
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. if err := u.CollectAndProcess(); err != nil {
if u.Config.ReAuth { u.LogErrorf("%v", err)
u.LogDebugf("Re-authenticating to UniFi Controller")
if err := u.Unifi.Login(); err != nil { if u.Unifi != nil {
return err 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
} }

View File

@ -11,6 +11,13 @@ import (
// GetUnifi returns a UniFi controller interface. // GetUnifi returns a UniFi controller interface.
func (u *UnifiPoller) GetUnifi() (err error) { 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. // Create an authenticated session to the Unifi Controller.
u.Unifi, err = unifi.NewUnifi(&unifi.Config{ u.Unifi, err = unifi.NewUnifi(&unifi.Config{
User: u.Config.UnifiUser, User: u.Config.UnifiUser,
@ -21,8 +28,10 @@ func (u *UnifiPoller) GetUnifi() (err error) {
DebugLog: u.LogDebugf, // Log debug messages. DebugLog: u.LogDebugf, // Log debug messages.
}) })
if err != nil { if err != nil {
u.Unifi = nil
return fmt.Errorf("unifi controller: %v", err) return fmt.Errorf("unifi controller: %v", err)
} }
u.LogDebugf("Authenticated with controller successfully") u.LogDebugf("Authenticated with controller successfully")
return u.CheckSites() return u.CheckSites()
@ -34,20 +43,26 @@ func (u *UnifiPoller) CheckSites() error {
if strings.Contains(strings.ToLower(u.Config.Mode), "lambda") { if strings.Contains(strings.ToLower(u.Config.Mode), "lambda") {
return nil // Skip this in lambda mode. return nil // Skip this in lambda mode.
} }
u.LogDebugf("Checking Controller Sites List") u.LogDebugf("Checking Controller Sites List")
sites, err := u.Unifi.GetSites() sites, err := u.Unifi.GetSites()
if err != nil { if err != nil {
return err return err
} }
msg := []string{} msg := []string{}
for _, site := range sites { for _, site := range sites {
msg = append(msg, site.Name+" ("+site.Desc+")") msg = append(msg, site.Name+" ("+site.Desc+")")
} }
u.Logf("Found %d site(s) on controller: %v", len(msg), strings.Join(msg, ", ")) u.Logf("Found %d site(s) on controller: %v", len(msg), strings.Join(msg, ", "))
if StringInSlice("all", u.Config.Sites) { if StringInSlice("all", u.Config.Sites) {
u.Config.Sites = []string{"all"} u.Config.Sites = []string{"all"}
return nil return nil
} }
FIRST: FIRST:
for _, s := range u.Config.Sites { for _, s := range u.Config.Sites {
for _, site := range sites { for _, site := range sites {
@ -58,26 +73,44 @@ FIRST:
// This is fine, it may get added later. // This is fine, it may get added later.
u.LogErrorf("configured site not found on controller: %v", s) u.LogErrorf("configured site not found on controller: %v", s)
} }
return nil return nil
} }
// CollectMetrics grabs all the measurements from a UniFi controller and returns them. // CollectMetrics grabs all the measurements from a UniFi controller and returns them.
func (u *UnifiPoller) CollectMetrics() (*metrics.Metrics, error) { func (u *UnifiPoller) CollectMetrics() (*metrics.Metrics, error) {
m := &metrics.Metrics{TS: u.LastCheck} // At this point, it's the Current Check.
var err error 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. // Get the sites we care about.
m.Sites, err = u.GetFilteredSites() if m.Sites, err = u.GetFilteredSites(); err != nil {
u.LogError(err, "unifi.GetSites()") return m, fmt.Errorf("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()") return m, fmt.Errorf("unifi.GetIDS(): %v", err)
} }
// Get all the points. // Get all the points.
m.Clients, err = u.Unifi.GetClients(m.Sites) if m.Clients, err = u.Unifi.GetClients(m.Sites); err != nil {
u.LogError(err, "unifi.GetClients()") return m, fmt.Errorf("unifi.GetClients(): %v", err)
m.Devices, err = u.Unifi.GetDevices(m.Sites) }
u.LogError(err, "unifi.GetDevices()")
return m, 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. // 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 { if metrics == nil || metrics.Devices == nil || metrics.Clients == nil {
return return
} }
devices := make(map[string]string) devices := make(map[string]string)
bssdIDs := make(map[string]string) bssdIDs := make(map[string]string)
for _, r := range metrics.UAPs { for _, r := range metrics.UAPs {
devices[r.Mac] = r.Name devices[r.Mac] = r.Name
for _, v := range r.VapTable { for _, v := range r.VapTable {
bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName) bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName)
} }
} }
for _, r := range metrics.USGs { for _, r := range metrics.USGs {
devices[r.Mac] = r.Name devices[r.Mac] = r.Name
} }
for _, r := range metrics.USWs { for _, r := range metrics.USWs {
devices[r.Mac] = r.Name devices[r.Mac] = r.Name
} }
for _, r := range metrics.UDMs { for _, r := range metrics.UDMs {
devices[r.Mac] = r.Name devices[r.Mac] = r.Name
} }
// These come blank, so set them here. // These come blank, so set them here.
for i, c := range metrics.Clients { for i, c := range metrics.Clients {
metrics.Clients[i].SwName = devices[c.SwMac] 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].GwName = devices[c.GwMac]
metrics.Clients[i].RadioDescription = bssdIDs[metrics.Clients[i].Bssid] + metrics.Clients[i].RadioProto metrics.Clients[i].RadioDescription = bssdIDs[metrics.Clients[i].Bssid] + metrics.Clients[i].RadioProto
} }
if !u.Config.SaveSites { if !u.Config.SaveSites {
metrics.Sites = nil 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 // Omits requested but unconfigured sites. Grabs the full list from the
// controller and returns the sites provided in the config file. // controller and returns the sites provided in the config file.
func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) { func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) {
var i int
sites, err := u.Unifi.GetSites() sites, err := u.Unifi.GetSites()
if err != nil { if err != nil {
return nil, err return nil, err
} else if len(u.Config.Sites) < 1 || StringInSlice("all", u.Config.Sites) { } else if len(u.Config.Sites) < 1 || StringInSlice("all", u.Config.Sites) {
return sites, nil return sites, nil
} }
var i int
for _, s := range sites { for _, s := range sites {
// Only include valid sites in the request filter. // Only include valid sites in the request filter.
if StringInSlice(s.Name, u.Config.Sites) { if StringInSlice(s.Name, u.Config.Sites) {
@ -134,5 +176,6 @@ func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) {
i++ i++
} }
} }
return sites[:i], nil return sites[:i], nil
} }

View File

@ -159,6 +159,9 @@ func descUAP(ns string) *uap {
} }
func (u *promUnifi) exportUAP(r report, d *unifi.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} 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} 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) 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. // 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) { 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]} labelU := []string{"user", labels[1], labels[2]}
labelG := []string{"guest", labels[1], labels[2]} labelG := []string{"guest", labels[1], labels[2]}
r.send([]*metric{ r.send([]*metric{

View File

@ -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. // 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) { 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} 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} 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). // Shared data (all devices do this).

View File

@ -69,6 +69,9 @@ func descUSG(ns string) *usg {
} }
func (u *promUnifi) exportUSG(r report, d *unifi.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} 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} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt}
// Gateway System Data. // Gateway System Data.
@ -85,6 +88,9 @@ func (u *promUnifi) exportUSG(r report, d *unifi.USG) {
// Gateway States // Gateway States
func (u *promUnifi) exportUSGstats(r report, labels []string, gw *unifi.Gw, st unifi.SpeedtestStatus, ul unifi.Uplink) { 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]} labelLan := []string{"lan", labels[1], labels[2]}
labelWan := []string{"all", labels[1], labels[2]} labelWan := []string{"all", labels[1], labels[2]}
r.send([]*metric{ r.send([]*metric{

View File

@ -91,6 +91,9 @@ func descUSW(ns string) *usw {
} }
func (u *promUnifi) exportUSW(r report, d *unifi.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} 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} 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) u.exportUSWstats(r, labels, d.Stat.Sw)
@ -116,6 +119,9 @@ func (u *promUnifi) exportUSW(r report, d *unifi.USW) {
// Switch Stats // Switch Stats
func (u *promUnifi) exportUSWstats(r report, labels []string, sw *unifi.Sw) { func (u *promUnifi) exportUSWstats(r report, labels []string, sw *unifi.Sw) {
if sw == nil {
return
}
labelS := labels[1:] labelS := labels[1:]
r.send([]*metric{ r.send([]*metric{
{u.USW.SwRxPackets, counter, sw.RxPackets, labelS}, {u.USW.SwRxPackets, counter, sw.RxPackets, labelS},