unpoller_unpoller/promunifi/collector.go

203 lines
5.1 KiB
Go

// Package promunifi provides the bridge between unifi metrics and prometheus.
package promunifi
import (
"fmt"
"reflect"
"strings"
"sync"
"time"
"github.com/davidnewhall/unifi-poller/metrics"
"github.com/prometheus/client_golang/prometheus"
"golift.io/unifi"
)
// channel buffer, fits at least one batch.
const buffer = 50
// UnifiCollectorCnfg defines the data needed to collect and report UniFi Metrics.
type UnifiCollectorCnfg struct {
// If non-empty, each of the collected metrics is prefixed by the
// provided string and an underscore ("_").
Namespace string
// If true, any error encountered during collection is reported as an
// invalid metric (see NewInvalidMetric). Otherwise, errors are ignored
// and the collected metrics will be incomplete. Possibly, no metrics
// will be collected at all.
ReportErrors bool
// This function is passed to the Collect() method. The Collect method runs
// this function to retrieve the latest UniFi measurements and export them.
CollectFn func() (*metrics.Metrics, error)
// Provide a logger function if you want to run a routine *after* prometheus checks in.
LoggingFn func(*Report)
}
type unifiCollector struct {
Config UnifiCollectorCnfg
Client *uclient
Device *unifiDevice
UAP *uap
USG *usg
USW *usw
Site *site
}
type metricExports struct {
Desc *prometheus.Desc
ValueType prometheus.ValueType
Value interface{}
Labels []string
}
// Report is passed into LoggingFn to log the export metrics to stdout (outside this package).
type Report struct {
Total int
Errors int
Zeros int
Descs int
Metrics *metrics.Metrics
Elapsed time.Duration
Start time.Time
ch chan []*metricExports
wg sync.WaitGroup
}
// internal interface used to "process metrics" - can be mocked and overridden for tests.
type report interface {
send([]*metricExports)
add()
done()
metrics() *metrics.Metrics
}
// NewUnifiCollector returns a prometheus collector that will export any available
// UniFi metrics. You must provide a collection function in the opts.
func NewUnifiCollector(opts UnifiCollectorCnfg) prometheus.Collector {
if opts.CollectFn == nil {
panic("nil collector function")
}
if opts.Namespace = strings.Trim(opts.Namespace, "_") + "_"; opts.Namespace == "_" {
opts.Namespace = ""
}
return &unifiCollector{
Config: opts,
Client: descClient(opts.Namespace + "client_"),
Device: descDevice(opts.Namespace + "device_"), // stats for all device types.
UAP: descUAP(opts.Namespace + "device_"),
USG: descUSG(opts.Namespace + "device_"),
USW: descUSW(opts.Namespace + "device_"),
Site: descSite(opts.Namespace + "site_"),
}
}
// Describe satisfies the prometheus Collector. This returns all of the
// metric descriptions that this packages produces.
func (u *unifiCollector) Describe(ch chan<- *prometheus.Desc) {
describe := func(from interface{}) {
v := reflect.Indirect(reflect.ValueOf(from))
// Loop each struct member and send it to the provided channel.
for i := 0; i < v.NumField(); i++ {
desc, ok := v.Field(i).Interface().(*prometheus.Desc)
if ok && desc != nil {
ch <- desc
}
}
}
describe(u.Client)
describe(u.Device)
describe(u.UAP)
describe(u.USG)
describe(u.USW)
describe(u.Site)
}
// 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 *unifiCollector) Collect(ch chan<- prometheus.Metric) {
var err error
r := &Report{Start: time.Now(), ch: make(chan []*metricExports, buffer)}
defer func() {
r.wg.Wait()
close(r.ch)
}()
if r.Metrics, err = u.Config.CollectFn(); err != nil {
ch <- prometheus.NewInvalidMetric(
prometheus.NewInvalidDesc(fmt.Errorf("metric fetch failed")), err)
return
}
go u.exportMetrics(r, ch)
u.exportClients(r)
u.exportSites(r)
u.exportUAPs(r)
u.exportUSWs(r)
u.exportUSGs(r)
u.exportUDMs(r)
}
// This is closely tied to the method above with a sync.WaitGroup.
// This method runs in a go routine and exits when the channel closes.
func (u *unifiCollector) exportMetrics(r *Report, ch chan<- prometheus.Metric) {
descs := make(map[*prometheus.Desc]bool) // used as a counter
for newMetrics := range r.ch {
for _, m := range newMetrics {
r.Total++
descs[m.Desc] = true
var value float64
switch v := m.Value.(type) {
case unifi.FlexInt:
value = v.Val
case float64:
value = v
case int64:
value = float64(v)
case int:
value = float64(v)
default:
r.Errors++
if u.Config.ReportErrors {
ch <- prometheus.NewInvalidMetric(m.Desc, fmt.Errorf("not a number: %v", m.Value))
}
continue
}
if value == 0 {
r.Zeros++
}
ch <- prometheus.MustNewConstMetric(m.Desc, m.ValueType, value, m.Labels...)
}
r.wg.Done()
}
if u.Config.LoggingFn == nil {
return
}
r.Descs, r.Elapsed = len(descs), time.Since(r.Start)
u.Config.LoggingFn(r)
}
func (r *Report) metrics() *metrics.Metrics {
return r.Metrics
}
// satisfy gomnd
const one = 1
func (r *Report) add() {
r.wg.Add(one)
}
func (r *Report) done() {
r.wg.Done()
}
func (r *Report) send(m []*metricExports) {
r.wg.Add(one)
r.ch <- m
}