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`
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
---

View File

@ -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

View File

@ -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,

View File

@ -18,22 +18,33 @@
<influxdb disable="false">
<interval>30s</interval>
<url>http://127.0.0.1:8086</url>
<user>unifi</user>
<pass>unifi</pass>
<user>unifipoller</user>
<pass>unifipoller</pass>
<db>unifi</db>
<verify_ssl>false</verify_ssl>
</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. -->
<controller name="">
<site>all</site>
<user>influx</user>
<pass></pass>
<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>
</controller>
</unifi>
</poller>

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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)

View File

@ -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
}

View File

@ -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{}
}

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{}) {
r.Errors++
if r.Config.ReportErrors {
if r.ReportErrors {
ch <- prometheus.NewInvalidMetric(d, fmt.Errorf("error: %v", v))
}
}