diff --git a/.metadata.sh b/.metadata.sh index c4b30be1..7a78171d 100755 --- a/.metadata.sh +++ b/.metadata.sh @@ -11,7 +11,7 @@ HBREPO="golift/homebrew-mugs" MAINT="David Newhall II " VENDOR="Go Lift " DESC="Polls a UniFi controller, exports metrics to InfluxDB and Prometheus" -GOLANGCI_LINT_ARGS="--enable-all -D gochecknoglobals -D dupl -D lll -D funlen -D wsl -e G402 -D gochecknoinits" +GOLANGCI_LINT_ARGS="--enable-all -D gochecknoglobals -D funlen -e G402 -D gochecknoinits" # Example must exist at examples/$CONFIG_FILE.example CONFIG_FILE="up.conf" LICENSE="MIT" diff --git a/main.go b/main.go index be0fe13b..24315b5d 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,9 @@ import ( "log" "github.com/davidnewhall/unifi-poller/pkg/poller" - // Enable output plugins! + // Load input plugins! + _ "github.com/davidnewhall/unifi-poller/pkg/inputunifi" + // Load output plugins! _ "github.com/davidnewhall/unifi-poller/pkg/influxunifi" _ "github.com/davidnewhall/unifi-poller/pkg/promunifi" ) diff --git a/pkg/influxunifi/clients.go b/pkg/influxunifi/clients.go index 127ab7e2..b4c346dc 100644 --- a/pkg/influxunifi/clients.go +++ b/pkg/influxunifi/clients.go @@ -66,14 +66,7 @@ func (u *InfluxUnifi) batchClient(r report, s *unifi.Client) { "wired-tx_bytes": s.WiredTxBytes, "wired-tx_bytes-r": s.WiredTxBytesR, "wired-tx_packets": s.WiredTxPackets, - /* - "dpi_app": c.DpiStats.App.Val, - "dpi_cat": c.DpiStats.Cat.Val, - "dpi_rx_bytes": c.DpiStats.RxBytes.Val, - "dpi_rx_packets": c.DpiStats.RxPackets.Val, - "dpi_tx_bytes": c.DpiStats.TxBytes.Val, - "dpi_tx_packets": c.DpiStats.TxPackets.Val, - */ } + r.send(&metric{Table: "clients", Tags: tags, Fields: fields}) } diff --git a/pkg/influxunifi/ids.go b/pkg/influxunifi/ids.go index c7b8edba..ad0b855a 100644 --- a/pkg/influxunifi/ids.go +++ b/pkg/influxunifi/ids.go @@ -35,5 +35,6 @@ func (u *InfluxUnifi) batchIDS(r report, i *unifi.IDS) { "srcipASN": i.SrcipASN, "usgipASN": i.UsgipASN, } + r.send(&metric{Table: "intrusion_detect", Tags: tags, Fields: fields}) } diff --git a/pkg/influxunifi/metrics.go b/pkg/influxunifi/influxdb.go similarity index 97% rename from pkg/influxunifi/metrics.go rename to pkg/influxunifi/influxdb.go index e983edce..199ea594 100644 --- a/pkg/influxunifi/metrics.go +++ b/pkg/influxunifi/influxdb.go @@ -17,7 +17,7 @@ const ( defaultInterval = 30 * time.Second minimumInterval = 10 * time.Second defaultInfluxDB = "unifi" - defaultInfluxUser = "unifi" + defaultInfluxUser = "unifipoller" defaultInfluxURL = "http://127.0.0.1:8086" ) @@ -53,6 +53,7 @@ type metric struct { func init() { u := &InfluxUnifi{InfluxDB: &InfluxDB{}, LastCheck: time.Now()} + poller.NewOutput(&poller.Output{ Name: "influxdb", Config: u.InfluxDB, @@ -143,9 +144,12 @@ func (u *InfluxUnifi) setConfigDefaults() { func (u *InfluxUnifi) ReportMetrics(m *poller.Metrics) (*Report, error) { r := &Report{Metrics: m, ch: make(chan *metric), Start: time.Now()} defer close(r.ch) - // Make a new Influx Points Batcher. + var err error + + // Make a new Influx Points Batcher. r.bp, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.Config.DB}) + if err != nil { return nil, fmt.Errorf("influx.NewBatchPoints: %v", err) } @@ -159,7 +163,9 @@ func (u *InfluxUnifi) ReportMetrics(m *poller.Metrics) (*Report, error) { if err = u.influx.Write(r.bp); err != nil { return nil, fmt.Errorf("influxdb.Write(points): %v", err) } + r.Elapsed = time.Since(r.Start) + return r, nil } @@ -172,6 +178,7 @@ func (u *InfluxUnifi) collect(r report, ch chan *metric) { } else { r.batch(m, pt) } + r.done() } } @@ -182,24 +189,28 @@ func (u *InfluxUnifi) loopPoints(r report) { m := r.metrics() r.add() + r.add() + r.add() + go func() { defer r.done() + for _, s := range m.Sites { u.batchSite(r, s) } }() - r.add() go func() { defer r.done() + for _, s := range m.Clients { u.batchClient(r, s) } }() - r.add() go func() { defer r.done() + for _, s := range m.IDSList { u.batchIDS(r, s) } @@ -209,33 +220,44 @@ func (u *InfluxUnifi) loopPoints(r report) { return } + u.loopDevicePoints(r) +} + +func (u *InfluxUnifi) loopDevicePoints(r report) { + m := r.metrics() + r.add() + r.add() + r.add() + r.add() + go func() { defer r.done() + for _, s := range m.UAPs { u.batchUAP(r, s) } }() - r.add() go func() { defer r.done() + for _, s := range m.USGs { u.batchUSG(r, s) } }() - r.add() go func() { defer r.done() + for _, s := range m.USWs { u.batchUSW(r, s) } }() - r.add() go func() { defer r.done() + for _, s := range m.UDMs { u.batchUDM(r, s) } diff --git a/pkg/influxunifi/site.go b/pkg/influxunifi/site.go index 243d2acc..30e2ce37 100644 --- a/pkg/influxunifi/site.go +++ b/pkg/influxunifi/site.go @@ -51,6 +51,7 @@ func (u *InfluxUnifi) batchSite(r report, s *unifi.Site) { "remote_user_tx_packets": h.RemoteUserTxPackets.Val, "num_new_alarms": s.NumNewAlarms.Val, } + r.send(&metric{Table: "subsystems", Tags: tags, Fields: fields}) } } diff --git a/pkg/influxunifi/uap.go b/pkg/influxunifi/uap.go index 46c25c32..f47a11fc 100644 --- a/pkg/influxunifi/uap.go +++ b/pkg/influxunifi/uap.go @@ -10,6 +10,7 @@ func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) { if !s.Adopted.Val || s.Locating.Val { return } + tags := map[string]string{ "mac": s.Mac, "site_name": s.SiteName, @@ -30,6 +31,7 @@ func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) { fields["user-num_sta"] = int(s.UserNumSta.Val) fields["guest-num_sta"] = int(s.GuestNumSta.Val) fields["num_sta"] = s.NumSta.Val + r.send(&metric{Table: "uap", Tags: tags, Fields: fields}) u.processRadTable(r, tags, s.RadioTable, s.RadioTableStats) u.processVAPTable(r, tags, s.VapTable) @@ -39,6 +41,7 @@ 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, @@ -135,6 +138,7 @@ func (u *InfluxUnifi) processVAPTable(r report, t map[string]string, vt unifi.Va "wifi_tx_latency_mov_total": s.WifiTxLatencyMov.Total.Val, "wifi_tx_latency_mov_cuont": s.WifiTxLatencyMov.TotalCount.Val, } + r.send(&metric{Table: "uap_vaps", Tags: tags, Fields: fields}) } } @@ -155,6 +159,7 @@ func (u *InfluxUnifi) processRadTable(r report, t map[string]string, rt unifi.Ra "nss": p.Nss.Val, "radio_caps": p.RadioCaps.Val, } + for _, t := range rts { if t.Name == p.Name { fields["ast_be_xmit"] = t.AstBeXmit.Val @@ -171,9 +176,11 @@ func (u *InfluxUnifi) processRadTable(r report, t map[string]string, rt unifi.Ra fields["tx_power"] = t.TxPower.Val fields["tx_retries"] = t.TxRetries.Val fields["user-num_sta"] = t.UserNumSta.Val + break } } + r.send(&metric{Table: "uap_radios", Tags: tags, Fields: fields}) } } diff --git a/pkg/influxunifi/udm.go b/pkg/influxunifi/udm.go index af42e5d0..20cea055 100644 --- a/pkg/influxunifi/udm.go +++ b/pkg/influxunifi/udm.go @@ -4,14 +4,16 @@ import ( "golift.io/unifi" ) -// Combines concatenates N maps. This will delete things if not used with caution. +// Combine concatenates N maps. This will delete things if not used with caution. func Combine(in ...map[string]interface{}) map[string]interface{} { out := make(map[string]interface{}) + for i := range in { for k := range in[i] { out[k] = in[i][k] } } + return out } @@ -36,6 +38,7 @@ func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) { if !s.Adopted.Val || s.Locating.Val { return } + tags := map[string]string{ "mac": s.Mac, "site_name": s.SiteName, @@ -65,6 +68,7 @@ func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) { "num_mobile": s.NumMobile.Val, }, ) + r.send(&metric{Table: "usg", Tags: tags, Fields: fields}) u.batchNetTable(r, tags, s.NetworkTable) u.batchUSGwans(r, tags, s.Wan1, s.Wan2) @@ -90,13 +94,14 @@ func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) { "uptime": s.Uptime.Val, "state": s.State.Val, }) + r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) u.batchPortTable(r, tags, s.PortTable) if s.Stat.Ap == nil { - return - // we're done now. the following code process UDM (non-pro) UAP data. + return // we're done now. the following code process UDM (non-pro) UAP data. } + tags = map[string]string{ "mac": s.Mac, "site_name": s.SiteName, @@ -117,6 +122,7 @@ func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) { fields["user-num_sta"] = int(s.UserNumSta.Val) fields["guest-num_sta"] = int(s.GuestNumSta.Val) fields["num_sta"] = s.NumSta.Val + r.send(&metric{Table: "uap", Tags: tags, Fields: fields}) u.processRadTable(r, tags, *s.RadioTable, *s.RadioTableStats) u.processVAPTable(r, tags, *s.VapTable) diff --git a/pkg/influxunifi/usg.go b/pkg/influxunifi/usg.go index 221e0e40..36c9cdbd 100644 --- a/pkg/influxunifi/usg.go +++ b/pkg/influxunifi/usg.go @@ -10,6 +10,7 @@ func (u *InfluxUnifi) batchUSG(r report, s *unifi.USG) { if !s.Adopted.Val || s.Locating.Val { return } + tags := map[string]string{ "mac": s.Mac, "site_name": s.SiteName, @@ -39,44 +40,17 @@ func (u *InfluxUnifi) batchUSG(r report, s *unifi.USG) { "num_mobile": s.NumMobile.Val, }, ) + r.send(&metric{Table: "usg", Tags: tags, Fields: fields}) u.batchNetTable(r, tags, s.NetworkTable) u.batchUSGwans(r, tags, s.Wan1, s.Wan2) - - /* - for _, p := range s.PortTable { - t := map[string]string{ - "device_name": tags["name"], - "site_name": tags["site_name"], - "name": p.Name, - "ifname": p.Ifname, - "ip": p.IP, - "mac": p.Mac, - "up": p.Up.Txt, - "speed": p.Speed.Txt, - "full_duplex": p.FullDuplex.Txt, - "enable": p.Enable.Txt, - } - f := map[string]interface{}{ - "rx_bytes": p.RxBytes.Val, - "rx_dropped": p.RxDropped.Val, - "rx_errors": p.RxErrors.Val, - "rx_packets": p.RxBytes.Val, - "tx_bytes": p.TxBytes.Val, - "tx_dropped": p.TxDropped.Val, - "tx_errors": p.TxErrors.Val, - "tx_packets": p.TxPackets.Val, - "rx_multicast": p.RxMulticast.Val, - "dns_servers": strings.Join(p.DNS, ","), - } - r.send(&metric{Table: "usg_ports", Tags: t, Fields: f}) - } - */ } + 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, @@ -92,11 +66,13 @@ func (u *InfluxUnifi) batchUSGstat(ss unifi.SpeedtestStatus, gw *unifi.Gw, ul un "lan-rx_dropped": gw.LanRxDropped.Val, } } + func (u *InfluxUnifi) batchUSGwans(r report, tags map[string]string, wans ...unifi.Wan) { for _, wan := range wans { if !wan.Up.Val { continue } + tags := map[string]string{ "device_name": tags["name"], "site_name": tags["site_name"], @@ -129,6 +105,7 @@ func (u *InfluxUnifi) batchUSGwans(r report, tags map[string]string, wans ...uni "tx_broadcast": wan.TxBroadcast.Val, "tx_multicast": wan.TxMulticast.Val, } + r.send(&metric{Table: "usg_wan_ports", Tags: tags, Fields: fields}) } } @@ -154,6 +131,7 @@ func (u *InfluxUnifi) batchNetTable(r report, tags map[string]string, nt unifi.N "tx_bytes": p.TxBytes.Val, "tx_packets": p.TxPackets.Val, } + r.send(&metric{Table: "usg_networks", Tags: tags, Fields: fields}) } } diff --git a/pkg/influxunifi/usw.go b/pkg/influxunifi/usw.go index 7bc31c37..0a91a506 100644 --- a/pkg/influxunifi/usw.go +++ b/pkg/influxunifi/usw.go @@ -36,6 +36,7 @@ func (u *InfluxUnifi) batchUSW(r report, s *unifi.USW) { "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) } @@ -44,6 +45,7 @@ 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, @@ -59,11 +61,13 @@ func (u *InfluxUnifi) batchUSWstat(sw *unifi.Sw) map[string]interface{} { "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 { continue // only record UP ports. } + tags := map[string]string{ "site_name": t["site_name"], "device_name": t["name"], @@ -96,11 +100,13 @@ func (u *InfluxUnifi) batchPortTable(r report, t map[string]string, pt []unifi.P "tx_multicast": p.TxMulticast.Val, "tx_packets": p.TxPackets.Val, } + if p.PoeEnable.Val && p.PortPoe.Val { fields["poe_current"] = p.PoeCurrent.Val fields["poe_power"] = p.PoePower.Val fields["poe_voltage"] = p.PoeVoltage.Val } + r.send(&metric{Table: "usw_ports", Tags: tags, Fields: fields}) } } diff --git a/pkg/inputunifi/collector.go b/pkg/inputunifi/collector.go new file mode 100644 index 00000000..20aeafac --- /dev/null +++ b/pkg/inputunifi/collector.go @@ -0,0 +1,138 @@ +package inputunifi + +import ( + "fmt" + "time" + + "github.com/davidnewhall/unifi-poller/pkg/poller" + "golift.io/unifi" +) + +func (u *InputUnifi) isNill(c Controller) bool { + u.Config.RLock() + defer u.Config.RUnlock() + + return c.Unifi == nil +} + +func (u *InputUnifi) collectController(c Controller) (*poller.Metrics, error) { + if u.isNill(c) { + u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) + + if err := u.getUnifi(c); err != nil { + return nil, fmt.Errorf("re-authenticating to %s: %v", c.Name, err) + } + } + + m, err := u.pollController(c) + if err == nil { + return m, nil + } + + return u.pollController(c) +} + +func (u *InputUnifi) pollController(c Controller) (*poller.Metrics, error) { + var err error + + u.Config.RLock() + defer u.Config.RUnlock() + + m := &poller.Metrics{TS: time.Now()} // At this point, it's the Current Check. + + // Get the sites we care about. + if m.Sites, err = u.getFilteredSites(c); err != nil { + return m, fmt.Errorf("unifi.GetSites(%v): %v", c.URL, err) + } + + if c.SaveIDS { + m.IDSList, err = c.Unifi.GetIDS(m.Sites, time.Now().Add(2*time.Minute), time.Now()) + if err != nil { + return m, fmt.Errorf("unifi.GetIDS(%v): %v", c.URL, err) + } + } + + // Get all the points. + if m.Clients, err = c.Unifi.GetClients(m.Sites); err != nil { + return m, fmt.Errorf("unifi.GetClients(%v): %v", c.URL, err) + } + + if m.Devices, err = c.Unifi.GetDevices(m.Sites); err != nil { + return m, fmt.Errorf("unifi.GetDevices(%v): %v", c.URL, err) + } + + return u.augmentMetrics(c, m), nil +} + +// augmentMetrics is our middleware layer between collecting metrics and writing them. +// This is where we can manipuate the returned data or make arbitrary decisions. +// This function currently adds parent device names to client metrics. +func (u *InputUnifi) augmentMetrics(c Controller, metrics *poller.Metrics) *poller.Metrics { + if metrics == nil || metrics.Devices == nil || metrics.Clients == nil { + return metrics + } + + 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] + metrics.Clients[i].ApName = devices[c.ApMac] + metrics.Clients[i].GwName = devices[c.GwMac] + metrics.Clients[i].RadioDescription = bssdIDs[metrics.Clients[i].Bssid] + metrics.Clients[i].RadioProto + } + + if !c.SaveSites { + metrics.Sites = nil + } + + return metrics +} + +// getFilteredSites returns a list of sites to fetch data for. +// Omits requested but unconfigured sites. Grabs the full list from the +// controller and returns the sites provided in the config file. +func (u *InputUnifi) getFilteredSites(c Controller) (unifi.Sites, error) { + u.Config.RLock() + defer u.Config.RUnlock() + + sites, err := c.Unifi.GetSites() + if err != nil { + return nil, err + } else if len(c.Sites) < 1 || poller.StringInSlice("all", c.Sites) { + return sites, nil + } + + var i int + + for _, s := range sites { + // Only include valid sites in the request filter. + if poller.StringInSlice(s.Name, c.Sites) { + sites[i] = s + i++ + } + } + + return sites[:i], nil +} diff --git a/pkg/inputunifi/input.go b/pkg/inputunifi/input.go new file mode 100644 index 00000000..f797d35b --- /dev/null +++ b/pkg/inputunifi/input.go @@ -0,0 +1,78 @@ +// Package inputunifi implements the poller.Input interface and bridges the gap between +// metrics from the unifi library, and the augments required to pump them into unifi-poller. +package inputunifi + +import ( + "fmt" + + "sync" + + "github.com/davidnewhall/unifi-poller/pkg/poller" + "golift.io/unifi" +) + +// InputUnifi contains the running data. +type InputUnifi struct { + Config Config `json:"unifi" toml:"unifi" xml:"unifi" yaml:"unifi"` + poller.Logger +} + +// Controller represents the configuration for a UniFi Controller. +// Each polled controller may have its own configuration. +type Controller struct { + VerifySSL bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` + SaveIDS bool `json:"save_ids" toml:"save_ids" xml:"save_ids" yaml:"save_ids"` + SaveSites bool `json:"save_sites" toml:"save_sites" xml:"save_sites" yaml:"save_sites"` + Name string `json:"name" toml:"name" xml:"name,attr" yaml:"name"` + User string `json:"user" toml:"user" xml:"user" yaml:"user"` + Pass string `json:"pass" toml:"pass" xml:"pass" yaml:"pass"` + URL string `json:"url" toml:"url" xml:"url" yaml:"url"` + Sites []string `json:"sites,omitempty" toml:"sites,omitempty" xml:"sites" yaml:"sites"` + Unifi *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"` +} + +// Config contains our configuration data +type Config struct { + sync.RWMutex // locks the Unifi struct member when re-authing to unifi. + Disable bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"` + Controllers []Controller `json:"controller" toml:"controller" xml:"controller" yaml:"controller"` +} + +func init() { + u := &InputUnifi{} + + poller.NewInput(&poller.InputPlugin{ + Input: u, // this library implements poller.Input interface for Metrics(). + Config: u, // Defines our config data interface. + }) +} + +// getUnifi (re-)authenticates to a unifi controller. +func (u *InputUnifi) getUnifi(c Controller) error { + var err error + + u.Config.Lock() + defer u.Config.Unlock() + + if c.Unifi != nil { + c.Unifi.CloseIdleConnections() + } + + // Create an authenticated session to the Unifi Controller. + c.Unifi, err = unifi.NewUnifi(&unifi.Config{ + User: c.User, + Pass: c.Pass, + URL: c.URL, + VerifySSL: c.VerifySSL, + ErrorLog: u.LogErrorf, // Log all errors. + DebugLog: u.LogDebugf, // Log debug messages. + }) + if err != nil { + c.Unifi = nil + return fmt.Errorf("unifi controller: %v", err) + } + + u.LogDebugf("Authenticated with controller successfully, %s", c.URL) + + return nil +} diff --git a/pkg/inputunifi/interface.go b/pkg/inputunifi/interface.go new file mode 100644 index 00000000..a2ef9765 --- /dev/null +++ b/pkg/inputunifi/interface.go @@ -0,0 +1,122 @@ +package inputunifi + +import ( + "fmt" + "strings" + + "github.com/davidnewhall/unifi-poller/pkg/poller" + "golift.io/unifi" +) + +// Metrics grabs all the measurements from a UniFi controller and returns them. +func (u *InputUnifi) Metrics() (*poller.Metrics, error) { + errs := []string{} + metrics := &poller.Metrics{} + + for _, c := range u.Config.Controllers { + m, err := u.collectController(c) + if err != nil { + errs = append(errs, err.Error()) + } + + if m == nil { + continue + } + + metrics.Sites = append(metrics.Sites, m.Sites...) + metrics.Clients = append(metrics.Clients, m.Clients...) + metrics.IDSList = append(metrics.IDSList, m.IDSList...) + + if m.Devices == nil { + continue + } + + if metrics.Devices == nil { + metrics.Devices = &unifi.Devices{} + } + + metrics.UAPs = append(metrics.UAPs, m.UAPs...) + metrics.USGs = append(metrics.USGs, m.USGs...) + metrics.USWs = append(metrics.USWs, m.USWs...) + metrics.UDMs = append(metrics.UDMs, m.UDMs...) + } + + if len(errs) > 0 { + return metrics, fmt.Errorf(strings.Join(errs, ", ")) + } + + return metrics, nil +} + +// Initialize gets called one time when starting up. +// Satisfies poller.Input interface. +func (u *InputUnifi) Initialize(l poller.Logger) error { + if u.Config.Disable { + l.Logf("unifi input disabled") + return nil + } + + if len(u.Config.Controllers) < 1 { + return fmt.Errorf("no unifi controllers defined for unifi input") + } + + u.Logger = l + + for i, c := range u.Config.Controllers { + if c.Name == "" { + u.Config.Controllers[i].Name = c.URL + } + + switch err := u.getUnifi(c); err { + case nil: + if err := u.checkSites(c); err != nil { + u.LogErrorf("checking sites on %s: %v", c.Name, err) + } + + u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v", + c.URL, c.Unifi.ServerVersion, c.User, c.Sites) + default: + u.LogErrorf("Controller Auth or Connection failed, but continuing to retry! %s: %v", c.Name, err) + } + } + + return nil +} + +// checkSites makes sure the list of provided sites exists on the controller. +// This only runs once during initialization. +func (u *InputUnifi) checkSites(c Controller) error { + u.Config.RLock() + defer u.Config.RUnlock() + u.LogDebugf("Checking Controller Sites List") + + sites, err := c.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 poller.StringInSlice("all", c.Sites) { + c.Sites = []string{"all"} + return nil + } + +FIRST: + for _, s := range c.Sites { + for _, site := range sites { + if s == site.Name { + continue FIRST + } + } + return fmt.Errorf("configured site not found on controller: %v", s) + } + + return nil +} diff --git a/pkg/poller/build_macos.go b/pkg/poller/build_macos.go index 1ab32471..b3f37dbf 100644 --- a/pkg/poller/build_macos.go +++ b/pkg/poller/build_macos.go @@ -2,5 +2,5 @@ package poller -// DefaultConfFile is where to find config is --config is not prvided. +// DefaultConfFile is where to find config if --config is not prvided. const DefaultConfFile = "/usr/local/etc/unifi-poller/up.conf" diff --git a/pkg/poller/build_unix.go b/pkg/poller/build_unix.go index c1001ac9..c1f525a9 100644 --- a/pkg/poller/build_unix.go +++ b/pkg/poller/build_unix.go @@ -2,5 +2,5 @@ package poller -// DefaultConfFile is where to find config is --config is not prvided. +// DefaultConfFile is where to find config if --config is not prvided. const DefaultConfFile = "/etc/unifi-poller/up.conf" diff --git a/pkg/poller/build_windows.go b/pkg/poller/build_windows.go index 5c31504f..a74c76a8 100644 --- a/pkg/poller/build_windows.go +++ b/pkg/poller/build_windows.go @@ -2,5 +2,5 @@ package poller -// DefaultConfFile is where to find config is --config is not prvided. +// DefaultConfFile is where to find config if --config is not prvided. const DefaultConfFile = `C:\ProgramData\unifi-poller\up.conf` diff --git a/pkg/poller/config.go b/pkg/poller/config.go index e77db20b..03e69b81 100644 --- a/pkg/poller/config.go +++ b/pkg/poller/config.go @@ -9,7 +9,6 @@ package poller */ import ( - "sync" "time" "github.com/spf13/pflag" @@ -17,23 +16,17 @@ import ( "golift.io/unifi" ) -// App defaults in case they're missing from the config. const ( // AppName is the name of the application. - AppName = "unifi-poller" - defaultUnifiUser = "influx" - defaultUnifiURL = "https://127.0.0.1:8443" + AppName = "unifi-poller" + // ENVConfigPrefix is the prefix appended to an env variable tag name. + ENVConfigPrefix = "UP" ) -// ENVConfigPrefix is the prefix appended to an env variable tag -// name before retrieving the value from the OS. -const ENVConfigPrefix = "UP" - // UnifiPoller contains the application startup data, and auth info for UniFi & Influx. type UnifiPoller struct { - Flags *Flags - Config *Config - sync.Mutex // locks the Unifi struct member when re-authing to unifi. + Flags *Flags + *Config } // Flags represents the CLI args available and their settings. @@ -53,26 +46,9 @@ type Metrics struct { *unifi.Devices } -// Controller represents the configuration for a UniFi Controller. -// Each polled controller may have its own configuration. -type Controller struct { - VerifySSL bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` - SaveIDS bool `json:"save_ids" toml:"save_ids" xml:"save_ids" yaml:"save_ids"` - SaveSites bool `json:"save_sites,omitempty" toml:"save_sites,omitempty" xml:"save_sites" yaml:"save_sites"` - Name string `json:"name" toml:"name" xml:"name,attr" yaml:"name"` - User string `json:"user,omitempty" toml:"user,omitempty" xml:"user" yaml:"user"` - Pass string `json:"pass,omitempty" toml:"pass,omitempty" xml:"pass" yaml:"pass"` - URL string `json:"url,omitempty" toml:"url,omitempty" xml:"url" yaml:"url"` - Sites []string `json:"sites,omitempty" toml:"sites,omitempty" xml:"sites" yaml:"sites"` - Unifi *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"` -} - -// Config represents the data needed to poll a controller and report to influxdb. -// This is all of the data stored in the config file. -// Any with explicit defaults have omitempty on json and toml tags. +// Config represents the core library input data. type Config struct { - Poller `json:"poller" toml:"poller" xml:"poller" yaml:"poller"` - Controllers []Controller `json:"controller,omitempty" toml:"controller,omitempty" xml:"controller" yaml:"controller"` + Poller `json:"poller" toml:"poller" xml:"poller" yaml:"poller"` } // Poller is the global config values. @@ -83,31 +59,43 @@ type Poller struct { // ParseConfigs parses the poller config and the config for each registered output plugin. func (u *UnifiPoller) ParseConfigs() error { - // Parse config file. - if err := config.ParseFile(u.Config, u.Flags.ConfigFile); err != nil { - u.Flags.Usage() - return err - } - - // Update Config with ENV variable overrides. - if _, err := config.ParseENV(u.Config, ENVConfigPrefix); err != nil { + // Parse core config. + if err := u.ParseInterface(u.Config); err != nil { return err } + // Parse output plugin configs. outputSync.Lock() defer outputSync.Unlock() for _, o := range outputs { - // Parse config file for each output plugin. - if err := config.ParseFile(o.Config, u.Flags.ConfigFile); err != nil { + if err := u.ParseInterface(o.Config); err != nil { return err } + } - // Update Config for each output with ENV variable overrides. - if _, err := config.ParseENV(o.Config, ENVConfigPrefix); err != nil { + // Parse input plugin configs. + inputSync.Lock() + defer inputSync.Unlock() + + for _, i := range inputs { + if err := u.ParseInterface(i.Config); err != nil { return err } } return nil } + +// ParseInterface parses the config file and environment variables into the provided interface. +func (u *UnifiPoller) ParseInterface(i interface{}) error { + // Parse config file into provided interface. + if err := config.ParseFile(i, u.Flags.ConfigFile); err != nil { + return err + } + + // Parse environment variables into provided interface. + _, err := config.ParseENV(i, ENVConfigPrefix) + + return err +} diff --git a/pkg/poller/dumper.go b/pkg/poller/dumper.go index 5892488b..18abe901 100644 --- a/pkg/poller/dumper.go +++ b/pkg/poller/dumper.go @@ -1,55 +1,58 @@ package poller import ( - "fmt" - "os" "strings" - - "golift.io/unifi" ) // DumpJSONPayload prints raw json from the UniFi Controller. // This only works with controller 0 (first one) in the config. func (u *UnifiPoller) DumpJSONPayload() (err error) { - u.Config.Quiet = true - config := u.Config.Controllers[0] - - config.Unifi, err = unifi.NewUnifi(&unifi.Config{ - User: config.User, - Pass: config.Pass, - URL: config.URL, - VerifySSL: config.VerifySSL, - }) - if err != nil { - return err + if true { + return nil } + /* + u.Config.Quiet = true + config := u.Config.Controllers[0] - fmt.Fprintf(os.Stderr, "[INFO] Authenticated to UniFi Controller @ %v as user %v", config.URL, config.User) + config.Unifi, err = unifi.NewUnifi(&unifi.Config{ + User: config.User, + Pass: config.Pass, + URL: config.URL, + VerifySSL: config.VerifySSL, + }) + if err != nil { + return err + } - if err := u.CheckSites(config); err != nil { - return err - } + fmt.Fprintf(os.Stderr, "[INFO] Authenticated to UniFi Controller @ %v as user %v", config.URL, config.User) - config.Unifi.ErrorLog = func(m string, v ...interface{}) { - fmt.Fprintf(os.Stderr, "[ERROR] "+m, v...) - } // Log all errors to stderr. + if err := u.CheckSites(config); err != nil { + return err + } - switch sites, err := u.GetFilteredSites(config); { - case err != nil: - return err - case StringInSlice(u.Flags.DumpJSON, []string{"d", "device", "devices"}): - return u.dumpSitesJSON(config, unifi.APIDevicePath, "Devices", sites) - case StringInSlice(u.Flags.DumpJSON, []string{"client", "clients", "c"}): - return u.dumpSitesJSON(config, unifi.APIClientPath, "Clients", sites) - case strings.HasPrefix(u.Flags.DumpJSON, "other "): - apiPath := strings.SplitN(u.Flags.DumpJSON, " ", 2)[1] - _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", apiPath) - return u.PrintRawAPIJSON(config, apiPath) - default: - return fmt.Errorf("must provide filter: devices, clients, other") - } + config.Unifi.ErrorLog = func(m string, v ...interface{}) { + fmt.Fprintf(os.Stderr, "[ERROR] "+m, v...) + } // Log all errors to stderr. + + switch sites, err := u.GetFilteredSites(config); { + case err != nil: + return err + case StringInSlice(u.Flags.DumpJSON, []string{"d", "device", "devices"}): + return u.dumpSitesJSON(config, unifi.APIDevicePath, "Devices", sites) + case StringInSlice(u.Flags.DumpJSON, []string{"client", "clients", "c"}): + return u.dumpSitesJSON(config, unifi.APIClientPath, "Clients", sites) + case strings.HasPrefix(u.Flags.DumpJSON, "other "): + apiPath := strings.SplitN(u.Flags.DumpJSON, " ", 2)[1] + _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", apiPath) + return u.PrintRawAPIJSON(config, apiPath) + default: + return fmt.Errorf("must provide filter: devices, clients, other") + } + */ + return nil } +/* func (u *UnifiPoller) dumpSitesJSON(c Controller, path, name string, sites unifi.Sites) error { for _, s := range sites { apiPath := fmt.Sprintf(path, s.Name) @@ -68,3 +71,15 @@ func (u *UnifiPoller) PrintRawAPIJSON(c Controller, apiPath string) error { fmt.Println(string(body)) return err } +*/ + +// StringInSlice returns true if a string is in a slice. +func StringInSlice(str string, slice []string) bool { + for _, s := range slice { + if strings.EqualFold(s, str) { + return true + } + } + + return false +} diff --git a/pkg/poller/inputs.go b/pkg/poller/inputs.go new file mode 100644 index 00000000..c2140b01 --- /dev/null +++ b/pkg/poller/inputs.go @@ -0,0 +1,96 @@ +package poller + +import ( + "fmt" + "strings" + "sync" + + "golift.io/unifi" +) + +var ( + inputs []*InputPlugin + inputSync sync.Mutex +) + +// Input plugins must implement this interface. +type Input interface { + Initialize(Logger) error // Called once on startup to initialize the plugin. + Metrics() (*Metrics, error) // Called every time new metrics are requested. +} + +// InputPlugin describes an input plugin's consumable interface. +type InputPlugin struct { + Config interface{} // Each config is passed into an unmarshaller later. + Input +} + +// NewInput creates a metric input. This should be called by input plugins +// init() functions. +func NewInput(i *InputPlugin) { + inputSync.Lock() + defer inputSync.Unlock() + + if i == nil || i.Input == nil { + panic("nil output or method passed to poller.NewOutput") + } + + inputs = append(inputs, i) +} + +// InitializeInputs runs the passed-in initializer method for each input plugin. +func (u *UnifiPoller) InitializeInputs() error { + inputSync.Lock() + defer inputSync.Unlock() + + for _, input := range inputs { + // This must return, or the app locks up here. + if err := input.Initialize(u); err != nil { + return err + } + } + + return nil +} + +// Metrics aggregates all the measurements from all configured inputs and returns them. +func (u *UnifiPoller) Metrics() (*Metrics, error) { + errs := []string{} + metrics := &Metrics{} + + for _, input := range inputs { + m, err := input.Metrics() + if err != nil { + errs = append(errs, err.Error()) + } + + if m == nil { + continue + } + + metrics.Sites = append(metrics.Sites, m.Sites...) + metrics.Clients = append(metrics.Clients, m.Clients...) + metrics.IDSList = append(metrics.IDSList, m.IDSList...) + + if m.Devices == nil { + continue + } + + if metrics.Devices == nil { + metrics.Devices = &unifi.Devices{} + } + + metrics.UAPs = append(metrics.UAPs, m.UAPs...) + metrics.USGs = append(metrics.USGs, m.USGs...) + metrics.USWs = append(metrics.USWs, m.USWs...) + metrics.UDMs = append(metrics.UDMs, m.UDMs...) + } + + var err error + + if len(errs) > 0 { + err = fmt.Errorf(strings.Join(errs, ", ")) + } + + return metrics, err +} diff --git a/pkg/poller/helpers.go b/pkg/poller/logger.go similarity index 75% rename from pkg/poller/helpers.go rename to pkg/poller/logger.go index 92acd223..b498a9b5 100644 --- a/pkg/poller/helpers.go +++ b/pkg/poller/logger.go @@ -3,19 +3,15 @@ package poller import ( "fmt" "log" - "strings" ) const callDepth = 2 -// StringInSlice returns true if a string is in a slice. -func StringInSlice(str string, slice []string) bool { - for _, s := range slice { - if strings.EqualFold(s, str) { - return true - } - } - return false +// Logger is passed into input packages so they may write logs. +type Logger interface { + Logf(m string, v ...interface{}) + LogErrorf(m string, v ...interface{}) + LogDebugf(m string, v ...interface{}) } // Logf prints a log entry if quiet is false. diff --git a/pkg/poller/outputs.go b/pkg/poller/outputs.go index aaa9a47d..3bbcb72d 100644 --- a/pkg/poller/outputs.go +++ b/pkg/poller/outputs.go @@ -14,9 +14,7 @@ var ( // Output packages must implement this interface. type Collect interface { Metrics() (*Metrics, error) - Logf(m string, v ...interface{}) - LogErrorf(m string, v ...interface{}) - LogDebugf(m string, v ...interface{}) + Logger } // Output defines the output data for a metric exporter like influx or prometheus. @@ -49,6 +47,7 @@ func (u *UnifiPoller) InitializeOutputs() error { for _, o := range outputs { count++ + go func(o *Output) { v <- o.Method(u) }(o) diff --git a/pkg/poller/start.go b/pkg/poller/start.go index cd293b2f..9f6bedc8 100644 --- a/pkg/poller/start.go +++ b/pkg/poller/start.go @@ -37,25 +37,6 @@ func (u *UnifiPoller) Start() error { return err } - if len(u.Config.Controllers) < 1 { - u.Config.Controllers = []Controller{{ - Sites: []string{"all"}, - User: defaultUnifiUser, - Pass: "", - URL: defaultUnifiURL, - SaveSites: true, - }} - } - - if u.Flags.DumpJSON != "" { - return u.DumpJSONPayload() - } - - if u.Config.Debug { - log.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate) - u.LogDebugf("Debug Logging Enabled") - } - return u.Run() } @@ -79,20 +60,19 @@ func (f *Flags) 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 u.Flags.DumpJSON != "" { + return u.DumpJSONPayload() + } + + if u.Config.Debug { + log.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate) + u.LogDebugf("Debug Logging Enabled") + } + log.Printf("[INFO] UniFi Poller v%v Starting Up! PID: %d", version.Version, os.Getpid()) - for i, c := range u.Config.Controllers { - if c.Name == "" { - u.Config.Controllers[i].Name = c.URL - } - - switch err := u.GetUnifi(c); err { - case nil: - u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v", - c.URL, c.Unifi.ServerVersion, c.User, c.Sites) - default: - u.LogErrorf("Controller Auth or Connection failed, but continuing to retry! %s: %v", c.Name, err) - } + if err := u.InitializeInputs(); err != nil { + return err } return u.InitializeOutputs() diff --git a/pkg/poller/unifi.go b/pkg/poller/unifi.go deleted file mode 100644 index 516310a3..00000000 --- a/pkg/poller/unifi.go +++ /dev/null @@ -1,241 +0,0 @@ -package poller - -import ( - "fmt" - "strings" - "time" - - "golift.io/unifi" -) - -// GetUnifi returns a UniFi controller interface. -func (u *UnifiPoller) GetUnifi(c Controller) error { - var err error - - u.Lock() - defer u.Unlock() - - if c.Unifi != nil { - c.Unifi.CloseIdleConnections() - } - // Create an authenticated session to the Unifi Controller. - c.Unifi, err = unifi.NewUnifi(&unifi.Config{ - User: c.User, - Pass: c.Pass, - URL: c.URL, - VerifySSL: c.VerifySSL, - ErrorLog: u.LogErrorf, // Log all errors. - DebugLog: u.LogDebugf, // Log debug messages. - }) - - if err != nil { - c.Unifi = nil - return fmt.Errorf("unifi controller: %v", err) - } - - u.LogDebugf("Authenticated with controller successfully, %s", c.URL) - - return u.CheckSites(c) -} - -// CheckSites makes sure the list of provided sites exists on the controller. -// This does not run in Lambda (run-once) mode. -func (u *UnifiPoller) CheckSites(c Controller) error { - u.LogDebugf("Checking Controller Sites List") - - sites, err := c.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", c.Sites) { - c.Sites = []string{"all"} - return nil - } - -FIRST: - for _, s := range c.Sites { - for _, site := range sites { - if s == site.Name { - continue FIRST - } - } - return fmt.Errorf("configured site not found on controller: %v", s) - } - - return nil -} - -// Metrics grabs all the measurements from a UniFi controller and returns them. -func (u *UnifiPoller) Metrics() (*Metrics, error) { - errs := []string{} - metrics := &Metrics{} - - for _, c := range u.Config.Controllers { - m, err := u.checkAndPollController(c) - if err != nil { - errs = append(errs, err.Error()) - } - - if m == nil { - continue - } - - metrics.Sites = append(metrics.Sites, m.Sites...) - metrics.Clients = append(metrics.Clients, m.Clients...) - metrics.IDSList = append(metrics.IDSList, m.IDSList...) - - if m.Devices == nil { - continue - } - - if metrics.Devices == nil { - metrics.Devices = &unifi.Devices{} - } - - metrics.UAPs = append(metrics.UAPs, m.UAPs...) - metrics.USGs = append(metrics.USGs, m.USGs...) - metrics.USWs = append(metrics.USWs, m.USWs...) - metrics.UDMs = append(metrics.UDMs, m.UDMs...) - } - - var err error - - if len(errs) > 0 { - err = fmt.Errorf(strings.Join(errs, ", ")) - } - - return metrics, err -} - -func (u *UnifiPoller) checkAndPollController(c Controller) (*Metrics, error) { - if c.Unifi == nil { - u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) - - if err := u.GetUnifi(c); err != nil { - u.LogErrorf("re-authenticating to %s: %v", c.URL, err) - return nil, err - } - } - - m, err := u.collectController(c) - if err == nil { - return m, nil - } - - u.LogErrorf("collecting metrics %v", err) - u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) - - if err := u.GetUnifi(c); err != nil { - u.LogErrorf("re-authenticating to %s: %v", c.URL, err) - return nil, err - } - - return u.collectController(c) -} - -func (u *UnifiPoller) collectController(c Controller) (*Metrics, error) { - var err error - - m := &Metrics{TS: time.Now()} // At this point, it's the Current Check. - - // Get the sites we care about. - if m.Sites, err = u.GetFilteredSites(c); err != nil { - return m, fmt.Errorf("unifi.GetSites(%v): %v", c.URL, err) - } - - if c.SaveIDS { - m.IDSList, err = c.Unifi.GetIDS(m.Sites, time.Now().Add(2*time.Minute), time.Now()) - if err != nil { - return m, fmt.Errorf("unifi.GetIDS(%v): %v", c.URL, err) - } - } - - // Get all the points. - if m.Clients, err = c.Unifi.GetClients(m.Sites); err != nil { - return m, fmt.Errorf("unifi.GetClients(%v): %v", c.URL, err) - } - - if m.Devices, err = c.Unifi.GetDevices(m.Sites); err != nil { - return m, fmt.Errorf("unifi.GetDevices(%v): %v", c.URL, err) - } - - return u.augmentMetrics(c, m), nil -} - -// augmentMetrics is our middleware layer between collecting metrics and writing them. -// This is where we can manipuate the returned data or make arbitrary decisions. -// This function currently adds parent device names to client metrics. -func (u *UnifiPoller) augmentMetrics(c Controller, metrics *Metrics) *Metrics { - if metrics == nil || metrics.Devices == nil || metrics.Clients == nil { - return metrics - } - - 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] - metrics.Clients[i].ApName = devices[c.ApMac] - metrics.Clients[i].GwName = devices[c.GwMac] - metrics.Clients[i].RadioDescription = bssdIDs[metrics.Clients[i].Bssid] + metrics.Clients[i].RadioProto - } - - if !c.SaveSites { - metrics.Sites = nil - } - - return metrics -} - -// GetFilteredSites returns a list of sites to fetch data for. -// 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(c Controller) (unifi.Sites, error) { - var i int - - sites, err := c.Unifi.GetSites() - if err != nil { - return nil, err - } else if len(c.Sites) < 1 || StringInSlice("all", c.Sites) { - return sites, nil - } - - for _, s := range sites { - // Only include valid sites in the request filter. - if StringInSlice(s.Name, c.Sites) { - sites[i] = s - i++ - } - } - - return sites[:i], nil -} diff --git a/pkg/promunifi/clients.go b/pkg/promunifi/clients.go index fff79857..0ffaf54c 100644 --- a/pkg/promunifi/clients.go +++ b/pkg/promunifi/clients.go @@ -41,7 +41,8 @@ type uclient struct { } func descClient(ns string) *uclient { - labels := []string{"name", "mac", "site_name", "gw_name", "sw_name", "vlan", "ip", "oui", "network", "sw_port", "ap_name", "wired"} + labels := []string{"name", "mac", "site_name", "gw_name", "sw_name", "vlan", + "ip", "oui", "network", "sw_port", "ap_name", "wired"} labelW := append([]string{"radio_name", "radio", "radio_proto", "channel", "essid", "bssid", "radio_desc"}, labels...) return &uclient{ @@ -64,25 +65,34 @@ func descClient(ns string) *uclient { TxPower: prometheus.NewDesc(ns+"radio_transmit_power_dbm", "Client Transmit Power", labelW, nil), TxRate: prometheus.NewDesc(ns+"radio_transmit_rate_bps", "Client Transmit Rate", labelW, nil), WifiTxAttempts: prometheus.NewDesc(ns+"wifi_attempts_transmit_total", "Client Wifi Transmit Attempts", labelW, nil), - Uptime: prometheus.NewDesc(ns+"uptime_seconds", "Client Uptime", labelW, nil), // XXX: re-purpose for info tags. + Uptime: prometheus.NewDesc(ns+"uptime_seconds", "Client Uptime", labelW, nil), /* needs more "looking into" - DpiStatsApp: prometheus.NewDesc(ns+"dpi_stats_app", "Client DPI Stats App", labels, nil), - DpiStatsCat: prometheus.NewDesc(ns+"dpi_stats_cat", "Client DPI Stats Cat", labels, nil), - DpiStatsRxBytes: prometheus.NewDesc(ns+"dpi_stats_receive_bytes_total", "Client DPI Stats Receive Bytes", labels, nil), - DpiStatsRxPackets: prometheus.NewDesc(ns+"dpi_stats_receive_packets_total", "Client DPI Stats Receive Packets", labels, nil), - DpiStatsTxBytes: prometheus.NewDesc(ns+"dpi_stats_transmit_bytes_total", "Client DPI Stats Transmit Bytes", labels, nil), - DpiStatsTxPackets: prometheus.NewDesc(ns+"dpi_stats_transmit_packets_total", "Client DPI Stats Transmit Packets", labels, nil), + DpiStatsApp: prometheus.NewDesc(ns+"dpi_stats_app", + "Client DPI Stats App", labels, nil), + DpiStatsCat: prometheus.NewDesc(ns+"dpi_stats_cat", + "Client DPI Stats Cat", labels, nil), + DpiStatsRxBytes: prometheus.NewDesc(ns+"dpi_stats_receive_bytes_total", + "Client DPI Stats Receive Bytes", labels, nil), + DpiStatsRxPackets: prometheus.NewDesc(ns+"dpi_stats_receive_packets_total", + "Client DPI Stats Receive Packets", labels, nil), + DpiStatsTxBytes: prometheus.NewDesc(ns+"dpi_stats_transmit_bytes_total", + "Client DPI Stats Transmit Bytes", labels, nil), + DpiStatsTxPackets: prometheus.NewDesc(ns+"dpi_stats_transmit_packets_total", + "Client DPI Stats Transmit Packets", labels, nil), */ } } func (u *promUnifi) exportClient(r report, c *unifi.Client) { - labels := []string{c.Name, c.Mac, c.SiteName, c.GwName, c.SwName, c.Vlan.Txt, c.IP, c.Oui, c.Network, c.SwPort.Txt, c.ApName, ""} - labelW := append([]string{c.RadioName, c.Radio, c.RadioProto, c.Channel.Txt, c.Essid, c.Bssid, c.RadioDescription}, labels...) + labels := []string{c.Name, c.Mac, c.SiteName, c.GwName, c.SwName, c.Vlan.Txt, + c.IP, c.Oui, c.Network, c.SwPort.Txt, c.ApName, ""} + labelW := append([]string{c.RadioName, c.Radio, c.RadioProto, c.Channel.Txt, + c.Essid, c.Bssid, c.RadioDescription}, labels...) if c.IsWired.Val { labels[len(labels)-1] = "true" labelW[len(labelW)-1] = "true" + r.send([]*metric{ {u.Client.RxBytes, counter, c.WiredRxBytes, labels}, {u.Client.RxBytesR, gauge, c.WiredRxBytesR, labels}, @@ -94,6 +104,7 @@ func (u *promUnifi) exportClient(r report, c *unifi.Client) { } else { labels[len(labels)-1] = "false" labelW[len(labelW)-1] = "false" + r.send([]*metric{ {u.Client.Anomalies, counter, c.Anomalies, labelW}, {u.Client.CCQ, gauge, float64(c.Ccq) / 1000.0, labelW}, @@ -118,12 +129,13 @@ func (u *promUnifi) exportClient(r report, c *unifi.Client) { } r.send([]*metric{{u.Client.Uptime, gauge, c.Uptime, labelW}}) - /* needs more "looking into" - {u.Client.DpiStatsApp, gauge, c.DpiStats.App, labels}, - {u.Client.DpiStatsCat, gauge, c.DpiStats.Cat, labels}, - {u.Client.DpiStatsRxBytes, counter, c.DpiStats.RxBytes, labels}, - {u.Client.DpiStatsRxPackets, counter, c.DpiStats.RxPackets, labels}, - {u.Client.DpiStatsTxBytes, counter, c.DpiStats.TxBytes, labels}, - {u.Client.DpiStatsTxPackets, counter, c.DpiStats.TxPackets, labels}, - */ } + +/* needs more "looking into" +{u.Client.DpiStatsApp, gauge, c.DpiStats.App, labels}, +{u.Client.DpiStatsCat, gauge, c.DpiStats.Cat, labels}, +{u.Client.DpiStatsRxBytes, counter, c.DpiStats.RxBytes, labels}, +{u.Client.DpiStatsRxPackets, counter, c.DpiStats.RxPackets, labels}, +{u.Client.DpiStatsTxBytes, counter, c.DpiStats.TxBytes, labels}, +{u.Client.DpiStatsTxPackets, counter, c.DpiStats.TxPackets, labels}, +*/ diff --git a/pkg/promunifi/collector.go b/pkg/promunifi/collector.go index e999ac3f..a4aad9f3 100644 --- a/pkg/promunifi/collector.go +++ b/pkg/promunifi/collector.go @@ -79,6 +79,7 @@ type Report struct { func init() { u := &promUnifi{Prometheus: &Prometheus{}} + poller.NewOutput(&poller.Output{ Name: "prometheus", Config: u.Prometheus, @@ -93,33 +94,27 @@ func (u *promUnifi) Run(c poller.Collect) error { return nil } + u.Config.Namespace = strings.Trim(strings.Replace(u.Config.Namespace, "-", "_", -1), "_") if u.Config.Namespace == "" { u.Config.Namespace = strings.Replace(poller.AppName, "-", "", -1) } - u.Config.Namespace = strings.Replace(u.Config.Namespace, "-", "_", -1) - if u.Config.HTTPListen == "" { u.Config.HTTPListen = defaultHTTPListen } prometheus.MustRegister(version.NewCollector(u.Config.Namespace)) - - if u.Config.Namespace = strings.Trim(u.Config.Namespace, "_") + "_"; u.Config.Namespace == "_" { - u.Config.Namespace = "" - } - prometheus.MustRegister(&promUnifi{ Collector: c, - Client: descClient(u.Config.Namespace + "client_"), - Device: descDevice(u.Config.Namespace + "device_"), // stats for all device types. - UAP: descUAP(u.Config.Namespace + "device_"), - USG: descUSG(u.Config.Namespace + "device_"), - USW: descUSW(u.Config.Namespace + "device_"), - Site: descSite(u.Config.Namespace + "site_"), + Client: descClient(u.Config.Namespace + "_client_"), + Device: descDevice(u.Config.Namespace + "_device_"), // stats for all device types. + UAP: descUAP(u.Config.Namespace + "_device_"), + USG: descUSG(u.Config.Namespace + "_device_"), + USW: descUSW(u.Config.Namespace + "_device_"), + Site: descSite(u.Config.Namespace + "_site_"), }) - - c.Logf("Exporting Measurements for Prometheus at https://%s/metrics, namespace: %s", u.Config.HTTPListen, u.Config.Namespace) + c.Logf("Exporting Measurements for Prometheus at https://%s/metrics, namespace: %s", + u.Config.HTTPListen, u.Config.Namespace) return http.ListenAndServe(u.Config.HTTPListen, nil) } @@ -152,6 +147,7 @@ func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { r.error(ch, prometheus.NewInvalidDesc(fmt.Errorf("metric fetch failed")), err) return } + r.Fetch = time.Since(r.Start) if r.Metrics.Devices == nil { @@ -173,6 +169,7 @@ func (u *promUnifi) exportMetrics(r report, ch chan<- prometheus.Metric, ourChan for newMetrics := range ourChan { for _, m := range newMetrics { descs[m.Desc] = true + switch v := m.Value.(type) { case unifi.FlexInt: ch <- r.export(m, v.Val) @@ -195,48 +192,55 @@ func (u *promUnifi) loopExports(r report) { m := r.metrics() r.add() + r.add() + r.add() + r.add() + r.add() + r.add() + go func() { defer r.done() + for _, s := range m.Sites { u.exportSite(r, s) } }() - r.add() go func() { defer r.done() + for _, d := range m.UAPs { u.exportUAP(r, d) } }() - r.add() go func() { defer r.done() + for _, d := range m.UDMs { u.exportUDM(r, d) } }() - r.add() go func() { defer r.done() + for _, d := range m.USGs { u.exportUSG(r, d) } }() - r.add() go func() { defer r.done() + for _, d := range m.USWs { u.exportUSW(r, d) } }() - r.add() go func() { defer r.done() + for _, c := range m.Clients { u.exportClient(r, c) } diff --git a/pkg/promunifi/site.go b/pkg/promunifi/site.go index 1f8999e9..79c57b41 100644 --- a/pkg/promunifi/site.go +++ b/pkg/promunifi/site.go @@ -35,32 +35,34 @@ type site struct { func descSite(ns string) *site { labels := []string{"subsystem", "status", "site_name"} + nd := prometheus.NewDesc + return &site{ - NumUser: prometheus.NewDesc(ns+"users", "Number of Users", labels, nil), - NumGuest: prometheus.NewDesc(ns+"guests", "Number of Guests", labels, nil), - NumIot: prometheus.NewDesc(ns+"iots", "Number of IoT Devices", labels, nil), - TxBytesR: prometheus.NewDesc(ns+"transmit_rate_bytes", "Bytes Transmit Rate", labels, nil), - RxBytesR: prometheus.NewDesc(ns+"receive_rate_bytes", "Bytes Receive Rate", labels, nil), - NumAp: prometheus.NewDesc(ns+"aps", "Access Point Count", labels, nil), - NumAdopted: prometheus.NewDesc(ns+"adopted", "Adoption Count", labels, nil), - NumDisabled: prometheus.NewDesc(ns+"disabled", "Disabled Count", labels, nil), - NumDisconnected: prometheus.NewDesc(ns+"disconnected", "Disconnected Count", labels, nil), - NumPending: prometheus.NewDesc(ns+"pending", "Pending Count", labels, nil), - NumGw: prometheus.NewDesc(ns+"gateways", "Gateway Count", labels, nil), - NumSw: prometheus.NewDesc(ns+"switches", "Switch Count", labels, nil), - NumSta: prometheus.NewDesc(ns+"stations", "Station Count", labels, nil), - Latency: prometheus.NewDesc(ns+"latency_seconds", "Latency", labels, nil), - Uptime: prometheus.NewDesc(ns+"uptime_seconds", "Uptime", labels, nil), - Drops: prometheus.NewDesc(ns+"intenet_drops_total", "Internet (WAN) Disconnections", labels, nil), - XputUp: prometheus.NewDesc(ns+"xput_up_rate", "Speedtest Upload", labels, nil), - XputDown: prometheus.NewDesc(ns+"xput_down_rate", "Speedtest Download", labels, nil), - SpeedtestPing: prometheus.NewDesc(ns+"speedtest_ping", "Speedtest Ping", labels, nil), - RemoteUserNumActive: prometheus.NewDesc(ns+"remote_user_active", "Remote Users Active", labels, nil), - RemoteUserNumInactive: prometheus.NewDesc(ns+"remote_user_inactive", "Remote Users Inactive", labels, nil), - RemoteUserRxBytes: prometheus.NewDesc(ns+"remote_user_receive_bytes_total", "Remote Users Receive Bytes", labels, nil), - RemoteUserTxBytes: prometheus.NewDesc(ns+"remote_user_transmit_bytes_total", "Remote Users Transmit Bytes", labels, nil), - RemoteUserRxPackets: prometheus.NewDesc(ns+"remote_user_receive_packets_total", "Remote Users Receive Packets", labels, nil), - RemoteUserTxPackets: prometheus.NewDesc(ns+"remote_user_transmit_packets_total", "Remote Users Transmit Packets", labels, nil), + NumUser: nd(ns+"users", "Number of Users", labels, nil), + NumGuest: nd(ns+"guests", "Number of Guests", labels, nil), + NumIot: nd(ns+"iots", "Number of IoT Devices", labels, nil), + TxBytesR: nd(ns+"transmit_rate_bytes", "Bytes Transmit Rate", labels, nil), + RxBytesR: nd(ns+"receive_rate_bytes", "Bytes Receive Rate", labels, nil), + NumAp: nd(ns+"aps", "Access Point Count", labels, nil), + NumAdopted: nd(ns+"adopted", "Adoption Count", labels, nil), + NumDisabled: nd(ns+"disabled", "Disabled Count", labels, nil), + NumDisconnected: nd(ns+"disconnected", "Disconnected Count", labels, nil), + NumPending: nd(ns+"pending", "Pending Count", labels, nil), + NumGw: nd(ns+"gateways", "Gateway Count", labels, nil), + NumSw: nd(ns+"switches", "Switch Count", labels, nil), + NumSta: nd(ns+"stations", "Station Count", labels, nil), + Latency: nd(ns+"latency_seconds", "Latency", labels, nil), + Uptime: nd(ns+"uptime_seconds", "Uptime", labels, nil), + Drops: nd(ns+"intenet_drops_total", "Internet (WAN) Disconnections", labels, nil), + XputUp: nd(ns+"xput_up_rate", "Speedtest Upload", labels, nil), + XputDown: nd(ns+"xput_down_rate", "Speedtest Download", labels, nil), + SpeedtestPing: nd(ns+"speedtest_ping", "Speedtest Ping", labels, nil), + RemoteUserNumActive: nd(ns+"remote_user_active", "Remote Users Active", labels, nil), + RemoteUserNumInactive: nd(ns+"remote_user_inactive", "Remote Users Inactive", labels, nil), + RemoteUserRxBytes: nd(ns+"remote_user_receive_bytes_total", "Remote Users Receive Bytes", labels, nil), + RemoteUserTxBytes: nd(ns+"remote_user_transmit_bytes_total", "Remote Users Transmit Bytes", labels, nil), + RemoteUserRxPackets: nd(ns+"remote_user_receive_packets_total", "Remote Users Receive Packets", labels, nil), + RemoteUserTxPackets: nd(ns+"remote_user_transmit_packets_total", "Remote Users Transmit Packets", labels, nil), } } @@ -78,7 +80,6 @@ func (u *promUnifi) exportSite(r report, s *unifi.Site) { {u.Site.SpeedtestPing, gauge, h.SpeedtestPing, labels}, {u.Site.Drops, counter, h.Drops, labels}, }) - case "wlan": r.send([]*metric{ {u.Site.TxBytesR, gauge, h.TxBytesR, labels}, @@ -92,7 +93,6 @@ func (u *promUnifi) exportSite(r report, s *unifi.Site) { {u.Site.NumAp, gauge, h.NumAp, labels}, {u.Site.NumDisabled, gauge, h.NumDisabled, labels}, }) - case "wan": r.send([]*metric{ {u.Site.TxBytesR, gauge, h.TxBytesR, labels}, @@ -103,7 +103,6 @@ func (u *promUnifi) exportSite(r report, s *unifi.Site) { {u.Site.NumGw, gauge, h.NumGw, labels}, {u.Site.NumSta, gauge, h.NumSta, labels}, }) - case "lan": r.send([]*metric{ {u.Site.TxBytesR, gauge, h.TxBytesR, labels}, @@ -116,7 +115,6 @@ func (u *promUnifi) exportSite(r report, s *unifi.Site) { {u.Site.NumIot, gauge, h.NumIot, labels}, {u.Site.NumSw, gauge, h.NumSw, labels}, }) - case "vpn": r.send([]*metric{ {u.Site.RemoteUserNumActive, gauge, h.RemoteUserNumActive, labels}, diff --git a/pkg/promunifi/uap.go b/pkg/promunifi/uap.go index 53242159..0f3a05aa 100644 --- a/pkg/promunifi/uap.go +++ b/pkg/promunifi/uap.go @@ -83,79 +83,80 @@ func descUAP(ns string) *uap { labelA := []string{"stat", "site_name", "name"} // stat + labels[1:] labelV := []string{"vap_name", "bssid", "radio", "radio_name", "essid", "usage", "site_name", "name"} labelR := []string{"radio_name", "radio", "site_name", "name"} + nd := prometheus.NewDesc return &uap{ // 3x each - stat table: total, guest, user - ApWifiTxDropped: prometheus.NewDesc(ns+"stat_wifi_transmt_dropped_total", "Wifi Transmissions Dropped", labelA, nil), - ApRxErrors: prometheus.NewDesc(ns+"stat_receive_errors_total", "Receive Errors", labelA, nil), - ApRxDropped: prometheus.NewDesc(ns+"stat_receive_dropped_total", "Receive Dropped", labelA, nil), - ApRxFrags: prometheus.NewDesc(ns+"stat_receive_frags_total", "Received Frags", labelA, nil), - ApRxCrypts: prometheus.NewDesc(ns+"stat_receive_crypts_total", "Receive Crypts", labelA, nil), - ApTxPackets: prometheus.NewDesc(ns+"stat_transmit_packets_total", "Transmit Packets", labelA, nil), - ApTxBytes: prometheus.NewDesc(ns+"stat_transmit_bytes_total", "Transmit Bytes", labelA, nil), - ApTxErrors: prometheus.NewDesc(ns+"stat_transmit_errors_total", "Transmit Errors", labelA, nil), - ApTxDropped: prometheus.NewDesc(ns+"stat_transmit_dropped_total", "Transmit Dropped", labelA, nil), - ApTxRetries: prometheus.NewDesc(ns+"stat_retries_tx_total", "Transmit Retries", labelA, nil), - ApRxPackets: prometheus.NewDesc(ns+"stat_receive_packets_total", "Receive Packets", labelA, nil), - ApRxBytes: prometheus.NewDesc(ns+"stat_receive_bytes_total", "Receive Bytes", labelA, nil), - WifiTxAttempts: prometheus.NewDesc(ns+"stat_wifi_transmit_attempts_total", "Wifi Transmission Attempts", labelA, nil), - MacFilterRejections: prometheus.NewDesc(ns+"stat_mac_filter_rejects_total", "MAC Filter Rejections", labelA, nil), + ApWifiTxDropped: nd(ns+"stat_wifi_transmt_dropped_total", "Wifi Transmissions Dropped", labelA, nil), + ApRxErrors: nd(ns+"stat_receive_errors_total", "Receive Errors", labelA, nil), + ApRxDropped: nd(ns+"stat_receive_dropped_total", "Receive Dropped", labelA, nil), + ApRxFrags: nd(ns+"stat_receive_frags_total", "Received Frags", labelA, nil), + ApRxCrypts: nd(ns+"stat_receive_crypts_total", "Receive Crypts", labelA, nil), + ApTxPackets: nd(ns+"stat_transmit_packets_total", "Transmit Packets", labelA, nil), + ApTxBytes: nd(ns+"stat_transmit_bytes_total", "Transmit Bytes", labelA, nil), + ApTxErrors: nd(ns+"stat_transmit_errors_total", "Transmit Errors", labelA, nil), + ApTxDropped: nd(ns+"stat_transmit_dropped_total", "Transmit Dropped", labelA, nil), + ApTxRetries: nd(ns+"stat_retries_tx_total", "Transmit Retries", labelA, nil), + ApRxPackets: nd(ns+"stat_receive_packets_total", "Receive Packets", labelA, nil), + ApRxBytes: nd(ns+"stat_receive_bytes_total", "Receive Bytes", labelA, nil), + WifiTxAttempts: nd(ns+"stat_wifi_transmit_attempts_total", "Wifi Transmission Attempts", labelA, nil), + MacFilterRejections: nd(ns+"stat_mac_filter_rejects_total", "MAC Filter Rejections", labelA, nil), // N each - 1 per Virtual AP (VAP) - VAPCcq: prometheus.NewDesc(ns+"vap_ccq_ratio", "VAP Client Connection Quality", labelV, nil), - VAPMacFilterRejections: prometheus.NewDesc(ns+"vap_mac_filter_rejects_total", "VAP MAC Filter Rejections", labelV, nil), - VAPNumSatisfactionSta: prometheus.NewDesc(ns+"vap_satisfaction_stations", "VAP Number Satisifaction Stations", labelV, nil), - VAPAvgClientSignal: prometheus.NewDesc(ns+"vap_average_client_signal", "VAP Average Client Signal", labelV, nil), - VAPSatisfaction: prometheus.NewDesc(ns+"vap_satisfaction_ratio", "VAP Satisfaction", labelV, nil), - VAPSatisfactionNow: prometheus.NewDesc(ns+"vap_satisfaction_now_ratio", "VAP Satisfaction Now", labelV, nil), - VAPDNSAvgLatency: prometheus.NewDesc(ns+"vap_dns_latency_average_seconds", "VAP DNS Latency Average", labelV, nil), - VAPRxBytes: prometheus.NewDesc(ns+"vap_receive_bytes_total", "VAP Bytes Received", labelV, nil), - VAPRxCrypts: prometheus.NewDesc(ns+"vap_receive_crypts_total", "VAP Crypts Received", labelV, nil), - VAPRxDropped: prometheus.NewDesc(ns+"vap_receive_dropped_total", "VAP Dropped Received", labelV, nil), - VAPRxErrors: prometheus.NewDesc(ns+"vap_receive_errors_total", "VAP Errors Received", labelV, nil), - VAPRxFrags: prometheus.NewDesc(ns+"vap_receive_frags_total", "VAP Frags Received", labelV, nil), - VAPRxNwids: prometheus.NewDesc(ns+"vap_receive_nwids_total", "VAP Nwids Received", labelV, nil), - VAPRxPackets: prometheus.NewDesc(ns+"vap_receive_packets_total", "VAP Packets Received", labelV, nil), - VAPTxBytes: prometheus.NewDesc(ns+"vap_transmit_bytes_total", "VAP Bytes Transmitted", labelV, nil), - VAPTxDropped: prometheus.NewDesc(ns+"vap_transmit_dropped_total", "VAP Dropped Transmitted", labelV, nil), - VAPTxErrors: prometheus.NewDesc(ns+"vap_transmit_errors_total", "VAP Errors Transmitted", labelV, nil), - VAPTxPackets: prometheus.NewDesc(ns+"vap_transmit_packets_total", "VAP Packets Transmitted", labelV, nil), - VAPTxPower: prometheus.NewDesc(ns+"vap_transmit_power", "VAP Transmit Power", labelV, nil), - VAPTxRetries: prometheus.NewDesc(ns+"vap_transmit_retries_total", "VAP Retries Transmitted", labelV, nil), - VAPTxCombinedRetries: prometheus.NewDesc(ns+"vap_transmit_retries_combined_total", "VAP Retries Combined Transmitted", labelV, nil), - VAPTxDataMpduBytes: prometheus.NewDesc(ns+"vap_data_mpdu_transmit_bytes_total", "VAP Data MPDU Bytes Transmitted", labelV, nil), - VAPTxRtsRetries: prometheus.NewDesc(ns+"vap_transmit_rts_retries_total", "VAP RTS Retries Transmitted", labelV, nil), - VAPTxSuccess: prometheus.NewDesc(ns+"vap_transmit_success_total", "VAP Success Transmits", labelV, nil), - VAPTxTotal: prometheus.NewDesc(ns+"vap_transmit_total", "VAP Transmit Total", labelV, nil), - VAPTxGoodbytes: prometheus.NewDesc(ns+"vap_transmit_goodbyes", "VAP Goodbyes Transmitted", labelV, nil), - VAPTxLatAvg: prometheus.NewDesc(ns+"vap_transmit_latency_average_seconds", "VAP Latency Average Transmit", labelV, nil), - VAPTxLatMax: prometheus.NewDesc(ns+"vap_transmit_latency_maximum_seconds", "VAP Latency Maximum Transmit", labelV, nil), - VAPTxLatMin: prometheus.NewDesc(ns+"vap_transmit_latency_minimum_seconds", "VAP Latency Minimum Transmit", labelV, nil), - VAPRxGoodbytes: prometheus.NewDesc(ns+"vap_receive_goodbyes", "VAP Goodbyes Received", labelV, nil), - VAPRxLatAvg: prometheus.NewDesc(ns+"vap_receive_latency_average_seconds", "VAP Latency Average Receive", labelV, nil), - VAPRxLatMax: prometheus.NewDesc(ns+"vap_receive_latency_maximum_seconds", "VAP Latency Maximum Receive", labelV, nil), - VAPRxLatMin: prometheus.NewDesc(ns+"vap_receive_latency_minimum_seconds", "VAP Latency Minimum Receive", labelV, nil), - VAPWifiTxLatencyMovAvg: prometheus.NewDesc(ns+"vap_transmit_latency_moving_avg_seconds", "VAP Latency Moving Average Tramsit", labelV, nil), - VAPWifiTxLatencyMovMax: prometheus.NewDesc(ns+"vap_transmit_latency_moving_max_seconds", "VAP Latency Moving Maximum Tramsit", labelV, nil), - VAPWifiTxLatencyMovMin: prometheus.NewDesc(ns+"vap_transmit_latency_moving_min_seconds", "VAP Latency Moving Minimum Tramsit", labelV, nil), - VAPWifiTxLatencyMovTotal: prometheus.NewDesc(ns+"vap_transmit_latency_moving_total", "VAP Latency Moving Total Tramsit", labelV, nil), - VAPWifiTxLatencyMovCount: prometheus.NewDesc(ns+"vap_transmit_latency_moving_count", "VAP Latency Moving Count Tramsit", labelV, nil), + VAPCcq: nd(ns+"vap_ccq_ratio", "VAP Client Connection Quality", labelV, nil), + VAPMacFilterRejections: nd(ns+"vap_mac_filter_rejects_total", "VAP MAC Filter Rejections", labelV, nil), + VAPNumSatisfactionSta: nd(ns+"vap_satisfaction_stations", "VAP Number Satisifaction Stations", labelV, nil), + VAPAvgClientSignal: nd(ns+"vap_average_client_signal", "VAP Average Client Signal", labelV, nil), + VAPSatisfaction: nd(ns+"vap_satisfaction_ratio", "VAP Satisfaction", labelV, nil), + VAPSatisfactionNow: nd(ns+"vap_satisfaction_now_ratio", "VAP Satisfaction Now", labelV, nil), + VAPDNSAvgLatency: nd(ns+"vap_dns_latency_average_seconds", "VAP DNS Latency Average", labelV, nil), + VAPRxBytes: nd(ns+"vap_receive_bytes_total", "VAP Bytes Received", labelV, nil), + VAPRxCrypts: nd(ns+"vap_receive_crypts_total", "VAP Crypts Received", labelV, nil), + VAPRxDropped: nd(ns+"vap_receive_dropped_total", "VAP Dropped Received", labelV, nil), + VAPRxErrors: nd(ns+"vap_receive_errors_total", "VAP Errors Received", labelV, nil), + VAPRxFrags: nd(ns+"vap_receive_frags_total", "VAP Frags Received", labelV, nil), + VAPRxNwids: nd(ns+"vap_receive_nwids_total", "VAP Nwids Received", labelV, nil), + VAPRxPackets: nd(ns+"vap_receive_packets_total", "VAP Packets Received", labelV, nil), + VAPTxBytes: nd(ns+"vap_transmit_bytes_total", "VAP Bytes Transmitted", labelV, nil), + VAPTxDropped: nd(ns+"vap_transmit_dropped_total", "VAP Dropped Transmitted", labelV, nil), + VAPTxErrors: nd(ns+"vap_transmit_errors_total", "VAP Errors Transmitted", labelV, nil), + VAPTxPackets: nd(ns+"vap_transmit_packets_total", "VAP Packets Transmitted", labelV, nil), + VAPTxPower: nd(ns+"vap_transmit_power", "VAP Transmit Power", labelV, nil), + VAPTxRetries: nd(ns+"vap_transmit_retries_total", "VAP Retries Transmitted", labelV, nil), + VAPTxCombinedRetries: nd(ns+"vap_transmit_retries_combined_total", "VAP Retries Combined Tx", labelV, nil), + VAPTxDataMpduBytes: nd(ns+"vap_data_mpdu_transmit_bytes_total", "VAP Data MPDU Bytes Tx", labelV, nil), + VAPTxRtsRetries: nd(ns+"vap_transmit_rts_retries_total", "VAP RTS Retries Transmitted", labelV, nil), + VAPTxSuccess: nd(ns+"vap_transmit_success_total", "VAP Success Transmits", labelV, nil), + VAPTxTotal: nd(ns+"vap_transmit_total", "VAP Transmit Total", labelV, nil), + VAPTxGoodbytes: nd(ns+"vap_transmit_goodbyes", "VAP Goodbyes Transmitted", labelV, nil), + VAPTxLatAvg: nd(ns+"vap_transmit_latency_average_seconds", "VAP Latency Average Tx", labelV, nil), + VAPTxLatMax: nd(ns+"vap_transmit_latency_maximum_seconds", "VAP Latency Maximum Tx", labelV, nil), + VAPTxLatMin: nd(ns+"vap_transmit_latency_minimum_seconds", "VAP Latency Minimum Tx", labelV, nil), + VAPRxGoodbytes: nd(ns+"vap_receive_goodbyes", "VAP Goodbyes Received", labelV, nil), + VAPRxLatAvg: nd(ns+"vap_receive_latency_average_seconds", "VAP Latency Average Rx", labelV, nil), + VAPRxLatMax: nd(ns+"vap_receive_latency_maximum_seconds", "VAP Latency Maximum Rx", labelV, nil), + VAPRxLatMin: nd(ns+"vap_receive_latency_minimum_seconds", "VAP Latency Minimum Rx", labelV, nil), + VAPWifiTxLatencyMovAvg: nd(ns+"vap_transmit_latency_moving_avg_seconds", "VAP Latency Moving Avg Tx", labelV, nil), + VAPWifiTxLatencyMovMax: nd(ns+"vap_transmit_latency_moving_max_seconds", "VAP Latency Moving Min Tx", labelV, nil), + VAPWifiTxLatencyMovMin: nd(ns+"vap_transmit_latency_moving_min_seconds", "VAP Latency Moving Max Tx", labelV, nil), + VAPWifiTxLatencyMovTotal: nd(ns+"vap_transmit_latency_moving_total", "VAP Latency Moving Total Tramsit", labelV, nil), + VAPWifiTxLatencyMovCount: nd(ns+"vap_transmit_latency_moving_count", "VAP Latency Moving Count Tramsit", labelV, nil), // N each - 1 per Radio. 1-4 radios per AP usually - RadioCurrentAntennaGain: prometheus.NewDesc(ns+"radio_current_antenna_gain", "Radio Current Antenna Gain", labelR, nil), - RadioHt: prometheus.NewDesc(ns+"radio_ht", "Radio HT", labelR, nil), - RadioMaxTxpower: prometheus.NewDesc(ns+"radio_max_transmit_power", "Radio Maximum Transmit Power", labelR, nil), - RadioMinTxpower: prometheus.NewDesc(ns+"radio_min_transmit_power", "Radio Minimum Transmit Power", labelR, nil), - RadioNss: prometheus.NewDesc(ns+"radio_nss", "Radio Nss", labelR, nil), - RadioRadioCaps: prometheus.NewDesc(ns+"radio_caps", "Radio Capabilities", labelR, nil), - RadioTxPower: prometheus.NewDesc(ns+"radio_transmit_power", "Radio Transmit Power", labelR, nil), - RadioAstBeXmit: prometheus.NewDesc(ns+"radio_ast_be_xmit", "Radio AstBe Transmit", labelR, nil), - RadioChannel: prometheus.NewDesc(ns+"radio_channel", "Radio Channel", labelR, nil), - RadioCuSelfRx: prometheus.NewDesc(ns+"radio_channel_utilization_receive_ratio", "Radio Channel Utilization Receive", labelR, nil), - RadioCuSelfTx: prometheus.NewDesc(ns+"radio_channel_utilization_transmit_ratio", "Radio Channel Utilization Transmit", labelR, nil), - RadioExtchannel: prometheus.NewDesc(ns+"radio_ext_channel", "Radio Ext Channel", labelR, nil), - RadioGain: prometheus.NewDesc(ns+"radio_gain", "Radio Gain", labelR, nil), - RadioNumSta: prometheus.NewDesc(ns+"radio_stations", "Radio Total Station Count", append(labelR, "station_type"), nil), - RadioTxPackets: prometheus.NewDesc(ns+"radio_transmit_packets", "Radio Transmitted Packets", labelR, nil), - RadioTxRetries: prometheus.NewDesc(ns+"radio_transmit_retries", "Radio Transmit Retries", labelR, nil), + RadioCurrentAntennaGain: nd(ns+"radio_current_antenna_gain", "Radio Current Antenna Gain", labelR, nil), + RadioHt: nd(ns+"radio_ht", "Radio HT", labelR, nil), + RadioMaxTxpower: nd(ns+"radio_max_transmit_power", "Radio Maximum Transmit Power", labelR, nil), + RadioMinTxpower: nd(ns+"radio_min_transmit_power", "Radio Minimum Transmit Power", labelR, nil), + RadioNss: nd(ns+"radio_nss", "Radio Nss", labelR, nil), + RadioRadioCaps: nd(ns+"radio_caps", "Radio Capabilities", labelR, nil), + RadioTxPower: nd(ns+"radio_transmit_power", "Radio Transmit Power", labelR, nil), + RadioAstBeXmit: nd(ns+"radio_ast_be_xmit", "Radio AstBe Transmit", labelR, nil), + RadioChannel: nd(ns+"radio_channel", "Radio Channel", labelR, nil), + RadioCuSelfRx: nd(ns+"radio_channel_utilization_receive_ratio", "Channel Utilization Rx", labelR, nil), + RadioCuSelfTx: nd(ns+"radio_channel_utilization_transmit_ratio", "Channel Utilization Tx", labelR, nil), + RadioExtchannel: nd(ns+"radio_ext_channel", "Radio Ext Channel", labelR, nil), + RadioGain: nd(ns+"radio_gain", "Radio Gain", labelR, nil), + RadioNumSta: nd(ns+"radio_stations", "Radio Total Station Count", append(labelR, "station_type"), nil), + RadioTxPackets: nd(ns+"radio_transmit_packets", "Radio Transmitted Packets", labelR, nil), + RadioTxRetries: nd(ns+"radio_transmit_retries", "Radio Transmit Retries", labelR, nil), } } @@ -283,6 +284,7 @@ func (u *promUnifi) exportRADtable(r report, labels []string, rt unifi.RadioTabl labelR := []string{p.Name, p.Radio, labels[1], labels[2]} labelRUser := append(labelR, "user") labelRGuest := append(labelR, "guest") + r.send([]*metric{ {u.UAP.RadioCurrentAntennaGain, gauge, p.CurrentAntennaGain, labelR}, {u.UAP.RadioHt, gauge, p.Ht, labelR}, @@ -311,6 +313,7 @@ func (u *promUnifi) exportRADtable(r report, labels []string, rt unifi.RadioTabl {u.UAP.RadioTxPackets, gauge, t.TxPackets, labelR}, {u.UAP.RadioTxRetries, gauge, t.TxRetries, labelR}, }) + break } } diff --git a/pkg/promunifi/usg.go b/pkg/promunifi/usg.go index 631ef358..08ee3781 100644 --- a/pkg/promunifi/usg.go +++ b/pkg/promunifi/usg.go @@ -37,6 +37,7 @@ type usg struct { func descUSG(ns string) *usg { labels := []string{"port", "site_name", "name"} + return &usg{ WanRxPackets: prometheus.NewDesc(ns+"wan_receive_packets_total", "WAN Receive Packets Total", labels, nil), WanRxBytes: prometheus.NewDesc(ns+"wan_receive_bytes_total", "WAN Receive Bytes Total", labels, nil), @@ -75,6 +76,7 @@ func (u *promUnifi) exportUSG(r report, d *unifi.USG) { 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. u.exportWANPorts(r, labels, d.Wan1, d.Wan2) u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) @@ -95,6 +97,7 @@ func (u *promUnifi) exportUSGstats(r report, labels []string, gw *unifi.Gw, st u labelLan := []string{"lan", labels[1], labels[2]} labelWan := []string{"all", labels[1], labels[2]} + r.send([]*metric{ {u.USG.LanRxPackets, counter, gw.LanRxPackets, labelLan}, {u.USG.LanRxBytes, counter, gw.LanRxBytes, labelLan}, @@ -119,6 +122,7 @@ func (u *promUnifi) exportWANPorts(r report, labels []string, wans ...unifi.Wan) } labelWan := []string{wan.Name, labels[1], labels[2]} + r.send([]*metric{ {u.USG.WanRxPackets, counter, wan.RxPackets, labelWan}, {u.USG.WanRxBytes, counter, wan.RxBytes, labelWan}, diff --git a/pkg/promunifi/usw.go b/pkg/promunifi/usw.go index e89506cb..d700583f 100644 --- a/pkg/promunifi/usw.go +++ b/pkg/promunifi/usw.go @@ -49,45 +49,46 @@ func descUSW(ns string) *usw { pns := ns + "port_" labelS := []string{"site_name", "name"} labelP := []string{"port_id", "port_num", "port_name", "port_mac", "port_ip", "site_name", "name"} + nd := prometheus.NewDesc return &usw{ // This data may be derivable by sum()ing the port data. - SwRxPackets: prometheus.NewDesc(ns+"switch_receive_packets_total", "Switch Packets Received Total", labelS, nil), - SwRxBytes: prometheus.NewDesc(ns+"switch_receive_bytes_total", "Switch Bytes Received Total", labelS, nil), - SwRxErrors: prometheus.NewDesc(ns+"switch_receive_errors_total", "Switch Errors Received Total", labelS, nil), - SwRxDropped: prometheus.NewDesc(ns+"switch_receive_dropped_total", "Switch Dropped Received Total", labelS, nil), - SwRxCrypts: prometheus.NewDesc(ns+"switch_receive_crypts_total", "Switch Crypts Received Total", labelS, nil), - SwRxFrags: prometheus.NewDesc(ns+"switch_receive_frags_total", "Switch Frags Received Total", labelS, nil), - SwTxPackets: prometheus.NewDesc(ns+"switch_transmit_packets_total", "Switch Packets Transmit Total", labelS, nil), - SwTxBytes: prometheus.NewDesc(ns+"switch_transmit_bytes_total", "Switch Bytes Transmit Total", labelS, nil), - SwTxErrors: prometheus.NewDesc(ns+"switch_transmit_errors_total", "Switch Errors Transmit Total", labelS, nil), - SwTxDropped: prometheus.NewDesc(ns+"switch_transmit_dropped_total", "Switch Dropped Transmit Total", labelS, nil), - SwTxRetries: prometheus.NewDesc(ns+"switch_transmit_retries_total", "Switch Retries Transmit Total", labelS, nil), - SwRxMulticast: prometheus.NewDesc(ns+"switch_receive_multicast_total", "Switch Multicast Receive Total", labelS, nil), - SwRxBroadcast: prometheus.NewDesc(ns+"switch_receive_broadcast_total", "Switch Broadcast Receive Total", labelS, nil), - SwTxMulticast: prometheus.NewDesc(ns+"switch_transmit_multicast_total", "Switch Multicast Transmit Total", labelS, nil), - SwTxBroadcast: prometheus.NewDesc(ns+"switch_transmit_broadcast_total", "Switch Broadcast Transmit Total", labelS, nil), - SwBytes: prometheus.NewDesc(ns+"switch_bytes_total", "Switch Bytes Transferred Total", labelS, nil), + SwRxPackets: nd(ns+"switch_receive_packets_total", "Switch Packets Received Total", labelS, nil), + SwRxBytes: nd(ns+"switch_receive_bytes_total", "Switch Bytes Received Total", labelS, nil), + SwRxErrors: nd(ns+"switch_receive_errors_total", "Switch Errors Received Total", labelS, nil), + SwRxDropped: nd(ns+"switch_receive_dropped_total", "Switch Dropped Received Total", labelS, nil), + SwRxCrypts: nd(ns+"switch_receive_crypts_total", "Switch Crypts Received Total", labelS, nil), + SwRxFrags: nd(ns+"switch_receive_frags_total", "Switch Frags Received Total", labelS, nil), + SwTxPackets: nd(ns+"switch_transmit_packets_total", "Switch Packets Transmit Total", labelS, nil), + SwTxBytes: nd(ns+"switch_transmit_bytes_total", "Switch Bytes Transmit Total", labelS, nil), + SwTxErrors: nd(ns+"switch_transmit_errors_total", "Switch Errors Transmit Total", labelS, nil), + SwTxDropped: nd(ns+"switch_transmit_dropped_total", "Switch Dropped Transmit Total", labelS, nil), + SwTxRetries: nd(ns+"switch_transmit_retries_total", "Switch Retries Transmit Total", labelS, nil), + SwRxMulticast: nd(ns+"switch_receive_multicast_total", "Switch Multicast Receive Total", labelS, nil), + SwRxBroadcast: nd(ns+"switch_receive_broadcast_total", "Switch Broadcast Receive Total", labelS, nil), + SwTxMulticast: nd(ns+"switch_transmit_multicast_total", "Switch Multicast Transmit Total", labelS, nil), + SwTxBroadcast: nd(ns+"switch_transmit_broadcast_total", "Switch Broadcast Transmit Total", labelS, nil), + SwBytes: nd(ns+"switch_bytes_total", "Switch Bytes Transferred Total", labelS, nil), // per-port data - PoeCurrent: prometheus.NewDesc(pns+"poe_amperes", "POE Current", labelP, nil), - PoePower: prometheus.NewDesc(pns+"poe_watts", "POE Power", labelP, nil), - PoeVoltage: prometheus.NewDesc(pns+"poe_volts", "POE Voltage", labelP, nil), - RxBroadcast: prometheus.NewDesc(pns+"receive_broadcast_total", "Receive Broadcast", labelP, nil), - RxBytes: prometheus.NewDesc(pns+"receive_bytes_total", "Total Receive Bytes", labelP, nil), - RxBytesR: prometheus.NewDesc(pns+"receive_rate_bytes", "Receive Bytes Rate", labelP, nil), - RxDropped: prometheus.NewDesc(pns+"receive_dropped_total", "Total Receive Dropped", labelP, nil), - RxErrors: prometheus.NewDesc(pns+"receive_errors_total", "Total Receive Errors", labelP, nil), - RxMulticast: prometheus.NewDesc(pns+"receive_multicast_total", "Total Receive Multicast", labelP, nil), - RxPackets: prometheus.NewDesc(pns+"receive_packets_total", "Total Receive Packets", labelP, nil), - Satisfaction: prometheus.NewDesc(pns+"satisfaction_ratio", "Satisfaction", labelP, nil), - Speed: prometheus.NewDesc(pns+"port_speed_bps", "Speed", labelP, nil), - TxBroadcast: prometheus.NewDesc(pns+"transmit_broadcast_total", "Total Transmit Broadcast", labelP, nil), - TxBytes: prometheus.NewDesc(pns+"transmit_bytes_total", "Total Transmit Bytes", labelP, nil), - TxBytesR: prometheus.NewDesc(pns+"transmit_rate_bytes", "Transmit Bytes Rate", labelP, nil), - TxDropped: prometheus.NewDesc(pns+"transmit_dropped_total", "Total Transmit Dropped", labelP, nil), - TxErrors: prometheus.NewDesc(pns+"transmit_errors_total", "Total Transmit Errors", labelP, nil), - TxMulticast: prometheus.NewDesc(pns+"transmit_multicast_total", "Total Tranmist Multicast", labelP, nil), - TxPackets: prometheus.NewDesc(pns+"transmit_packets_total", "Total Transmit Packets", labelP, nil), + PoeCurrent: nd(pns+"poe_amperes", "POE Current", labelP, nil), + PoePower: nd(pns+"poe_watts", "POE Power", labelP, nil), + PoeVoltage: nd(pns+"poe_volts", "POE Voltage", labelP, nil), + RxBroadcast: nd(pns+"receive_broadcast_total", "Receive Broadcast", labelP, nil), + RxBytes: nd(pns+"receive_bytes_total", "Total Receive Bytes", labelP, nil), + RxBytesR: nd(pns+"receive_rate_bytes", "Receive Bytes Rate", labelP, nil), + RxDropped: nd(pns+"receive_dropped_total", "Total Receive Dropped", labelP, nil), + RxErrors: nd(pns+"receive_errors_total", "Total Receive Errors", labelP, nil), + RxMulticast: nd(pns+"receive_multicast_total", "Total Receive Multicast", labelP, nil), + RxPackets: nd(pns+"receive_packets_total", "Total Receive Packets", labelP, nil), + Satisfaction: nd(pns+"satisfaction_ratio", "Satisfaction", labelP, nil), + Speed: nd(pns+"port_speed_bps", "Speed", labelP, nil), + TxBroadcast: nd(pns+"transmit_broadcast_total", "Total Transmit Broadcast", labelP, nil), + TxBytes: nd(pns+"transmit_bytes_total", "Total Transmit Bytes", labelP, nil), + TxBytesR: nd(pns+"transmit_rate_bytes", "Transmit Bytes Rate", labelP, nil), + TxDropped: nd(pns+"transmit_dropped_total", "Total Transmit Dropped", labelP, nil), + TxErrors: nd(pns+"transmit_errors_total", "Total Transmit Errors", labelP, nil), + TxMulticast: nd(pns+"transmit_multicast_total", "Total Tranmist Multicast", labelP, nil), + TxPackets: nd(pns+"transmit_packets_total", "Total Transmit Packets", labelP, nil), } } @@ -98,6 +99,7 @@ func (u *promUnifi) exportUSW(r report, d *unifi.USW) { 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) u.exportPRTtable(r, labels, d.PortTable) u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) @@ -129,6 +131,7 @@ func (u *promUnifi) exportUSWstats(r report, labels []string, sw *unifi.Sw) { } labelS := labels[1:] + r.send([]*metric{ {u.USW.SwRxPackets, counter, sw.RxPackets, labelS}, {u.USW.SwRxBytes, counter, sw.RxBytes, labelS},