From b8d9ac9f880aeb6338260aebcd0330d52786fba2 Mon Sep 17 00:00:00 2001 From: davidnewhall2 Date: Fri, 13 Dec 2019 14:17:28 -0800 Subject: [PATCH] not sure how far I got.. --- pkg/poller/config.go | 134 ++++++++++++++++++++++--------------- pkg/promunifi/clients.go | 2 + pkg/promunifi/collector.go | 6 ++ pkg/promunifi/report.go | 4 ++ pkg/promunifi/site.go | 3 +- pkg/promunifi/uap.go | 6 +- pkg/promunifi/udm.go | 4 ++ pkg/promunifi/usg.go | 3 + pkg/promunifi/usw.go | 8 +++ 9 files changed, 114 insertions(+), 56 deletions(-) diff --git a/pkg/poller/config.go b/pkg/poller/config.go index 1725b03a..bd811a04 100644 --- a/pkg/poller/config.go +++ b/pkg/poller/config.go @@ -14,7 +14,6 @@ import ( "fmt" "io/ioutil" "os" - "path" "reflect" "strconv" "strings" @@ -66,29 +65,44 @@ type Flag struct { *pflag.FlagSet } +// Controller represents the configuration for a UniFi Controller. +// Each polled controller may have its own configuration. +type Controller struct { + Interval Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"` + 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"` + ReAuth bool `json:"reauthenticate" toml:"reauthenticate" xml:"reauthenticate" yaml:"reauthenticate"` + SaveSites bool `json:"save_sites,omitempty" toml:"save_sites,omitempty" xml:"save_sites" yaml:"save_sites"` + User string `json:"unifi_user,omitempty" toml:"unifi_user,omitempty" xml:"unifi_user" yaml:"unifi_user"` + Pass string `json:"unifi_pass,omitempty" toml:"unifi_pass,omitempty" xml:"unifi_pass" yaml:"unifi_pass"` + URL string `json:"unifi_url,omitempty" toml:"unifi_url,omitempty" xml:"unifi_url" yaml:"unifi_url"` + Sites []string `json:"sites,omitempty" toml:"sites,omitempty" xml:"sites" yaml:"sites"` +} + // 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. type Config struct { - Interval Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"` - Debug bool `json:"debug" toml:"debug" xml:"debug" yaml:"debug"` - Quiet bool `json:"quiet,omitempty" toml:"quiet,omitempty" xml:"quiet" yaml:"quiet"` - 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"` - ReAuth bool `json:"reauthenticate" toml:"reauthenticate" xml:"reauthenticate" yaml:"reauthenticate"` - InfxBadSSL bool `json:"influx_insecure_ssl" toml:"influx_insecure_ssl" xml:"influx_insecure_ssl" yaml:"influx_insecure_ssl"` - SaveSites bool `json:"save_sites,omitempty" toml:"save_sites,omitempty" xml:"save_sites" yaml:"save_sites"` - Mode string `json:"mode" toml:"mode" xml:"mode" yaml:"mode"` - HTTPListen string `json:"http_listen" toml:"http_listen" xml:"http_listen" yaml:"http_listen"` - Namespace string `json:"namespace" toml:"namespace" xml:"namespace" yaml:"namespace"` - InfluxURL string `json:"influx_url,omitempty" toml:"influx_url,omitempty" xml:"influx_url" yaml:"influx_url"` - InfluxUser string `json:"influx_user,omitempty" toml:"influx_user,omitempty" xml:"influx_user" yaml:"influx_user"` - InfluxPass string `json:"influx_pass,omitempty" toml:"influx_pass,omitempty" xml:"influx_pass" yaml:"influx_pass"` - InfluxDB string `json:"influx_db,omitempty" toml:"influx_db,omitempty" xml:"influx_db" yaml:"influx_db"` - UnifiUser string `json:"unifi_user,omitempty" toml:"unifi_user,omitempty" xml:"unifi_user" yaml:"unifi_user"` - UnifiPass string `json:"unifi_pass,omitempty" toml:"unifi_pass,omitempty" xml:"unifi_pass" yaml:"unifi_pass"` - UnifiBase string `json:"unifi_url,omitempty" toml:"unifi_url,omitempty" xml:"unifi_url" yaml:"unifi_url"` - Sites []string `json:"sites,omitempty" toml:"sites,omitempty" xml:"sites" yaml:"sites"` + Interval Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"` + Debug bool `json:"debug" toml:"debug" xml:"debug" yaml:"debug"` + Quiet bool `json:"quiet,omitempty" toml:"quiet,omitempty" xml:"quiet" yaml:"quiet"` + 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"` + ReAuth bool `json:"reauthenticate" toml:"reauthenticate" xml:"reauthenticate" yaml:"reauthenticate"` + InfxBadSSL bool `json:"influx_insecure_ssl" toml:"influx_insecure_ssl" xml:"influx_insecure_ssl" yaml:"influx_insecure_ssl"` + SaveSites bool `json:"save_sites,omitempty" toml:"save_sites,omitempty" xml:"save_sites" yaml:"save_sites"` + Mode string `json:"mode" toml:"mode" xml:"mode" yaml:"mode"` + HTTPListen string `json:"http_listen" toml:"http_listen" xml:"http_listen" yaml:"http_listen"` + Namespace string `json:"namespace" toml:"namespace" xml:"namespace" yaml:"namespace"` + InfluxURL string `json:"influx_url,omitempty" toml:"influx_url,omitempty" xml:"influx_url" yaml:"influx_url"` + InfluxUser string `json:"influx_user,omitempty" toml:"influx_user,omitempty" xml:"influx_user" yaml:"influx_user"` + InfluxPass string `json:"influx_pass,omitempty" toml:"influx_pass,omitempty" xml:"influx_pass" yaml:"influx_pass"` + InfluxDB string `json:"influx_db,omitempty" toml:"influx_db,omitempty" xml:"influx_db" yaml:"influx_db"` + UnifiUser string `json:"unifi_user,omitempty" toml:"unifi_user,omitempty" xml:"unifi_user" yaml:"unifi_user"` + UnifiPass string `json:"unifi_pass,omitempty" toml:"unifi_pass,omitempty" xml:"unifi_pass" yaml:"unifi_pass"` + UnifiBase string `json:"unifi_url,omitempty" toml:"unifi_url,omitempty" xml:"unifi_url" yaml:"unifi_url"` + Sites []string `json:"sites,omitempty" toml:"sites,omitempty" xml:"sites" yaml:"sites"` + Controller []*Controller `json:"controller,omitempty" toml:"controller,omitempty" xml:"controller" yaml:"controller"` } // Duration is used to UnmarshalTOML into a time.Duration value. @@ -126,44 +140,58 @@ func (c *Config) ParseENV() error { for i := 0; i < t.NumField(); i++ { // Loop each Config struct member tag := t.Field(i).Tag.Get("json") // Get the ENV variable name from "json" struct tag tag = strings.Split(strings.ToUpper(tag), ",")[0] // Capitalize and remove ,omitempty suffix - env := os.Getenv(ENVConfigPrefix + tag) // Then pull value from OS. - if tag == "" || env == "" { // Skip if either are empty. + + env := os.Getenv(ENVConfigPrefix + tag) // Then pull value from OS. + if tag == "" || env == "" { // Skip if either are empty. continue } - // Reflect and update the u.Config struct member at position i. - switch field := reflect.ValueOf(c).Elem().Field(i); field.Type().String() { - // Handle each member type appropriately (differently). - case "string": - // This is a reflect package method to update a struct member by index. - field.SetString(env) - - case "int": - val, err := strconv.Atoi(env) - if err != nil { - return fmt.Errorf("%s: %v", tag, err) - } - field.Set(reflect.ValueOf(val)) - - case "[]string": - field.Set(reflect.ValueOf(strings.Split(env, ","))) - - case path.Base(t.PkgPath()) + ".Duration": - val, err := time.ParseDuration(env) - if err != nil { - return fmt.Errorf("%s: %v", tag, err) - } - field.Set(reflect.ValueOf(Duration{val})) - - case "bool": - val, err := strconv.ParseBool(env) - if err != nil { - return fmt.Errorf("%s: %v", tag, err) - } - field.SetBool(val) + if err := c.parseENV(reflect.ValueOf(c).Elem().Field(i), tag, env); err != nil { + return err } - // Add more types here if more types are added to the config struct. } return nil } + +func (c *Config) parseENV(field reflect.Value, tag, env string) error { + // Reflect and update the u.Config struct member at position i. + switch field.Type().String() { + // Handle each member type appropriately (differently). + case "string": + // This is a reflect package method to update a struct member by index. + field.SetString(env) + + case "int": + val, err := strconv.Atoi(env) + if err != nil { + return fmt.Errorf("%s: %v", tag, err) + } + field.Set(reflect.ValueOf(val)) + + case "[]string": + field.Set(reflect.ValueOf(strings.Split(env, ","))) + + case "poller.Duration": + val, err := time.ParseDuration(env) + if err != nil { + return fmt.Errorf("%s: %v", tag, err) + } + field.Set(reflect.ValueOf(Duration{val})) + + case "bool": + val, err := strconv.ParseBool(env) + if err != nil { + return fmt.Errorf("%s: %v", tag, err) + } + field.SetBool(val) + + case "poller.[]*Controller": + } + // Add more types here if more types are added to the config struct. + + return nil +} + +func (c *Config) parseSlice(field reflect.Value, tag, env string) error { +} diff --git a/pkg/promunifi/clients.go b/pkg/promunifi/clients.go index 3b48616a..fff79857 100644 --- a/pkg/promunifi/clients.go +++ b/pkg/promunifi/clients.go @@ -43,6 +43,7 @@ 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"} labelW := append([]string{"radio_name", "radio", "radio_proto", "channel", "essid", "bssid", "radio_desc"}, labels...) + return &uclient{ Anomalies: prometheus.NewDesc(ns+"anomalies", "Client Anomalies", labelW, nil), BytesR: prometheus.NewDesc(ns+"transfer_rate_bytes", "Client Data Rate", labelW, nil), @@ -115,6 +116,7 @@ func (u *promUnifi) exportClient(r report, c *unifi.Client) { {u.Client.BytesR, gauge, c.BytesR, labelW}, }) } + r.send([]*metric{{u.Client.Uptime, gauge, c.Uptime, labelW}}) /* needs more "looking into" {u.Client.DpiStatsApp, gauge, c.DpiStats.App, labels}, diff --git a/pkg/promunifi/collector.go b/pkg/promunifi/collector.go index aa14b1a8..5911d005 100644 --- a/pkg/promunifi/collector.go +++ b/pkg/promunifi/collector.go @@ -75,9 +75,11 @@ func NewUnifiCollector(opts UnifiCollectorCnfg) prometheus.Collector { if opts.CollectFn == nil { panic("nil collector function") } + if opts.Namespace = strings.Trim(opts.Namespace, "_") + "_"; opts.Namespace == "_" { opts.Namespace = "" } + return &promUnifi{ Config: opts, Client: descClient(opts.Namespace + "client_"), @@ -94,6 +96,7 @@ func NewUnifiCollector(opts UnifiCollectorCnfg) prometheus.Collector { func (u *promUnifi) Describe(ch chan<- *prometheus.Desc) { for _, f := range []interface{}{u.Client, u.Device, u.UAP, u.USG, u.USW, u.Site} { v := reflect.Indirect(reflect.ValueOf(f)) + // Loop each struct member and send it to the provided channel. for i := 0; i < v.NumField(); i++ { desc, ok := v.Field(i).Interface().(*prometheus.Desc) @@ -108,6 +111,7 @@ func (u *promUnifi) Describe(ch chan<- *prometheus.Desc) { // the current metrics (from another package) then exports them for prometheus. func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { var err error + r := &Report{cf: u.Config, ch: make(chan []*metric, buffer), Start: time.Now()} defer r.close() @@ -116,6 +120,7 @@ func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { return } r.Fetch = time.Since(r.Start) + if r.Metrics.Devices == nil { r.Metrics.Devices = &unifi.Devices{} } @@ -131,6 +136,7 @@ func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { func (u *promUnifi) exportMetrics(r report, ch chan<- prometheus.Metric, ourChan chan []*metric) { descs := make(map[*prometheus.Desc]bool) // used as a counter defer r.report(descs) + for newMetrics := range ourChan { for _, m := range newMetrics { descs[m.Desc] = true diff --git a/pkg/promunifi/report.go b/pkg/promunifi/report.go index 7d40ea31..0ddf29d3 100644 --- a/pkg/promunifi/report.go +++ b/pkg/promunifi/report.go @@ -46,20 +46,24 @@ func (r *Report) report(descs map[*prometheus.Desc]bool) { if r.cf.LoggingFn == nil { return } + r.Descs = len(descs) r.cf.LoggingFn(r) } func (r *Report) export(m *metric, v float64) prometheus.Metric { r.Total++ + if v == 0 { r.Zeros++ } + return prometheus.MustNewConstMetric(m.Desc, m.ValueType, v, m.Labels...) } func (r *Report) error(ch chan<- prometheus.Metric, d *prometheus.Desc, v interface{}) { r.Errors++ + if r.cf.ReportErrors { ch <- prometheus.NewInvalidMetric(d, fmt.Errorf("error: %v", v)) } diff --git a/pkg/promunifi/site.go b/pkg/promunifi/site.go index c15db2c6..1f8999e9 100644 --- a/pkg/promunifi/site.go +++ b/pkg/promunifi/site.go @@ -66,8 +66,7 @@ func descSite(ns string) *site { func (u *promUnifi) exportSite(r report, s *unifi.Site) { for _, h := range s.Health { - labels := []string{h.Subsystem, h.Status, s.SiteName} - switch h.Subsystem { + switch labels := []string{h.Subsystem, h.Status, s.SiteName}; labels[0] { case "www": r.send([]*metric{ {u.Site.TxBytesR, gauge, h.TxBytesR, labels}, diff --git a/pkg/promunifi/uap.go b/pkg/promunifi/uap.go index 9998df30..53242159 100644 --- a/pkg/promunifi/uap.go +++ b/pkg/promunifi/uap.go @@ -83,6 +83,7 @@ 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"} + return &uap{ // 3x each - stat table: total, guest, user ApWifiTxDropped: prometheus.NewDesc(ns+"stat_wifi_transmt_dropped_total", "Wifi Transmissions Dropped", labelA, nil), @@ -162,6 +163,7 @@ func (u *promUnifi) exportUAP(r report, d *unifi.UAP) { if !d.Adopted.Val || d.Locating.Val { return } + labels := []string{d.Type, d.SiteName, d.Name} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} u.exportUAPstats(r, labels, d.Stat.Ap, d.BytesD, d.TxBytesD, d.RxBytesD, d.BytesR) @@ -181,6 +183,7 @@ func (u *promUnifi) exportUAPstats(r report, labels []string, ap *unifi.Ap, byte if ap == nil { return } + labelU := []string{"user", labels[1], labels[2]} labelG := []string{"guest", labels[1], labels[2]} r.send([]*metric{ @@ -229,8 +232,8 @@ func (u *promUnifi) exportVAPtable(r report, labels []string, vt unifi.VapTable) if !v.Up.Val { continue } - labelV := []string{v.Name, v.Bssid, v.Radio, v.RadioName, v.Essid, v.Usage, labels[1], labels[2]} + labelV := []string{v.Name, v.Bssid, v.Radio, v.RadioName, v.Essid, v.Usage, labels[1], labels[2]} r.send([]*metric{ {u.UAP.VAPCcq, gauge, float64(v.Ccq) / 1000.0, labelV}, {u.UAP.VAPMacFilterRejections, counter, v.MacFilterRejections, labelV}, @@ -294,6 +297,7 @@ func (u *promUnifi) exportRADtable(r report, labels []string, rt unifi.RadioTabl if t.Name != p.Name { continue } + r.send([]*metric{ {u.UAP.RadioTxPower, gauge, t.TxPower, labelR}, {u.UAP.RadioAstBeXmit, gauge, t.AstBeXmit, labelR}, diff --git a/pkg/promunifi/udm.go b/pkg/promunifi/udm.go index 3ce95117..34d7e66d 100644 --- a/pkg/promunifi/udm.go +++ b/pkg/promunifi/udm.go @@ -33,6 +33,7 @@ type unifiDevice struct { func descDevice(ns string) *unifiDevice { labels := []string{"type", "site_name", "name"} infoLabels := []string{"version", "model", "serial", "mac", "ip", "id", "bytes", "uptime"} + return &unifiDevice{ Info: prometheus.NewDesc(ns+"info", "Device Information", append(labels, infoLabels...), nil), Uptime: prometheus.NewDesc(ns+"uptime_seconds", "Device Uptime", labels, nil), @@ -63,6 +64,7 @@ func (u *promUnifi) exportUDM(r report, d *unifi.UDM) { if !d.Adopted.Val || d.Locating.Val { return } + labels := []string{d.Type, d.SiteName, d.Name} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} // Shared data (all devices do this). @@ -80,6 +82,7 @@ func (u *promUnifi) exportUDM(r report, d *unifi.UDM) { {u.Device.Info, gauge, 1.0, append(labels, infoLabels...)}, {u.Device.Uptime, gauge, d.Uptime, labels}, }) + // Wireless Data - UDM (non-pro) only if d.Stat.Ap != nil && d.VapTable != nil { u.exportUAPstats(r, labels, d.Stat.Ap, d.BytesD, d.TxBytesD, d.RxBytesD, d.BytesR) @@ -103,6 +106,7 @@ func (u *promUnifi) exportSTAcount(r report, labels []string, stas ...unifi.Flex {u.Device.Counter, gauge, stas[0], append(labels, "user")}, {u.Device.Counter, gauge, stas[1], append(labels, "guest")}, }) + if len(stas) > 2 { r.send([]*metric{ {u.Device.Counter, gauge, stas[2], append(labels, "desktop")}, diff --git a/pkg/promunifi/usg.go b/pkg/promunifi/usg.go index 94a43f80..631ef358 100644 --- a/pkg/promunifi/usg.go +++ b/pkg/promunifi/usg.go @@ -72,6 +72,7 @@ func (u *promUnifi) exportUSG(r report, d *unifi.USG) { if !d.Adopted.Val || d.Locating.Val { return } + labels := []string{d.Type, d.SiteName, d.Name} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} // Gateway System Data. @@ -91,6 +92,7 @@ func (u *promUnifi) exportUSGstats(r report, labels []string, gw *unifi.Gw, st u if gw == nil { return } + labelLan := []string{"lan", labels[1], labels[2]} labelWan := []string{"all", labels[1], labels[2]} r.send([]*metric{ @@ -115,6 +117,7 @@ func (u *promUnifi) exportWANPorts(r report, labels []string, wans ...unifi.Wan) if !wan.Up.Val { continue // only record UP interfaces. } + labelWan := []string{wan.Name, labels[1], labels[2]} r.send([]*metric{ {u.USG.WanRxPackets, counter, wan.RxPackets, labelWan}, diff --git a/pkg/promunifi/usw.go b/pkg/promunifi/usw.go index fd089ea7..e89506cb 100644 --- a/pkg/promunifi/usw.go +++ b/pkg/promunifi/usw.go @@ -49,6 +49,7 @@ 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"} + 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), @@ -94,6 +95,7 @@ func (u *promUnifi) exportUSW(r report, d *unifi.USW) { if !d.Adopted.Val || d.Locating.Val { return } + labels := []string{d.Type, d.SiteName, d.Name} infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} u.exportUSWstats(r, labels, d.Stat.Sw) @@ -105,13 +107,16 @@ func (u *promUnifi) exportUSW(r report, d *unifi.USW) { {u.Device.Info, gauge, 1.0, append(labels, infoLabels...)}, {u.Device.Uptime, gauge, d.Uptime, labels}, }) + // Switch System Data. if d.HasTemperature.Val { r.send([]*metric{{u.Device.Temperature, gauge, d.GeneralTemperature, labels}}) } + if d.HasFan.Val { r.send([]*metric{{u.Device.FanLevel, gauge, d.FanLevel, labels}}) } + if d.TotalMaxPower.Txt != "" { r.send([]*metric{{u.Device.TotalMaxPower, gauge, d.TotalMaxPower, labels}}) } @@ -122,6 +127,7 @@ func (u *promUnifi) exportUSWstats(r report, labels []string, sw *unifi.Sw) { if sw == nil { return } + labelS := labels[1:] r.send([]*metric{ {u.USW.SwRxPackets, counter, sw.RxPackets, labelS}, @@ -150,8 +156,10 @@ func (u *promUnifi) exportPRTtable(r report, labels []string, pt []unifi.Port) { if !p.Up.Val || !p.Enable.Val { continue } + // Copy labels, and add four new ones. labelP := []string{labels[2] + " Port " + p.PortIdx.Txt, p.PortIdx.Txt, p.Name, p.Mac, p.IP, labels[1], labels[2]} + if p.PoeEnable.Val && p.PortPoe.Val { r.send([]*metric{ {u.USW.PoeCurrent, gauge, p.PoeCurrent, labelP},