unpoller_unpoller/pkg/promunifi/collector.go

363 lines
11 KiB
Go

// Package promunifi provides the bridge between unpoller metrics and prometheus.
package promunifi
import (
"fmt"
"net/http"
"reflect"
"strings"
"sync"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
promver "github.com/prometheus/common/version"
"github.com/unpoller/unifi"
"github.com/unpoller/unpoller/core/poller"
"github.com/unpoller/unpoller/core/webserver"
"golift.io/version"
)
// PluginName is the name of this plugin.
const PluginName = "prometheus"
const (
// channel buffer, fits at least one batch.
defaultBuffer = 50
defaultHTTPListen = "0.0.0.0:9130"
// simply fewer letters.
counter = prometheus.CounterValue
gauge = prometheus.GaugeValue
)
var ErrMetricFetchFailed = fmt.Errorf("metric fetch failed")
type promUnifi struct {
*Config `json:"prometheus" toml:"prometheus" xml:"prometheus" yaml:"prometheus"`
Client *uclient
Device *unifiDevice
UAP *uap
USG *usg
USW *usw
Site *site
RogueAP *rogueap
// 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
}
// 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
// provided string and an underscore ("_").
Namespace string `json:"namespace" toml:"namespace" xml:"namespace" yaml:"namespace"`
HTTPListen string `json:"http_listen" toml:"http_listen" xml:"http_listen" yaml:"http_listen"`
// If these are provided, the app will attempt to listen with an SSL connection.
SSLCrtPath string `json:"ssl_cert_path" toml:"ssl_cert_path" xml:"ssl_cert_path" yaml:"ssl_cert_path"`
SSLKeyPath string `json:"ssl_key_path" toml:"ssl_key_path" xml:"ssl_key_path" yaml:"ssl_key_path"`
// Buffer is a channel buffer.
// Default is probably 50. Seems fast there; try 1 to see if CPU usage goes down?
Buffer int `json:"buffer" toml:"buffer" xml:"buffer" yaml:"buffer"`
// 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 `json:"report_errors" toml:"report_errors" xml:"report_errors" yaml:"report_errors"`
Disable bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"`
// Save data for dead ports? ie. ports that are down or disabled.
DeadPorts bool `json:"dead_ports" toml:"dead_ports" xml:"dead_ports" yaml:"dead_ports"`
}
type metric struct {
Desc *prometheus.Desc
ValueType prometheus.ValueType
Value interface{}
Labels []string
}
// Report accumulates counters that are printed to a log line.
type Report struct {
*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.
USG int // Total count of USG devices.
USW int // Total count of USW devices.
UAP int // Total count of UAP devices.
UDM int // Total count of UDM devices.
UXG int // Total count of UXG devices.
Metrics *poller.Metrics // Metrics collected and recorded.
Elapsed time.Duration // Duration elapsed collecting and exporting.
Fetch time.Duration // Duration elapsed making controller requests.
Start time.Time // Time collection began.
ch chan []*metric
wg sync.WaitGroup
}
// target is used for targeted (sometimes dynamic) metrics scrapes.
type target struct {
*poller.Filter
u *promUnifi
}
// init is how this modular code is initialized by the main app.
// This module adds itself as an output module to the poller core.
func init() { // nolint: gochecknoinits
u := &promUnifi{Config: &Config{}}
poller.NewOutput(&poller.Output{
Name: PluginName,
Config: u,
Method: u.Run,
})
}
// 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.Collector = c; u.Config == nil || u.Disable {
u.Logf("Prometheus config missing (or disabled), Prometheus HTTP listener disabled!")
return nil
}
u.Namespace = strings.Trim(strings.ReplaceAll(u.Namespace, "-", "_"), "_")
if u.Namespace == "" {
u.Namespace = strings.ReplaceAll(poller.AppName, "-", "")
}
if u.HTTPListen == "" {
u.HTTPListen = defaultHTTPListen
}
if u.Buffer == 0 {
u.Buffer = defaultBuffer
}
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_")
u.RogueAP = descRogueAP(u.Namespace + "_rogueap_")
mux := http.NewServeMux()
promver.Version = version.Version
promver.Revision = version.Revision
promver.Branch = version.Branch
webserver.UpdateOutput(&webserver.Output{Name: PluginName, Config: u.Config})
prometheus.MustRegister(promver.NewCollector(u.Namespace))
prometheus.MustRegister(u)
mux.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer,
promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError},
))
mux.HandleFunc("/scrape", u.ScrapeHandler)
mux.HandleFunc("/", u.DefaultHandler)
switch u.SSLKeyPath == "" && u.SSLCrtPath == "" {
case true:
u.Logf("Prometheus exported at http://%s/ - namespace: %s", u.HTTPListen, u.Namespace)
return http.ListenAndServe(u.HTTPListen, mux)
default:
u.Logf("Prometheus exported at https://%s/ - namespace: %s", u.HTTPListen, u.Namespace)
return http.ListenAndServeTLS(u.HTTPListen, u.SSLCrtPath, u.SSLKeyPath, 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{u: u, Filter: &poller.Filter{
Name: r.URL.Query().Get("input"), // "unifi"
Path: r.URL.Query().Get("target"), // url: "https://127.0.0.1:8443"
}}
if t.Name == "" {
t.Name = "unifi" // the default
}
if pathOld := r.URL.Query().Get("path"); pathOld != "" {
u.LogErrorf("deprecated 'path' parameter used; update your config to use 'target'")
if t.Path == "" {
t.Path = pathOld
}
}
if roleOld := r.URL.Query().Get("role"); roleOld != "" {
u.LogErrorf("deprecated 'role' parameter used; update your config to use 'target'")
if t.Path == "" {
t.Path = roleOld
}
}
if t.Path == "" {
u.LogErrorf("'target' parameter missing on scrape from %v", r.RemoteAddr)
http.Error(w, "'target' parameter must be specified: configured OR unconfigured url", 400)
return
}
registry := prometheus.NewRegistry()
registry.MustRegister(t)
promhttp.HandlerFor(
registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError},
).ServeHTTP(w, r)
}
func (u *promUnifi) DefaultHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = 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.u.Describe(ch)
}
// Describe satisfies the prometheus Collector. This returns all of the
// metric descriptions that this packages produces.
func (u *promUnifi) Describe(ch chan<- *prometheus.Desc) {
for _, f := range []interface{}{u.Client, u.Device, u.UAP, u.USG, u.USW, u.Site} {
v := reflect.Indirect(reflect.ValueOf(f))
// Loop each struct member and send it to the provided channel.
for i := 0; i < v.NumField(); i++ {
desc, ok := v.Field(i).Interface().(*prometheus.Desc)
if ok && desc != nil {
ch <- 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) {
u.collect(ch, nil)
}
func (u *promUnifi) collect(ch chan<- prometheus.Metric, filter *poller.Filter) {
var err error
r := &Report{
Config: u.Config,
ch: make(chan []*metric, u.Config.Buffer),
Start: time.Now(),
}
defer r.close()
r.Metrics, err = u.Collector.Metrics(filter)
r.Fetch = time.Since(r.Start)
if err != nil {
r.error(ch, prometheus.NewInvalidDesc(err), ErrMetricFetchFailed)
u.LogErrorf("metric fetch failed: %v", err)
return
}
// Pass Report interface into our collecting and reporting methods.
go u.exportMetrics(r, ch, r.ch)
u.loopExports(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.
// This is where our channels connects to the prometheus channel.
func (u *promUnifi) exportMetrics(r report, ch chan<- prometheus.Metric, ourChan chan []*metric) {
descs := make(map[*prometheus.Desc]bool) // used as a counter
defer r.report(u, descs)
for newMetrics := range ourChan {
for _, m := range newMetrics {
descs[m.Desc] = true
switch v := m.Value.(type) {
case unifi.FlexInt:
ch <- r.export(m, v.Val)
case float64:
ch <- r.export(m, v)
case int64:
ch <- r.export(m, float64(v))
case int:
ch <- r.export(m, float64(v))
default:
r.error(ch, m.Desc, fmt.Sprintf("not a number: %v", m.Value))
}
}
r.done()
}
}
func (u *promUnifi) loopExports(r report) {
m := r.metrics()
for _, s := range m.RogueAPs {
u.switchExport(r, s)
}
for _, s := range m.Sites {
u.switchExport(r, s)
}
for _, s := range m.SitesDPI {
u.exportSiteDPI(r, s)
}
for _, c := range m.Clients {
u.switchExport(r, c)
}
for _, d := range m.Devices {
u.switchExport(r, d)
}
appTotal := make(totalsDPImap)
catTotal := make(totalsDPImap)
for _, c := range m.ClientsDPI {
u.exportClientDPI(r, c, appTotal, catTotal)
}
u.exportClientDPItotals(r, appTotal, catTotal)
}
func (u *promUnifi) switchExport(r report, v interface{}) {
switch v := v.(type) {
case *unifi.RogueAP:
// r.addRogueAP()
u.exportRogueAP(r, v)
case *unifi.UAP:
r.addUAP()
u.exportUAP(r, v)
case *unifi.USW:
r.addUSW()
u.exportUSW(r, v)
case *unifi.USG:
r.addUSG()
u.exportUSG(r, v)
case *unifi.UXG:
r.addUXG()
u.exportUXG(r, v)
case *unifi.UDM:
r.addUDM()
u.exportUDM(r, v)
case *unifi.Site:
u.exportSite(r, v)
case *unifi.Client:
u.exportClient(r, v)
default:
u.LogErrorf("invalid type: %T", v)
}
}