From 3a84c97270db1a2b67cc3c7a62afa2132cd44f58 Mon Sep 17 00:00:00 2001 From: davidnewhall2 Date: Fri, 20 Dec 2019 02:44:53 -0800 Subject: [PATCH] fixes --- integrations/promunifi/examples/MANUAL.md | 52 +------- .../promunifi/examples/up.conf.example | 122 ++++++++++-------- .../promunifi/examples/up.json.example | 20 ++- .../promunifi/examples/up.xml.example | 21 ++- .../promunifi/examples/up.yaml.example | 21 ++- .../promunifi/pkg/inputunifi/collector.go | 66 +++++----- .../promunifi/pkg/inputunifi/input.go | 17 ++- .../promunifi/pkg/inputunifi/interface.go | 50 ++++--- integrations/promunifi/pkg/poller/dumper.go | 15 ++- integrations/promunifi/pkg/poller/inputs.go | 62 ++++++--- .../promunifi/pkg/promunifi/collector.go | 112 ++++++++-------- .../promunifi/pkg/promunifi/report.go | 2 +- 12 files changed, 313 insertions(+), 247 deletions(-) diff --git a/integrations/promunifi/examples/MANUAL.md b/integrations/promunifi/examples/MANUAL.md index cabf4db8..d12c9ed3 100644 --- a/integrations/promunifi/examples/MANUAL.md +++ b/integrations/promunifi/examples/MANUAL.md @@ -65,56 +65,16 @@ is provided so the application can be easily adapted to any environment. `Config File Parameters` -Additional parameters are added by output packages. Parameters can also be set -using environment variables. See the GitHub wiki for more information! +Configuration file (up.conf) parameters are documented in the wiki. - >>> POLLER FIELDS FOLLOW - you may have multiple controllers: +* [https://github.com/davidnewhall/unifi-poller/wiki/Configuration](https://github.com/davidnewhall/unifi-poller/wiki/Configuration) - debug default: false - This turns on time stamps and line numbers in logs, outputs a few extra - lines of information while processing. +`Shell Environment Parameters` - quiet default: false - Setting this to true will turn off per-device and per-interval logs. Only - errors will be logged. Using this with debug=true adds line numbers to - any error logs. +This application can be fully configured using shell environment variables. +Find documentation for this feature on the Docker Wiki page. - >>> UNIFI CONTROLLER FIELDS FOLLOW - you may have multiple controllers: - - sites default: ["all"] - This list of strings should represent the names of sites on the UniFi - controller that will be polled for data. Pass `all` in the list to - poll all sites. On startup, the application prints out all site names - found in the controller; they're cryptic, but they have the human-name - next to them. The cryptic names go into the config file `sites` list. - The controller's first site is not cryptic and is named `default`. - - url default: https://127.0.0.1:8443 - This is the URL where the UniFi Controller is available. - - user default: influxdb - Username used to authenticate with UniFi controller. This should be a - special service account created on the control with read-only access. - - pass no default - Password used to authenticate with UniFi controller. This can also be - set in an environment variable instead of a configuration file. - - save_ids default: false - Setting this parameter to true will enable collection of Intrusion - Detection System data. IDS and IPS are the same data set. This is off - by default because most controllers do not have this enabled. It also - creates a lot of new metrics from controllers with a lot of IDS entries. - IDS data does not contain metrics, so this doesn't work with Prometheus. - - save_sites default: true - Setting this parameter to false will disable saving Network Site data. - This data populates the Sites dashboard, and this setting affects influx - and prometheus. - - verify_ssl default: false - If your UniFi controller has a valid SSL certificate, you can enable - this option to validate it. Otherwise, any SSL certificate is valid. +* [https://github.com/davidnewhall/unifi-poller/wiki/Docker](https://github.com/davidnewhall/unifi-poller/wiki/Docker) GO DURATION --- diff --git a/integrations/promunifi/examples/up.conf.example b/integrations/promunifi/examples/up.conf.example index 43aa9831..942c53cd 100644 --- a/integrations/promunifi/examples/up.conf.example +++ b/integrations/promunifi/examples/up.conf.example @@ -1,75 +1,95 @@ # UniFi Poller primary configuration file. TOML FORMAT # -# commented lines are defaults, uncomment to change. # ######################################################## - [poller] -# Turns on line numbers, microsecond logging, and a per-device log. -# The default is false, but I personally leave this on at home (four devices). -# This may be noisy if you have a lot of devices. It adds one line per device. -debug = false + # Turns on line numbers, microsecond logging, and a per-device log. + # The default is false, but I personally leave this on at home (four devices). + # This may be noisy if you have a lot of devices. It adds one line per device. + debug = false -# Turns off per-interval logs. Only startup and error logs will be emitted. -# Recommend enabling debug with this setting for better error logging. -quiet = false + # Turns off per-interval logs. Only startup and error logs will be emitted. + # Recommend enabling debug with this setting for better error logging. + quiet = false -# Load dynamic plugins. Advanced use; only sample mysql plugin provided by default. -plugins = [] + # Load dynamic plugins. Advanced use; only sample mysql plugin provided by default. + plugins = [] #### OUTPUTS + # If you don't use an output, you can disable it. + [prometheus] -disable = false -# This controls on which ip and port /metrics is exported when mode is "prometheus". -# This has no effect in other modes. Must contain a colon and port. -http_listen = "0.0.0.0:9130" -report_errors = false + disable = false + # This controls on which ip and port /metrics is exported when mode is "prometheus". + # This has no effect in other modes. Must contain a colon and port. + http_listen = "0.0.0.0:9130" + report_errors = false [influxdb] -disable = false -# InfluxDB does not require auth by default, so the user/password are probably unimportant. -url = "http://127.0.0.1:8086" -user = "unifi" -pass = "unifi" -# Be sure to create this database. -db = "unifi" -# If your InfluxDB uses a valid SSL cert, set this to true. -verify_ssl = false -# The UniFi Controller only updates traffic stats about every 30 seconds. -# Setting this to something lower may lead to "zeros" in your data. -# If you're getting zeros now, set this to "1m" -interval = "30s" + disable = false + # InfluxDB does not require auth by default, so the user/password are probably unimportant. + url = "http://127.0.0.1:8086" + user = "unifipoller" + pass = "unifipoller" + # Be sure to create this database. + db = "unifi" + # If your InfluxDB uses a valid SSL cert, set this to true. + verify_ssl = false + # The UniFi Controller only updates traffic stats about every 30 seconds. + # Setting this to something lower may lead to "zeros" in your data. + # If you're getting zeros now, set this to "1m" + interval = "30s" #### INPUTS [unifi] -disable = false + # Setting this to true and providing default credentials allows you to skip + # configuring controllers in this config file. Instead you configure them in + # your prometheus.yml config. Prometheus then sends the controller URL to + # unifi-poller when it performs the scrape. This is useful if you have many, + # or changing controllers. Most people can leave this off. See wiki for more. + dynamic = false + +# The following section contains the default credentials/configuration for any +# dynamic controller (see above section), or the primary controller if you do not +# provide one and dynamic is disabled. In other words, you can just add your +# controller here and delete the following section. Either works. +[unifi.defaults] + name = "https://127.0.0.1:8443" + url = "https://127.0.0.1:8443" + user = "unifipoller" + pass = "unifipoller" + sites = ["all"] + save_ids = false + save_sites = true + verify_ssl = false # You may repeat the following section to poll additional controllers. [[unifi.controller]] -# Friendly name used in dashboards. Uses URL if left empty. -name = "" + # Friendly name used in dashboards. Uses URL if left empty; which is fine. + # Avoid changing this later because it will live forever in your database. + name = "" -url = "https://127.0.0.1:8443" -# Make a read-only user in the UniFi Admin Settings. -user = "influx" -# You may also set env variable UNIFI_PASSWORD instead of putting this in the config. -pass = "4BB9345C-2341-48D7-99F5-E01B583FF77F" + url = "https://127.0.0.1:8443" + # Make a read-only user in the UniFi Admin Settings. + user = "unifipoller" + # You may also set env variable UNIFI_PASSWORD instead of putting this in the config. + pass = "4BB9345C-2341-48D7-99F5-E01B583FF77F" -# If the controller has more than one site, specify which sites to poll here. -# Set this to ["default"] to poll only the first site on the controller. -# A setting of ["all"] will poll all sites; this works if you only have 1 site too. -sites = ["all"] + # If the controller has more than one site, specify which sites to poll here. + # Set this to ["default"] to poll only the first site on the controller. + # A setting of ["all"] will poll all sites; this works if you only have 1 site too. + sites = ["all"] -# Enable collection of Intrusion Detection System Data (InfluxDB only). -# Only useful if IDS or IPS are enabled on one of the sites. -save_ids = false + # Enable collection of Intrusion Detection System Data (InfluxDB only). + # Only useful if IDS or IPS are enabled on one of the sites. + save_ids = false -# Enable collection of site data. This data powers the Network Sites dashboard. -# It's not valuable to everyone and setting this to false will save resources. -save_sites = true + # Enable collection of site data. This data powers the Network Sites dashboard. + # It's not valuable to everyone and setting this to false will save resources. + save_sites = true -# If your UniFi controller has a valid SSL certificate (like lets encrypt), -# you can enable this option to validate it. Otherwise, any SSL certificate is -# valid. If you don't know if you have a valid SSL cert, then you don't have one. -verify_ssl = false + # If your UniFi controller has a valid SSL certificate (like lets encrypt), + # you can enable this option to validate it. Otherwise, any SSL certificate is + # valid. If you don't know if you have a valid SSL cert, then you don't have one. + verify_ssl = false diff --git a/integrations/promunifi/examples/up.json.example b/integrations/promunifi/examples/up.json.example index 12ba4a4f..a4e0a401 100644 --- a/integrations/promunifi/examples/up.json.example +++ b/integrations/promunifi/examples/up.json.example @@ -14,20 +14,30 @@ "influxdb": { "disable": false, "url": "http://127.0.0.1:8086", - "user": "unifi", - "pass": "unifi", + "user": "unifipoller", + "pass": "unifipoller", "db": "unifi", "verify_ssl": false, "interval": "30s" }, "unifi": { - "disable": false, + "dynamic": false, + "defaults": { + "name": "https://127.0.0.1:8443", + "user": "unifipoller", + "pass": "unifipoller", + "url": "https://127.0.0.1:8443", + "sites": ["all"], + "save_ids": false, + "save_sites": true, + "verify_ssl": false + }, "controllers": [ { "name": "", - "user": "influx", - "pass": "", + "user": "unifipoller", + "pass": "unifipoller", "url": "https://127.0.0.1:8443", "sites": ["all"], "save_ids": false, diff --git a/integrations/promunifi/examples/up.xml.example b/integrations/promunifi/examples/up.xml.example index 14b2aa09..094f8347 100644 --- a/integrations/promunifi/examples/up.xml.example +++ b/integrations/promunifi/examples/up.xml.example @@ -18,22 +18,33 @@ 30s http://127.0.0.1:8086 - unifi - unifi + unifipoller + unifipoller unifi false - + + + all + unifipoller + unifipoller + https://127.0.0.1:8443 + false + false + true + + all - influx - + unifipoller + unifipoller https://127.0.0.1:8443 false false true + diff --git a/integrations/promunifi/examples/up.yaml.example b/integrations/promunifi/examples/up.yaml.example index bb8a4aa1..edf08506 100644 --- a/integrations/promunifi/examples/up.yaml.example +++ b/integrations/promunifi/examples/up.yaml.example @@ -18,17 +18,28 @@ influxdb: disable: false interval: "30s" url: "http://127.0.0.1:8086" - user: "unifi" - pass: "unifi" + user: "unifipoller" + pass: "unifipoller" db: "unifi" verify_ssl: false unifi: - disable: false + dynamic: false + defaults: + name: "https://127.0.0.1:8443" + user: "unifipoller" + pass: "unifipoller" + url: "https://127.0.0.1:8443" + sites: + - all + verify_ssl: false + save_ids: false + save_sites: true + controllers: - name: "" - user: "influx" - pass: "" + user: "unifipoller" + pass: "unifipoller" url: "https://127.0.0.1:8443" sites: - all diff --git a/integrations/promunifi/pkg/inputunifi/collector.go b/integrations/promunifi/pkg/inputunifi/collector.go index 1539f244..90f1feb4 100644 --- a/integrations/promunifi/pkg/inputunifi/collector.go +++ b/integrations/promunifi/pkg/inputunifi/collector.go @@ -2,6 +2,7 @@ package inputunifi import ( "fmt" + "strings" "time" "github.com/davidnewhall/unifi-poller/pkg/poller" @@ -15,47 +16,43 @@ func (u *InputUnifi) isNill(c *Controller) bool { return c.Unifi == nil } -func (u *InputUnifi) dynamicController(url string) (*poller.Metrics, bool, error) { - c := u.Config.Default // copy defaults into new controller +// newDynamicCntrlr creates and saves a controller (with auth cookie) for repeated use. +// This is called when an unconfigured controller is requested. +func (u *InputUnifi) newDynamicCntrlr(url string) (bool, *Controller) { + u.Lock() + defer u.Unlock() + + c := u.dynamic[url] + if c != nil { + // it already exists. + return false, c + } + + ccopy := u.Config.Default // copy defaults into new controller + c = &ccopy + u.dynamic[url] = c c.Name = url c.URL = url - u.Logf("Authenticating to Dynamic UniFi Controller: %s", url) - - if err := u.getUnifi(&c); err != nil { - return nil, false, fmt.Errorf("authenticating to %s: %v", url, err) - } - - metrics := &poller.Metrics{} - ok, err := u.appendController(&c, metrics) - - return metrics, ok, err + return true, c } -func (u *InputUnifi) appendController(c *Controller, metrics *poller.Metrics) (bool, error) { - m, err := u.collectController(c) - if err != nil || m == nil { - return false, err +func (u *InputUnifi) dynamicController(url string) (*poller.Metrics, error) { + if !strings.HasPrefix(url, "http") { + return nil, fmt.Errorf("scrape filter match failed, and filter is not http URL") } - metrics.Sites = append(metrics.Sites, m.Sites...) - metrics.Clients = append(metrics.Clients, m.Clients...) - metrics.IDSList = append(metrics.IDSList, m.IDSList...) + new, c := u.newDynamicCntrlr(url) - if m.Devices == nil { - return true, nil + if new { + u.Logf("Authenticating to Dynamic UniFi Controller: %s", url) + + if err := u.getUnifi(c); err != nil { + return nil, fmt.Errorf("authenticating to %s: %v", url, err) + } } - 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...) - - return true, nil + return u.collectController(c) } func (u *InputUnifi) collectController(c *Controller) (*poller.Metrics, error) { @@ -67,11 +64,6 @@ func (u *InputUnifi) collectController(c *Controller) (*poller.Metrics, error) { } } - m, err := u.pollController(c) - if err == nil { - return m, nil - } - return u.pollController(c) } @@ -146,7 +138,7 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *poller.Metrics) *pol metrics.Clients[i].RadioDescription = bssdIDs[metrics.Clients[i].Bssid] + metrics.Clients[i].RadioProto } - if !c.SaveSites { + if !*c.SaveSites { metrics.Sites = nil } diff --git a/integrations/promunifi/pkg/inputunifi/input.go b/integrations/promunifi/pkg/inputunifi/input.go index 4a2881aa..a540964d 100644 --- a/integrations/promunifi/pkg/inputunifi/input.go +++ b/integrations/promunifi/pkg/inputunifi/input.go @@ -16,13 +16,15 @@ import ( const ( defaultURL = "https://127.0.0.1:8443" defaultUser = "unifipoller" - defaultPass = "unifipollerp4$$w0rd" + defaultPass = "unifipoller" defaultSite = "all" ) // InputUnifi contains the running data. type InputUnifi struct { - Config *Config `json:"unifi" toml:"unifi" xml:"unifi" yaml:"unifi"` + Config *Config `json:"unifi" toml:"unifi" xml:"unifi" yaml:"unifi"` + dynamic map[string]*Controller + sync.Mutex // to lock the map above. poller.Logger } @@ -31,7 +33,7 @@ type InputUnifi struct { 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"` + 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"` @@ -44,8 +46,8 @@ type Controller struct { type Config struct { sync.RWMutex // locks the Unifi struct member when re-authing to unifi. Default Controller `json:"defaults" toml:"defaults" xml:"default" yaml:"defaults"` - Disable bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"` - Dynamic bool `json:"dynamic" toml:"dynamic" xml:"dynamic" yaml:"dynamic"` + Disable bool `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"` + Dynamic bool `json:"dynamic" toml:"dynamic" xml:"dynamic,attr" yaml:"dynamic"` Controllers []*Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"` } @@ -157,6 +159,11 @@ func (u *InputUnifi) dumpSitesJSON(c *Controller, path, name string, sites unifi } func (u *InputUnifi) setDefaults(c *Controller) { + if c.SaveSites == nil { + t := true + c.SaveSites = &t + } + if c.URL == "" { c.URL = defaultURL } diff --git a/integrations/promunifi/pkg/inputunifi/interface.go b/integrations/promunifi/pkg/inputunifi/interface.go index 67f53cce..42745687 100644 --- a/integrations/promunifi/pkg/inputunifi/interface.go +++ b/integrations/promunifi/pkg/inputunifi/interface.go @@ -15,15 +15,20 @@ import ( // Satisfies poller.Input interface. func (u *InputUnifi) Initialize(l poller.Logger) error { if u.Config.Disable { - l.Logf("unifi input disabled") + l.Logf("UniFi input plugin disabled!") return nil } - if u.setDefaults(&u.Config.Default); len(u.Config.Controllers) < 1 { + if u.setDefaults(&u.Config.Default); len(u.Config.Controllers) < 1 && !u.Config.Dynamic { new := u.Config.Default // copy defaults. u.Config.Controllers = []*Controller{&new} } + if len(u.Config.Controllers) < 1 { + l.Logf("No controllers configured. Polling dynamic controllers only!") + } + + u.dynamic = make(map[string]*Controller) u.Logger = l for _, c := range u.Config.Controllers { @@ -35,7 +40,7 @@ func (u *InputUnifi) Initialize(l poller.Logger) error { u.LogErrorf("checking sites on %s: %v", c.Name, err) } - u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v", + u.Logf("Configured 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) @@ -52,7 +57,7 @@ func (u *InputUnifi) Metrics() (*poller.Metrics, bool, error) { // MetricsFrom grabs all the measurements from a UniFi controller and returns them. func (u *InputUnifi) MetricsFrom(filter *poller.Filter) (*poller.Metrics, bool, error) { - if u.Config.Disable || filter == nil || filter.Term == "" { + if u.Config.Disable { return nil, false, nil } @@ -62,35 +67,48 @@ func (u *InputUnifi) MetricsFrom(filter *poller.Filter) (*poller.Metrics, bool, // Check if the request is for an existing, configured controller. for _, c := range u.Config.Controllers { - if !strings.EqualFold(c.Name, filter.Term) { + if filter != nil && !strings.EqualFold(c.Name, filter.Term) { continue } - exists, err := u.appendController(c, metrics) + m, err := u.collectController(c) if err != nil { errs = append(errs, err.Error()) } - if exists { - ok = true + if m == nil { + continue } + + ok = true + metrics = poller.AppendMetrics(metrics, m) } if len(errs) > 0 { return metrics, ok, fmt.Errorf(strings.Join(errs, ", ")) } - if u.Config.Dynamic && !ok && strings.HasPrefix(filter.Term, "http") { - // Attempt to a dynamic metrics fetch from an unconfigured controller. - return u.dynamicController(filter.Term) + if ok { + return metrics, true, nil } - return metrics, ok, nil + if filter != nil && !u.Config.Dynamic { + return metrics, false, fmt.Errorf("scrape filter match failed and dynamic lookups disabled") + } + + // Attempt a dynamic metrics fetch from an unconfigured controller. + m, err := u.dynamicController(filter.Term) + + return m, err == nil && m != nil, err } // RawMetrics returns API output from the first configured unifi controller. func (u *InputUnifi) RawMetrics(filter *poller.Filter) ([]byte, error) { - c := u.Config.Controllers[0] // We could pull the controller number from the filter. + if l := len(u.Config.Controllers); filter.Unit >= l { + return nil, fmt.Errorf("control number %d not found, %d controller(s) configured (0 index)", filter.Unit, l) + } + + c := u.Config.Controllers[filter.Unit] if u.isNill(c) { u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) @@ -108,14 +126,14 @@ func (u *InputUnifi) RawMetrics(filter *poller.Filter) ([]byte, error) { return nil, err } - switch filter.Type { + switch filter.Kind { case "d", "device", "devices": return u.dumpSitesJSON(c, unifi.APIDevicePath, "Devices", sites) case "client", "clients", "c": return u.dumpSitesJSON(c, unifi.APIClientPath, "Clients", sites) case "other", "o": - _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", filter.Term) - return c.Unifi.GetJSON(filter.Term) + _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", filter.Path) + return c.Unifi.GetJSON(filter.Path) default: return []byte{}, fmt.Errorf("must provide filter: devices, clients, other") } diff --git a/integrations/promunifi/pkg/poller/dumper.go b/integrations/promunifi/pkg/poller/dumper.go index 80c7f728..c78edabb 100644 --- a/integrations/promunifi/pkg/poller/dumper.go +++ b/integrations/promunifi/pkg/poller/dumper.go @@ -2,19 +2,24 @@ package poller import ( "fmt" + "strconv" "strings" ) -// DumpJSONPayload prints raw json from the UniFi Controller. -// This only works with controller 0 (first one) in the config. +// DumpJSONPayload prints raw json from the UniFi Controller. This is currently +// tied into the -j CLI arg, and is probably not very useful outside that context. func (u *UnifiPoller) DumpJSONPayload() (err error) { u.Config.Quiet = true - split := strings.SplitN(u.Flags.DumpJSON, " ", 2) - filter := &Filter{Type: split[0]} + filter := &Filter{Kind: split[0]} + + if split2 := strings.Split(filter.Kind, ":"); len(split2) > 1 { + filter.Kind = split2[0] + filter.Unit, _ = strconv.Atoi(split2[1]) + } if len(split) > 1 { - filter.Term = split[1] + filter.Path = split[1] } m, err := inputs[0].RawMetrics(filter) diff --git a/integrations/promunifi/pkg/poller/inputs.go b/integrations/promunifi/pkg/poller/inputs.go index 8633ca2c..a637239c 100644 --- a/integrations/promunifi/pkg/poller/inputs.go +++ b/integrations/promunifi/pkg/poller/inputs.go @@ -28,10 +28,26 @@ type InputPlugin struct { Input } -// Filter is used for raw metrics filters. +// Filter is used for metrics filters. Many fields for lots of expansion. type Filter struct { Type string Term string + Name string + Tags string + Role string + Kind string + Path string + Area int + Item int + Unit int + Sign int64 + Mass int64 + Rate float64 + Cost float64 + Free bool + True bool + Done bool + Stop bool } // NewInput creates a metric input. This should be called by input plugins @@ -107,14 +123,14 @@ func (u *UnifiPoller) Metrics() (*Metrics, bool, error) { return metrics, ok, err } -// MetricsFrom aggregates all the measurements from all configured inputs and returns them. +// MetricsFrom aggregates all the measurements from filtered inputs and returns them. func (u *UnifiPoller) MetricsFrom(filter *Filter) (*Metrics, bool, error) { errs := []string{} metrics := &Metrics{} ok := false for _, input := range inputs { - if input.Name != filter.Type { + if !strings.EqualFold(input.Name, filter.Name) { continue } @@ -128,23 +144,7 @@ func (u *UnifiPoller) MetricsFrom(filter *Filter) (*Metrics, bool, error) { } ok = true - - 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...) + metrics = AppendMetrics(metrics, m) } var err error @@ -155,3 +155,25 @@ func (u *UnifiPoller) MetricsFrom(filter *Filter) (*Metrics, bool, error) { return metrics, ok, err } + +// AppendMetrics combined the metrics from two sources. +func AppendMetrics(existing *Metrics, m *Metrics) *Metrics { + existing.Sites = append(existing.Sites, m.Sites...) + existing.Clients = append(existing.Clients, m.Clients...) + existing.IDSList = append(existing.IDSList, m.IDSList...) + + if m.Devices == nil { + return existing + } + + if existing.Devices == nil { + existing.Devices = &unifi.Devices{} + } + + existing.UAPs = append(existing.UAPs, m.UAPs...) + existing.USGs = append(existing.USGs, m.USGs...) + existing.USWs = append(existing.USWs, m.USWs...) + existing.UDMs = append(existing.UDMs, m.UDMs...) + + return existing +} diff --git a/integrations/promunifi/pkg/promunifi/collector.go b/integrations/promunifi/pkg/promunifi/collector.go index e4cf1e9c..bbfbda25 100644 --- a/integrations/promunifi/pkg/promunifi/collector.go +++ b/integrations/promunifi/pkg/promunifi/collector.go @@ -26,23 +26,18 @@ const ( ) type promUnifi struct { - *Prometheus - Client *uclient - Device *unifiDevice - UAP *uap - USG *usg - USW *usw - Site *site + *Config `json:"prometheus" toml:"prometheus" xml:"prometheus" yaml:"prometheus"` + Client *uclient + Device *unifiDevice + UAP *uap + USG *usg + USW *usw + Site *site // This interface is passed to the Collect() method. The Collect method uses // this interface to retrieve the latest UniFi measurements and export them. Collector poller.Collect } -// Prometheus allows the data to be nested in the config file. -type Prometheus struct { - Config Config `json:"prometheus" toml:"prometheus" xml:"prometheus" yaml:"prometheus"` -} - // Config is the input (config file) data used to initialize this output plugin. type Config struct { // If non-empty, each of the collected metrics is prefixed by the @@ -66,7 +61,7 @@ type metric struct { // Report accumulates counters that are printed to a log line. type Report struct { - Config + *Config Total int // Total count of metrics recorded. Errors int // Total count of errors recording metrics. Zeros int // Total count of metrics equal to zero. @@ -78,17 +73,18 @@ type Report struct { wg sync.WaitGroup } +// target is used for targeted (sometimes dynamic) metrics scrapes. type target struct { *poller.Filter - *promUnifi + u *promUnifi } func init() { - u := &promUnifi{Prometheus: &Prometheus{}} + u := &promUnifi{Config: &Config{}} poller.NewOutput(&poller.Output{ Name: "prometheus", - Config: u.Prometheus, + Config: u, Method: u.Run, }) } @@ -96,51 +92,55 @@ func init() { // Run creates the collectors and starts the web server up. // Should be run in a Go routine. Returns nil if not configured. func (u *promUnifi) Run(c poller.Collect) error { - if u.Config.Disable { + if u.Disable { 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.Namespace = strings.Trim(strings.Replace(u.Namespace, "-", "_", -1), "_") + if u.Namespace == "" { + u.Namespace = strings.Replace(poller.AppName, "-", "", -1) } - if u.Config.HTTPListen == "" { - u.Config.HTTPListen = defaultHTTPListen + if u.HTTPListen == "" { + u.HTTPListen = defaultHTTPListen } + // Later can pass this in from poller by adding a method to the interface. + u.Collector = c + u.Client = descClient(u.Namespace + "_client_") + u.Device = descDevice(u.Namespace + "_device_") // stats for all device types. + u.UAP = descUAP(u.Namespace + "_device_") + u.USG = descUSG(u.Namespace + "_device_") + u.USW = descUSW(u.Namespace + "_device_") + u.Site = descSite(u.Namespace + "_site_") mux := http.NewServeMux() - prometheus.MustRegister(version.NewCollector(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_"), - }) - c.Logf("Exporting Measurements for Prometheus at https://%s/metrics, namespace: %s", - u.Config.HTTPListen, u.Config.Namespace) - mux.Handle("/metrics", promhttp.HandlerFor( - prometheus.DefaultGatherer, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}, + prometheus.MustRegister(version.NewCollector(u.Namespace)) + prometheus.MustRegister(u) + c.Logf("Prometheus exported at https://%s/ - namespace: %s", u.HTTPListen, u.Namespace) + mux.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, + promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}, )) mux.HandleFunc("/scrape", u.ScrapeHandler) + mux.HandleFunc("/", u.DefaultHandler) - return http.ListenAndServe(u.Config.HTTPListen, mux) + return http.ListenAndServe(u.HTTPListen, mux) } // ScrapeHandler allows prometheus to scrape a single source, instead of all sources. func (u *promUnifi) ScrapeHandler(w http.ResponseWriter, r *http.Request) { - t := &target{promUnifi: u, Filter: &poller.Filter{}} - if t.Filter.Type = r.URL.Query().Get("input"); t.Filter.Type == "" { + t := &target{u: u, Filter: &poller.Filter{}} + if t.Name = r.URL.Query().Get("input"); t.Name == "" { + u.Collector.LogErrorf("input parameter missing on scrape from %v", r.RemoteAddr) http.Error(w, `'input' parameter must be specified (try "unifi")`, 400) + return } - if t.Filter.Term = r.URL.Query().Get("target"); t.Filter.Term == "" { + if t.Term = r.URL.Query().Get("target"); t.Term == "" { + u.Collector.LogErrorf("target parameter missing on scrape from %v", r.RemoteAddr) http.Error(w, "'target' parameter must be specified, configured name, or unconfigured url", 400) + return } @@ -152,14 +152,15 @@ func (u *promUnifi) ScrapeHandler(w http.ResponseWriter, r *http.Request) { ).ServeHTTP(w, r) } +func (u *promUnifi) DefaultHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + _, _ = w.Write([]byte(poller.AppName + "\n")) +} + // Describe satisfies the prometheus Collector. This returns all of the // metric descriptions that this packages produces. func (t *target) Describe(ch chan<- *prometheus.Desc) { - t.promUnifi.Describe(ch) -} - -func (t *target) Collect(ch chan<- prometheus.Metric) { - t.promUnifi.collect(ch, t.Filter) + t.u.Describe(ch) } // Describe satisfies the prometheus Collector. This returns all of the @@ -178,6 +179,11 @@ func (u *promUnifi) Describe(ch chan<- *prometheus.Desc) { } } +// Collect satisfies the prometheus Collector. This runs for a single controller poll. +func (t *target) Collect(ch chan<- prometheus.Metric) { + t.u.collect(ch, t.Filter) +} + // Collect satisfies the prometheus Collector. This runs the input method to get // the current metrics (from another package) then exports them for prometheus. func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { @@ -187,27 +193,31 @@ func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { func (u *promUnifi) collect(ch chan<- prometheus.Metric, filter *poller.Filter) { var err error - ok := false - - r := &Report{Config: u.Config, ch: make(chan []*metric, buffer), Start: time.Now()} + r := &Report{ + Config: u.Config, + ch: make(chan []*metric, buffer), + Start: time.Now()} defer r.close() + ok := false + if filter == nil { r.Metrics, ok, err = u.Collector.Metrics() } else { r.Metrics, ok, err = u.Collector.MetricsFrom(filter) } + r.Fetch = time.Since(r.Start) + if err != nil { - r.error(ch, prometheus.NewInvalidDesc(fmt.Errorf("metric fetch failed")), err) + r.error(ch, prometheus.NewInvalidDesc(err), fmt.Errorf("metric fetch failed")) + u.Collector.LogErrorf("metric fetch failed: %v", err) if !ok { return } } - r.Fetch = time.Since(r.Start) - if r.Metrics.Devices == nil { r.Metrics.Devices = &unifi.Devices{} } diff --git a/integrations/promunifi/pkg/promunifi/report.go b/integrations/promunifi/pkg/promunifi/report.go index 9b6df74c..3eb66638 100644 --- a/integrations/promunifi/pkg/promunifi/report.go +++ b/integrations/promunifi/pkg/promunifi/report.go @@ -67,7 +67,7 @@ func (r *Report) export(m *metric, v float64) prometheus.Metric { func (r *Report) error(ch chan<- prometheus.Metric, d *prometheus.Desc, v interface{}) { r.Errors++ - if r.Config.ReportErrors { + if r.ReportErrors { ch <- prometheus.NewInvalidMetric(d, fmt.Errorf("error: %v", v)) } }