This commit is contained in:
davidnewhall2 2019-12-20 02:44:53 -08:00
parent f12d31c019
commit 3a84c97270
12 changed files with 313 additions and 247 deletions

View File

@ -65,56 +65,16 @@ is provided so the application can be easily adapted to any environment.
`Config File Parameters` `Config File Parameters`
Additional parameters are added by output packages. Parameters can also be set Configuration file (up.conf) parameters are documented in the wiki.
using environment variables. See the GitHub wiki for more information!
>>> 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 `Shell Environment Parameters`
This turns on time stamps and line numbers in logs, outputs a few extra
lines of information while processing.
quiet default: false This application can be fully configured using shell environment variables.
Setting this to true will turn off per-device and per-interval logs. Only Find documentation for this feature on the Docker Wiki page.
errors will be logged. Using this with debug=true adds line numbers to
any error logs.
>>> UNIFI CONTROLLER FIELDS FOLLOW - you may have multiple controllers: * [https://github.com/davidnewhall/unifi-poller/wiki/Docker](https://github.com/davidnewhall/unifi-poller/wiki/Docker)
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.
GO DURATION GO DURATION
--- ---

View File

@ -1,75 +1,95 @@
# UniFi Poller primary configuration file. TOML FORMAT # # UniFi Poller primary configuration file. TOML FORMAT #
# commented lines are defaults, uncomment to change. #
######################################################## ########################################################
[poller] [poller]
# Turns on line numbers, microsecond logging, and a per-device log. # 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). # 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. # This may be noisy if you have a lot of devices. It adds one line per device.
debug = false debug = false
# Turns off per-interval logs. Only startup and error logs will be emitted. # Turns off per-interval logs. Only startup and error logs will be emitted.
# Recommend enabling debug with this setting for better error logging. # Recommend enabling debug with this setting for better error logging.
quiet = false quiet = false
# Load dynamic plugins. Advanced use; only sample mysql plugin provided by default. # Load dynamic plugins. Advanced use; only sample mysql plugin provided by default.
plugins = [] plugins = []
#### OUTPUTS #### OUTPUTS
# If you don't use an output, you can disable it.
[prometheus] [prometheus]
disable = false disable = false
# This controls on which ip and port /metrics is exported when mode is "prometheus". # 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. # This has no effect in other modes. Must contain a colon and port.
http_listen = "0.0.0.0:9130" http_listen = "0.0.0.0:9130"
report_errors = false report_errors = false
[influxdb] [influxdb]
disable = false disable = false
# InfluxDB does not require auth by default, so the user/password are probably unimportant. # InfluxDB does not require auth by default, so the user/password are probably unimportant.
url = "http://127.0.0.1:8086" url = "http://127.0.0.1:8086"
user = "unifi" user = "unifipoller"
pass = "unifi" pass = "unifipoller"
# Be sure to create this database. # Be sure to create this database.
db = "unifi" db = "unifi"
# If your InfluxDB uses a valid SSL cert, set this to true. # If your InfluxDB uses a valid SSL cert, set this to true.
verify_ssl = false verify_ssl = false
# The UniFi Controller only updates traffic stats about every 30 seconds. # The UniFi Controller only updates traffic stats about every 30 seconds.
# Setting this to something lower may lead to "zeros" in your data. # Setting this to something lower may lead to "zeros" in your data.
# If you're getting zeros now, set this to "1m" # If you're getting zeros now, set this to "1m"
interval = "30s" interval = "30s"
#### INPUTS #### INPUTS
[unifi] [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. # You may repeat the following section to poll additional controllers.
[[unifi.controller]] [[unifi.controller]]
# Friendly name used in dashboards. Uses URL if left empty. # Friendly name used in dashboards. Uses URL if left empty; which is fine.
name = "" # Avoid changing this later because it will live forever in your database.
name = ""
url = "https://127.0.0.1:8443" url = "https://127.0.0.1:8443"
# Make a read-only user in the UniFi Admin Settings. # Make a read-only user in the UniFi Admin Settings.
user = "influx" user = "unifipoller"
# You may also set env variable UNIFI_PASSWORD instead of putting this in the config. # You may also set env variable UNIFI_PASSWORD instead of putting this in the config.
pass = "4BB9345C-2341-48D7-99F5-E01B583FF77F" pass = "4BB9345C-2341-48D7-99F5-E01B583FF77F"
# If the controller has more than one site, specify which sites to poll here. # 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. # 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. # A setting of ["all"] will poll all sites; this works if you only have 1 site too.
sites = ["all"] sites = ["all"]
# Enable collection of Intrusion Detection System Data (InfluxDB only). # Enable collection of Intrusion Detection System Data (InfluxDB only).
# Only useful if IDS or IPS are enabled on one of the sites. # Only useful if IDS or IPS are enabled on one of the sites.
save_ids = false save_ids = false
# Enable collection of site data. This data powers the Network Sites dashboard. # 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. # It's not valuable to everyone and setting this to false will save resources.
save_sites = true save_sites = true
# If your UniFi controller has a valid SSL certificate (like lets encrypt), # 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 # 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. # valid. If you don't know if you have a valid SSL cert, then you don't have one.
verify_ssl = false verify_ssl = false

View File

@ -14,20 +14,30 @@
"influxdb": { "influxdb": {
"disable": false, "disable": false,
"url": "http://127.0.0.1:8086", "url": "http://127.0.0.1:8086",
"user": "unifi", "user": "unifipoller",
"pass": "unifi", "pass": "unifipoller",
"db": "unifi", "db": "unifi",
"verify_ssl": false, "verify_ssl": false,
"interval": "30s" "interval": "30s"
}, },
"unifi": { "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": [ "controllers": [
{ {
"name": "", "name": "",
"user": "influx", "user": "unifipoller",
"pass": "", "pass": "unifipoller",
"url": "https://127.0.0.1:8443", "url": "https://127.0.0.1:8443",
"sites": ["all"], "sites": ["all"],
"save_ids": false, "save_ids": false,

View File

@ -18,22 +18,33 @@
<influxdb disable="false"> <influxdb disable="false">
<interval>30s</interval> <interval>30s</interval>
<url>http://127.0.0.1:8086</url> <url>http://127.0.0.1:8086</url>
<user>unifi</user> <user>unifipoller</user>
<pass>unifi</pass> <pass>unifipoller</pass>
<db>unifi</db> <db>unifi</db>
<verify_ssl>false</verify_ssl> <verify_ssl>false</verify_ssl>
</influxdb> </influxdb>
<unifi disable="false"> <unifi dynamic="false">
<default name="https://127.0.0.1:8443">
<site>all</site>
<user>unifipoller</user>
<pass>unifipoller</pass>
<url>https://127.0.0.1:8443</url>
<verify_ssl>false</verify_ssl>
<save_ids>false</save_ids>
<save_sites>true</save_sites>
</default>
<!-- Repeat this stanza to poll additional controllers. --> <!-- Repeat this stanza to poll additional controllers. -->
<controller name=""> <controller name="">
<site>all</site> <site>all</site>
<user>influx</user> <user>unifipoller</user>
<pass></pass> <pass>unifipoller</pass>
<url>https://127.0.0.1:8443</url> <url>https://127.0.0.1:8443</url>
<verify_ssl>false</verify_ssl> <verify_ssl>false</verify_ssl>
<save_ids>false</save_ids> <save_ids>false</save_ids>
<save_sites>true</save_sites> <save_sites>true</save_sites>
</controller> </controller>
</unifi> </unifi>
</poller> </poller>

View File

@ -18,17 +18,28 @@ influxdb:
disable: false disable: false
interval: "30s" interval: "30s"
url: "http://127.0.0.1:8086" url: "http://127.0.0.1:8086"
user: "unifi" user: "unifipoller"
pass: "unifi" pass: "unifipoller"
db: "unifi" db: "unifi"
verify_ssl: false verify_ssl: false
unifi: 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: controllers:
- name: "" - name: ""
user: "influx" user: "unifipoller"
pass: "" pass: "unifipoller"
url: "https://127.0.0.1:8443" url: "https://127.0.0.1:8443"
sites: sites:
- all - all

View File

@ -2,6 +2,7 @@ package inputunifi
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/davidnewhall/unifi-poller/pkg/poller" "github.com/davidnewhall/unifi-poller/pkg/poller"
@ -15,47 +16,43 @@ func (u *InputUnifi) isNill(c *Controller) bool {
return c.Unifi == nil return c.Unifi == nil
} }
func (u *InputUnifi) dynamicController(url string) (*poller.Metrics, bool, error) { // newDynamicCntrlr creates and saves a controller (with auth cookie) for repeated use.
c := u.Config.Default // copy defaults into new controller // 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.Name = url
c.URL = url c.URL = url
u.Logf("Authenticating to Dynamic UniFi Controller: %s", url) return true, c
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
} }
func (u *InputUnifi) appendController(c *Controller, metrics *poller.Metrics) (bool, error) { func (u *InputUnifi) dynamicController(url string) (*poller.Metrics, error) {
m, err := u.collectController(c) if !strings.HasPrefix(url, "http") {
if err != nil || m == nil { return nil, fmt.Errorf("scrape filter match failed, and filter is not http URL")
return false, err
} }
metrics.Sites = append(metrics.Sites, m.Sites...) new, c := u.newDynamicCntrlr(url)
metrics.Clients = append(metrics.Clients, m.Clients...)
metrics.IDSList = append(metrics.IDSList, m.IDSList...)
if m.Devices == nil { if new {
return true, nil 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 { return u.collectController(c)
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
} }
func (u *InputUnifi) collectController(c *Controller) (*poller.Metrics, error) { 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) 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 metrics.Clients[i].RadioDescription = bssdIDs[metrics.Clients[i].Bssid] + metrics.Clients[i].RadioProto
} }
if !c.SaveSites { if !*c.SaveSites {
metrics.Sites = nil metrics.Sites = nil
} }

View File

@ -16,13 +16,15 @@ import (
const ( const (
defaultURL = "https://127.0.0.1:8443" defaultURL = "https://127.0.0.1:8443"
defaultUser = "unifipoller" defaultUser = "unifipoller"
defaultPass = "unifipollerp4$$w0rd" defaultPass = "unifipoller"
defaultSite = "all" defaultSite = "all"
) )
// InputUnifi contains the running data. // InputUnifi contains the running data.
type InputUnifi struct { 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 poller.Logger
} }
@ -31,7 +33,7 @@ type InputUnifi struct {
type Controller struct { type Controller struct {
VerifySSL bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` 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"` 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"` Name string `json:"name" toml:"name" xml:"name,attr" yaml:"name"`
User string `json:"user" toml:"user" xml:"user" yaml:"user"` User string `json:"user" toml:"user" xml:"user" yaml:"user"`
Pass string `json:"pass" toml:"pass" xml:"pass" yaml:"pass"` Pass string `json:"pass" toml:"pass" xml:"pass" yaml:"pass"`
@ -44,8 +46,8 @@ type Controller struct {
type Config struct { type Config struct {
sync.RWMutex // locks the Unifi struct member when re-authing to unifi. sync.RWMutex // locks the Unifi struct member when re-authing to unifi.
Default Controller `json:"defaults" toml:"defaults" xml:"default" yaml:"defaults"` Default Controller `json:"defaults" toml:"defaults" xml:"default" yaml:"defaults"`
Disable bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"` Disable bool `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"`
Dynamic bool `json:"dynamic" toml:"dynamic" xml:"dynamic" yaml:"dynamic"` Dynamic bool `json:"dynamic" toml:"dynamic" xml:"dynamic,attr" yaml:"dynamic"`
Controllers []*Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"` 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) { func (u *InputUnifi) setDefaults(c *Controller) {
if c.SaveSites == nil {
t := true
c.SaveSites = &t
}
if c.URL == "" { if c.URL == "" {
c.URL = defaultURL c.URL = defaultURL
} }

View File

@ -15,15 +15,20 @@ import (
// Satisfies poller.Input interface. // Satisfies poller.Input interface.
func (u *InputUnifi) Initialize(l poller.Logger) error { func (u *InputUnifi) Initialize(l poller.Logger) error {
if u.Config.Disable { if u.Config.Disable {
l.Logf("unifi input disabled") l.Logf("UniFi input plugin disabled!")
return nil 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. new := u.Config.Default // copy defaults.
u.Config.Controllers = []*Controller{&new} 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 u.Logger = l
for _, c := range u.Config.Controllers { 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.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) c.URL, c.Unifi.ServerVersion, c.User, c.Sites)
default: default:
u.LogErrorf("Controller Auth or Connection failed, but continuing to retry! %s: %v", c.Name, err) 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. // MetricsFrom grabs all the measurements from a UniFi controller and returns them.
func (u *InputUnifi) MetricsFrom(filter *poller.Filter) (*poller.Metrics, bool, error) { 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 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. // Check if the request is for an existing, configured controller.
for _, c := range u.Config.Controllers { for _, c := range u.Config.Controllers {
if !strings.EqualFold(c.Name, filter.Term) { if filter != nil && !strings.EqualFold(c.Name, filter.Term) {
continue continue
} }
exists, err := u.appendController(c, metrics) m, err := u.collectController(c)
if err != nil { if err != nil {
errs = append(errs, err.Error()) errs = append(errs, err.Error())
} }
if exists { if m == nil {
ok = true continue
} }
ok = true
metrics = poller.AppendMetrics(metrics, m)
} }
if len(errs) > 0 { if len(errs) > 0 {
return metrics, ok, fmt.Errorf(strings.Join(errs, ", ")) return metrics, ok, fmt.Errorf(strings.Join(errs, ", "))
} }
if u.Config.Dynamic && !ok && strings.HasPrefix(filter.Term, "http") { if ok {
// Attempt to a dynamic metrics fetch from an unconfigured controller. return metrics, true, nil
return u.dynamicController(filter.Term)
} }
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. // RawMetrics returns API output from the first configured unifi controller.
func (u *InputUnifi) RawMetrics(filter *poller.Filter) ([]byte, error) { 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) { if u.isNill(c) {
u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) 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 return nil, err
} }
switch filter.Type { switch filter.Kind {
case "d", "device", "devices": case "d", "device", "devices":
return u.dumpSitesJSON(c, unifi.APIDevicePath, "Devices", sites) return u.dumpSitesJSON(c, unifi.APIDevicePath, "Devices", sites)
case "client", "clients", "c": case "client", "clients", "c":
return u.dumpSitesJSON(c, unifi.APIClientPath, "Clients", sites) return u.dumpSitesJSON(c, unifi.APIClientPath, "Clients", sites)
case "other", "o": case "other", "o":
_, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", filter.Term) _, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", filter.Path)
return c.Unifi.GetJSON(filter.Term) return c.Unifi.GetJSON(filter.Path)
default: default:
return []byte{}, fmt.Errorf("must provide filter: devices, clients, other") return []byte{}, fmt.Errorf("must provide filter: devices, clients, other")
} }

View File

@ -2,19 +2,24 @@ package poller
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
) )
// DumpJSONPayload prints raw json from the UniFi Controller. // DumpJSONPayload prints raw json from the UniFi Controller. This is currently
// This only works with controller 0 (first one) in the config. // tied into the -j CLI arg, and is probably not very useful outside that context.
func (u *UnifiPoller) DumpJSONPayload() (err error) { func (u *UnifiPoller) DumpJSONPayload() (err error) {
u.Config.Quiet = true u.Config.Quiet = true
split := strings.SplitN(u.Flags.DumpJSON, " ", 2) 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 { if len(split) > 1 {
filter.Term = split[1] filter.Path = split[1]
} }
m, err := inputs[0].RawMetrics(filter) m, err := inputs[0].RawMetrics(filter)

View File

@ -28,10 +28,26 @@ type InputPlugin struct {
Input Input
} }
// Filter is used for raw metrics filters. // Filter is used for metrics filters. Many fields for lots of expansion.
type Filter struct { type Filter struct {
Type string Type string
Term 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 // 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 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) { func (u *UnifiPoller) MetricsFrom(filter *Filter) (*Metrics, bool, error) {
errs := []string{} errs := []string{}
metrics := &Metrics{} metrics := &Metrics{}
ok := false ok := false
for _, input := range inputs { for _, input := range inputs {
if input.Name != filter.Type { if !strings.EqualFold(input.Name, filter.Name) {
continue continue
} }
@ -128,23 +144,7 @@ func (u *UnifiPoller) MetricsFrom(filter *Filter) (*Metrics, bool, error) {
} }
ok = true ok = true
metrics = AppendMetrics(metrics, m)
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 var err error
@ -155,3 +155,25 @@ func (u *UnifiPoller) MetricsFrom(filter *Filter) (*Metrics, bool, error) {
return metrics, ok, err 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
}

View File

@ -26,23 +26,18 @@ const (
) )
type promUnifi struct { type promUnifi struct {
*Prometheus *Config `json:"prometheus" toml:"prometheus" xml:"prometheus" yaml:"prometheus"`
Client *uclient Client *uclient
Device *unifiDevice Device *unifiDevice
UAP *uap UAP *uap
USG *usg USG *usg
USW *usw USW *usw
Site *site Site *site
// This interface is passed to the Collect() method. The Collect method uses // This interface is passed to the Collect() method. The Collect method uses
// this interface to retrieve the latest UniFi measurements and export them. // this interface to retrieve the latest UniFi measurements and export them.
Collector poller.Collect 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. // Config is the input (config file) data used to initialize this output plugin.
type Config struct { type Config struct {
// If non-empty, each of the collected metrics is prefixed by the // 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. // Report accumulates counters that are printed to a log line.
type Report struct { type Report struct {
Config *Config
Total int // Total count of metrics recorded. Total int // Total count of metrics recorded.
Errors int // Total count of errors recording metrics. Errors int // Total count of errors recording metrics.
Zeros int // Total count of metrics equal to zero. Zeros int // Total count of metrics equal to zero.
@ -78,17 +73,18 @@ type Report struct {
wg sync.WaitGroup wg sync.WaitGroup
} }
// target is used for targeted (sometimes dynamic) metrics scrapes.
type target struct { type target struct {
*poller.Filter *poller.Filter
*promUnifi u *promUnifi
} }
func init() { func init() {
u := &promUnifi{Prometheus: &Prometheus{}} u := &promUnifi{Config: &Config{}}
poller.NewOutput(&poller.Output{ poller.NewOutput(&poller.Output{
Name: "prometheus", Name: "prometheus",
Config: u.Prometheus, Config: u,
Method: u.Run, Method: u.Run,
}) })
} }
@ -96,51 +92,55 @@ func init() {
// Run creates the collectors and starts the web server up. // Run creates the collectors and starts the web server up.
// Should be run in a Go routine. Returns nil if not configured. // Should be run in a Go routine. Returns nil if not configured.
func (u *promUnifi) Run(c poller.Collect) error { func (u *promUnifi) Run(c poller.Collect) error {
if u.Config.Disable { if u.Disable {
return nil return nil
} }
u.Config.Namespace = strings.Trim(strings.Replace(u.Config.Namespace, "-", "_", -1), "_") u.Namespace = strings.Trim(strings.Replace(u.Namespace, "-", "_", -1), "_")
if u.Config.Namespace == "" { if u.Namespace == "" {
u.Config.Namespace = strings.Replace(poller.AppName, "-", "", -1) u.Namespace = strings.Replace(poller.AppName, "-", "", -1)
} }
if u.Config.HTTPListen == "" { if u.HTTPListen == "" {
u.Config.HTTPListen = defaultHTTPListen 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() mux := http.NewServeMux()
prometheus.MustRegister(version.NewCollector(u.Config.Namespace)) prometheus.MustRegister(version.NewCollector(u.Namespace))
prometheus.MustRegister(&promUnifi{ prometheus.MustRegister(u)
Collector: c, c.Logf("Prometheus exported at https://%s/ - namespace: %s", u.HTTPListen, u.Namespace)
Client: descClient(u.Config.Namespace + "_client_"), mux.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer,
Device: descDevice(u.Config.Namespace + "_device_"), // stats for all device types. promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError},
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},
)) ))
mux.HandleFunc("/scrape", u.ScrapeHandler) 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. // ScrapeHandler allows prometheus to scrape a single source, instead of all sources.
func (u *promUnifi) ScrapeHandler(w http.ResponseWriter, r *http.Request) { func (u *promUnifi) ScrapeHandler(w http.ResponseWriter, r *http.Request) {
t := &target{promUnifi: u, Filter: &poller.Filter{}} t := &target{u: u, Filter: &poller.Filter{}}
if t.Filter.Type = r.URL.Query().Get("input"); t.Filter.Type == "" { 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) http.Error(w, `'input' parameter must be specified (try "unifi")`, 400)
return 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) http.Error(w, "'target' parameter must be specified, configured name, or unconfigured url", 400)
return return
} }
@ -152,14 +152,15 @@ func (u *promUnifi) ScrapeHandler(w http.ResponseWriter, r *http.Request) {
).ServeHTTP(w, r) ).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 // Describe satisfies the prometheus Collector. This returns all of the
// metric descriptions that this packages produces. // metric descriptions that this packages produces.
func (t *target) Describe(ch chan<- *prometheus.Desc) { func (t *target) Describe(ch chan<- *prometheus.Desc) {
t.promUnifi.Describe(ch) t.u.Describe(ch)
}
func (t *target) Collect(ch chan<- prometheus.Metric) {
t.promUnifi.collect(ch, t.Filter)
} }
// Describe satisfies the prometheus Collector. This returns all of the // 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 // Collect satisfies the prometheus Collector. This runs the input method to get
// the current metrics (from another package) then exports them for prometheus. // the current metrics (from another package) then exports them for prometheus.
func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { 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) { func (u *promUnifi) collect(ch chan<- prometheus.Metric, filter *poller.Filter) {
var err error var err error
ok := false r := &Report{
Config: u.Config,
r := &Report{Config: u.Config, ch: make(chan []*metric, buffer), Start: time.Now()} ch: make(chan []*metric, buffer),
Start: time.Now()}
defer r.close() defer r.close()
ok := false
if filter == nil { if filter == nil {
r.Metrics, ok, err = u.Collector.Metrics() r.Metrics, ok, err = u.Collector.Metrics()
} else { } else {
r.Metrics, ok, err = u.Collector.MetricsFrom(filter) r.Metrics, ok, err = u.Collector.MetricsFrom(filter)
} }
r.Fetch = time.Since(r.Start)
if err != nil { 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 { if !ok {
return return
} }
} }
r.Fetch = time.Since(r.Start)
if r.Metrics.Devices == nil { if r.Metrics.Devices == nil {
r.Metrics.Devices = &unifi.Devices{} r.Metrics.Devices = &unifi.Devices{}
} }

View File

@ -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{}) { func (r *Report) error(ch chan<- prometheus.Metric, d *prometheus.Desc, v interface{}) {
r.Errors++ r.Errors++
if r.Config.ReportErrors { if r.ReportErrors {
ch <- prometheus.NewInvalidMetric(d, fmt.Errorf("error: %v", v)) ch <- prometheus.NewInvalidMetric(d, fmt.Errorf("error: %v", v))
} }
} }