Merge pull request #139 from davidnewhall/dn2_prometheus

Add prometheus exporter.
This commit is contained in:
David Newhall II 2019-11-30 00:11:34 -08:00 committed by GitHub
commit d052e699db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2019 additions and 446 deletions

View File

@ -35,7 +35,7 @@ URL="${SOURCE_URL}"
# This parameter is passed in as -X to go build. Used to override the Version variable in a package.
# This makes a path like github.com/user/hello-world/helloworld.Version=1.3.3
# Name the Version-containing library the same as the github repo, without dashes.
VERSION_PATH="${IMPORT_PATH}/$(echo ${BINARY} | tr -d -- -).Version"
VERSION_PATH="${IMPORT_PATH}/poller.Version"
# Dynamic. Recommend not changing.
VVERSION=$(git describe --abbrev=0 --tags $(git rev-list --tags --max-count=1))

90
Gopkg.lock generated
View File

@ -9,6 +9,22 @@
revision = "3012a1dbe2e4bd1391d42b32f0577cb7bbc7f005"
version = "v0.3.1"
[[projects]]
digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d"
name = "github.com/beorn7/perks"
packages = ["quantile"]
pruneopts = "UT"
revision = "37c8de3658fcb183f997c4e13e8337516ab753e6"
version = "v1.0.1"
[[projects]]
digest = "1:573ca21d3669500ff845bdebee890eb7fc7f0f50c59f2132f2a0c6b03d85086a"
name = "github.com/golang/protobuf"
packages = ["proto"]
pruneopts = "UT"
revision = "6c65a5562fc06764971b7c5d05c76c75e84bdbf7"
version = "v1.3.2"
[[projects]]
branch = "master"
digest = "1:50708c8fc92aec981df5c446581cf9f90ba9e2a5692118e0ce75d4534aaa14a2"
@ -21,6 +37,58 @@
pruneopts = "UT"
revision = "fc22c7df067eefd070157f157893fbce961d6359"
[[projects]]
digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc"
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
pruneopts = "UT"
revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c"
version = "v1.0.1"
[[projects]]
digest = "1:eb04f69c8991e52eff33c428bd729e04208bf03235be88e4df0d88497c6861b9"
name = "github.com/prometheus/client_golang"
packages = [
"prometheus",
"prometheus/internal",
"prometheus/promhttp",
]
pruneopts = "UT"
revision = "170205fb58decfd011f1550d4cfb737230d7ae4f"
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4"
name = "github.com/prometheus/client_model"
packages = ["go"]
pruneopts = "UT"
revision = "14fe0d1b01d4d5fc031dd4bec1823bd3ebbe8016"
[[projects]]
digest = "1:f119e3205d3a1f0f19dbd7038eb37528e2c6f0933269dc344e305951fb87d632"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model",
]
pruneopts = "UT"
revision = "287d3e634a1e550c9e463dd7e5a75a422c614505"
version = "v0.7.0"
[[projects]]
digest = "1:ec0ff4bd619a67065e34d6477711ed0117e335f99059a4c508e0fe21cfe7b304"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/fs",
"internal/util",
]
pruneopts = "UT"
revision = "6d489fc7f1d9cd890a250f3ea3431b1744b9623f"
version = "v0.0.8"
[[projects]]
digest = "1:524b71991fc7d9246cc7dc2d9e0886ccb97648091c63e30eef619e6862c955dd"
name = "github.com/spf13/pflag"
@ -30,20 +98,28 @@
version = "v1.0.5"
[[projects]]
digest = "1:e74d5f03545d51228b9539aaffc5eb8a692fcb22f38fa60253437b1fc063a73b"
branch = "master"
digest = "1:68fe4216878f16dd6ef33413365fbbe8d2eb781177c7adab874cfc752ce96a7e"
name = "golang.org/x/sys"
packages = ["windows"]
pruneopts = "UT"
revision = "6d18c012aee9febd81bbf9806760c8c4480e870d"
[[projects]]
digest = "1:0d815236933294be05901215b375de3a7c990abcb769069eccca32a825a15862"
name = "golift.io/unifi"
packages = ["."]
pruneopts = "UT"
revision = "2bdbccee871d4f36a4e1efa3463386ae70095033"
version = "v4.1.3"
revision = "68fa5c1f82da4567d8c192c52397984551cb08b9"
version = "v4.1.4"
[[projects]]
digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96"
digest = "1:b75b3deb2bce8bc079e16bb2aecfe01eb80098f5650f9e93e5643ca8b7b73737"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
version = "v2.2.2"
revision = "1f64d6156d11335c3f22d9330b0ad14fc1e789ce"
version = "v2.2.7"
[solve-meta]
analyzer-name = "dep"
@ -51,6 +127,8 @@
input-imports = [
"github.com/BurntSushi/toml",
"github.com/influxdata/influxdb1-client/v2",
"github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/client_golang/prometheus/promhttp",
"github.com/spf13/pflag",
"golift.io/unifi",
"gopkg.in/yaml.v2",

View File

@ -75,7 +75,8 @@ is provided so the application can be easily adapted to any environment.
interval default: 30s
How often to poll the controller for updated client and device data.
The UniFi Controller only updates traffic stats about every 30 seconds.
The UniFi Controller only updates traffic stats about every 30-60 seconds.
Only works if "mode" (below) is "influx" - other modes do not use interval.
debug default: false
This turns on time stamps and line numbers in logs, outputs a few extra
@ -89,10 +90,10 @@ is provided so the application can be easily adapted to any environment.
mode default: "influx"
* Value: influx
This default mode runs this application as a daemon. It will poll
the controller at the configured interval. Providing an invalid value
will run in this default mode.
the controller at the configured interval and report measurements to
InfluxDB. Providing an invalid value will run in this default mode.
* Value: influxlambda - (the only other available option right now)
* Value: influxlambda
Setting this value will invoke a run-once mode where the application
immediately polls the controller and reports the metrics to InfluxDB.
Then it exits. This mode is useful in an AWS Lambda or a crontab where
@ -101,6 +102,16 @@ is provided so the application can be easily adapted to any environment.
This mode can also be combined with a "test database" in InfluxDB to
give yourself a "test config file" you may run ad-hoc to test changes.
* Value: prometheus
In this mode the application opens an http interface and exports the
measurements at /metrics for collection by prometheus. Enabling this
mode disables InfluxDB usage entirely.
http_listen default: 0.0.0.0:9130
This option controls the IP and port the http listener uses when the
mode is set to prometheus. This setting has no effect when other modes
are in use. Metrics become available at the /metrics URI.
influx_url default: http://127.0.0.1:8086
This is the URL where the Influx web server is available.

View File

@ -8,7 +8,8 @@
sites = ["all"]
# The UniFi Controller only updates traffic stats about every 30 seconds.
# Setting this to something lower may lead to "zeros" in your data. You've been warned.
# Setting this to something lower may lead to "zeros" in your data.
# If you're getting zeros now, set this to "1m"
interval = "30s"
# Turns on line numbers, microsecond logging, and a per-device log.
@ -24,13 +25,22 @@ quiet = false
# an invalid mode will also result in "influx". In this default mode the application
# runs as a daemon and polls the controller at the configured interval.
#
# There is only one other option at this time: "influxlambda"
# There are two other options at this time: "influxlambda" and "prometheus"
#
# Lambda mode makes the application exit after collecting and reporting metrics
# Mode "influxlambda" makes the application exit after collecting and reporting metrics
# to InfluxDB one time. This mode requires an external process like an AWS Lambda
# or a simple crontab to keep the timings accurate on UniFi Poller run intervals.
#
# Mode "prometheus" opens an HTTP server on port 9130 and exports the metrics at
# /metrics for polling collection by a prometheus server. This disables influxdb.
# IMPORTANT: The prometheus mode is still beta.
# Please help us test and provide your feedback on the github repo!
mode = "influx"
# 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"
# InfluxDB does not require auth by default, so the user/password are probably unimportant.
influx_url = "http://127.0.0.1:8086"
influx_user = "unifi"

View File

@ -4,6 +4,7 @@
"debug": false,
"quiet": false,
"mode": "influx",
"http_listen": "0.0.0.0:9130",
"influx_url": "http://127.0.0.1:8086",
"influx_user": "unifi",
"influx_pass": "unifi",

View File

@ -18,7 +18,8 @@
<!--
# The UniFi Controller only updates traffic stats about every 30 seconds.
# Setting this to something lower may lead to "zeros" in your data. You've been warned.
# Setting this to something lower may lead to "zeros" in your data.
# If you're getting zeros now, set this to "1m"
-->
<interval>30s</interval>
@ -40,14 +41,25 @@
# an invalid mode will also result in "influx". In this default mode the application
# runs as a daemon and polls the controller at the configured interval.
#
# There is only one other option at this time: "influxlambda"
# There are two other options at this time: "influxlambda" and "prometheus"
#
# Lambda mode makes the application exit after collecting and reporting metrics
# Mode "influxlambda" makes the application exit after collecting and reporting metrics
# to InfluxDB one time. This mode requires an external process like an AWS Lambda
# or a simple crontab to keep the timings accurate on UniFi Poller run intervals.
#
# Mode "prometheus" opens an HTTP server on port 9130 and exports the metrics at
# /metrics for polling collection by a prometheus server. This disables influxdb.
# IMPORTANT: The prometheus mode is still beta.
# Please help us test and provide your feedback on the github repo!
-->
<mode>influx</mode>
<!--
# 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</http_listen>
<!--
# InfluxDB does not require auth by default, so the user/password are probably unimportant.
-->

View File

@ -9,7 +9,9 @@ sites:
- all
# The UniFi Controller only updates traffic stats about every 30 seconds.
# Setting this to something lower may lead to "zeros" in your data. You've been warned.
# Setting this to something lower may lead to "zeros" in your data.
# If you're getting zeros now, set this to "1m"
# Only has effect if "mode" (below) is "influx" - other modes do not use interval.
interval: "30s"
# Turns on line numbers, microsecond logging, and a per-device log.
@ -25,13 +27,22 @@ quiet: false
# an invalid mode will also result in "influx". In this default mode the application
# runs as a daemon and polls the controller at the configured interval.
#
# There is only one other option at this time: "influxlambda"
# There are two other options at this time: "influxlambda" and "prometheus"
#
# Lambda mode makes the application exit after collecting and reporting metrics
# Mode "influxlambda" makes the application exit after collecting and reporting metrics
# to InfluxDB one time. This mode requires an external process like an AWS Lambda
# or a simple crontab to keep the timings accurate on UniFi Poller run intervals.
#
# Mode "prometheus" opens an HTTP server on port 9130 and exports the metrics at
# /metrics for polling collection by a prometheus server. This disables influxdb.
# IMPORTANT: The prometheus mode is still beta.
# Please help us test and provide your feedback on the github repo!
mode: "influx"
# 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"
# InfluxDB does not require auth by default, so the user/password are probably unimportant.
influx_url: "http://127.0.0.1:8086"
influx_user: "unifi"

4
influxunifi/README.md Normal file
View File

@ -0,0 +1,4 @@
# influx
This package provides the methods to turn UniFi measurements into influx
data-points with appropriate tags and fields.

View File

@ -1,4 +1,4 @@
package unifipoller
package influxunifi
import (
"time"

View File

@ -1,4 +1,4 @@
package unifipoller
package influxunifi
import (
influx "github.com/influxdata/influxdb1-client/v2"

69
influxunifi/metrics.go Normal file
View File

@ -0,0 +1,69 @@
// Package influx provides the methods to turn UniFi measurements into influx
// data-points with appropriate tags and fields.
package influxunifi
import (
"github.com/davidnewhall/unifi-poller/metrics"
client "github.com/influxdata/influxdb1-client/v2"
)
// Metrics contains all the data from the controller and an influx endpoint to send it to.
type Metrics struct {
*metrics.Metrics
client.BatchPoints
}
// ProcessPoints batches all device and client data into influxdb data points.
// Call this after you've collected all the data you care about.
// This function is sorta weird and returns a slice of errors. The reasoning is
// that some points may process while others fail, so we attempt to process them
// all. This is (usually) run in a loop, so we can't really exit on error,
// we just log the errors and tally them on a counter. In reality, this never
// returns any errors because we control the data going in; cool right? But we
// still check&log it in case the data going is skewed up and causes errors!
func (m *Metrics) ProcessPoints() []error {
errs := []error{}
processPoints := func(m *Metrics, p []*client.Point, err error) {
switch {
case err != nil:
errs = append(errs, err)
case p == nil:
default:
m.AddPoints(p)
}
}
for _, asset := range m.Sites {
pts, err := SitePoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.Clients {
pts, err := ClientPoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.IDSList {
pts, err := IDSPoints(asset) // no m.TS.
processPoints(m, pts, err)
}
if m.Devices == nil {
return errs
}
for _, asset := range m.Devices.UAPs {
pts, err := UAPPoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.Devices.USGs {
pts, err := USGPoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.Devices.USWs {
pts, err := USWPoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.Devices.UDMs {
pts, err := UDMPoints(asset, m.TS)
processPoints(m, pts, err)
}
return errs
}

View File

@ -1,4 +1,4 @@
package unifipoller
package influxunifi
import (
"strings"

View File

@ -1,4 +1,4 @@
package unifipoller
package influxunifi
import (
"time"

View File

@ -1,4 +1,4 @@
package unifipoller
package influxunifi
import (
"time"

View File

@ -1,4 +1,4 @@
package unifipoller
package influxunifi
import (
"strings"

View File

@ -1,4 +1,4 @@
package unifipoller
package influxunifi
import (
"time"

View File

@ -9,4 +9,4 @@ UP_BRANCH=stable
UP_UNIFI_USER=influx
UP_UNIFI_PASS=
UP_UNIFI_URL=https://127.0.0.1:8443
UP_DEBUG_MODE=false
UP_DEBUG=false

View File

@ -48,7 +48,7 @@ services:
- UP_UNIFI_USER=${UP_UNIFI_USER}
- UP_UNIFI_PASS=${UP_UNIFI_PASS}
- UP_UNIFI_URL=${UP_UNIFI_URL}
- UP_DEBUG_MODE=${UP_DEBUG_MODE}
- UP_DEBUG=${UP_DEBUG}
volumes:
influxdb-storage:
chronograf-storage:

View File

@ -3,12 +3,12 @@ package main
import (
"log"
"github.com/davidnewhall/unifi-poller/unifipoller"
"github.com/davidnewhall/unifi-poller/poller"
)
// Keep it simple.
func main() {
if err := unifipoller.Start(); err != nil {
if err := poller.New().Start(); err != nil {
log.Fatalln("[ERROR]", err)
}
}

16
metrics/metrics.go Normal file
View File

@ -0,0 +1,16 @@
package metrics
import (
"time"
"golift.io/unifi"
)
// Metrics is a type shared by the exporting and reporting packages.
type Metrics struct {
TS time.Time
unifi.Sites
unifi.IDSList
unifi.Clients
*unifi.Devices
}

View File

@ -1,6 +1,6 @@
// +build darwin
package unifipoller
package poller
// DefaultConfFile is where to find config is --config is not prvided.
const DefaultConfFile = "/usr/local/etc/unifi-poller/up.conf"

View File

@ -1,6 +1,6 @@
// +build !windows,!darwin
package unifipoller
package poller
// DefaultConfFile is where to find config is --config is not prvided.
const DefaultConfFile = "/etc/unifi-poller/up.conf"

View File

@ -1,6 +1,6 @@
// +build windows
package unifipoller
package poller
// DefaultConfFile is where to find config is --config is not prvided.
const DefaultConfFile = `C:\ProgramData\unifi-poller\up.conf`

View File

@ -1,4 +1,4 @@
package unifipoller
package poller
import (
"encoding/json"
@ -24,6 +24,7 @@ var Version = "development"
const (
// App defaults in case they're missing from the config.
appName = "unifi-poller"
defaultInterval = 30 * time.Second
defaultInfluxDB = "unifi"
defaultInfluxUser = "unifi"
@ -31,6 +32,7 @@ const (
defaultInfluxURL = "http://127.0.0.1:8086"
defaultUnifiUser = "influx"
defaultUnifiURL = "https://127.0.0.1:8443"
defaultHTTPListen = "0.0.0.0:9130"
)
// ENVConfigPrefix is the prefix appended to an env variable tag
@ -55,36 +57,28 @@ type Flag struct {
*pflag.FlagSet
}
// Metrics contains all the data from the controller and an influx endpoint to send it to.
type Metrics struct {
TS time.Time
unifi.Sites
unifi.IDSList
unifi.Clients
*unifi.Devices
influx.BatchPoints
}
// Config represents the data needed to poll a controller and report to influxdb.
// This is all of the data stored in the config file.
// Any with explicit defaults have omitempty on json and toml tags.
type Config struct {
Interval Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval" env:"POLLING_INTERVAL"`
Debug bool `json:"debug" toml:"debug" xml:"debug" yaml:"debug" env:"DEBUG_MODE"`
Quiet bool `json:"quiet,omitempty" toml:"quiet,omitempty" xml:"quiet" yaml:"quiet" env:"QUIET_MODE"`
VerifySSL bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl" env:"VERIFY_SSL"`
CollectIDS bool `json:"collect_ids" toml:"collect_ids" xml:"collect_ids" yaml:"collect_ids" env:"COLLECT_IDS"`
ReAuth bool `json:"reauthenticate" toml:"reauthenticate" xml:"reauthenticate" yaml:"reauthenticate" env:"REAUTHENTICATE"`
InfxBadSSL bool `json:"influx_insecure_ssl" toml:"influx_insecure_ssl" xml:"influx_insecure_ssl" yaml:"influx_insecure_ssl" env:"INFLUX_INSECURE_SSL"`
Mode string `json:"mode" toml:"mode" xml:"mode" yaml:"mode" env:"POLLING_MODE"`
InfluxURL string `json:"influx_url,omitempty" toml:"influx_url,omitempty" xml:"influx_url" yaml:"influx_url" env:"INFLUX_URL"`
InfluxUser string `json:"influx_user,omitempty" toml:"influx_user,omitempty" xml:"influx_user" yaml:"influx_user" env:"INFLUX_USER"`
InfluxPass string `json:"influx_pass,omitempty" toml:"influx_pass,omitempty" xml:"influx_pass" yaml:"influx_pass" env:"INFLUX_PASS"`
InfluxDB string `json:"influx_db,omitempty" toml:"influx_db,omitempty" xml:"influx_db" yaml:"influx_db" env:"INFLUX_DB"`
UnifiUser string `json:"unifi_user,omitempty" toml:"unifi_user,omitempty" xml:"unifi_user" yaml:"unifi_user" env:"UNIFI_USER"`
UnifiPass string `json:"unifi_pass,omitempty" toml:"unifi_pass,omitempty" xml:"unifi_pass" yaml:"unifi_pass" env:"UNIFI_PASS"`
UnifiBase string `json:"unifi_url,omitempty" toml:"unifi_url,omitempty" xml:"unifi_url" yaml:"unifi_url" env:"UNIFI_URL"`
Sites []string `json:"sites,omitempty" toml:"sites,omitempty" xml:"sites" yaml:"sites" env:"POLL_SITES"`
Interval Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"`
Debug bool `json:"debug" toml:"debug" xml:"debug" yaml:"debug"`
Quiet bool `json:"quiet,omitempty" toml:"quiet,omitempty" xml:"quiet" yaml:"quiet"`
VerifySSL bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"`
CollectIDS bool `json:"collect_ids" toml:"collect_ids" xml:"collect_ids" yaml:"collect_ids"`
ReAuth bool `json:"reauthenticate" toml:"reauthenticate" xml:"reauthenticate" yaml:"reauthenticate"`
InfxBadSSL bool `json:"influx_insecure_ssl" toml:"influx_insecure_ssl" xml:"influx_insecure_ssl" yaml:"influx_insecure_ssl"`
Mode string `json:"mode" toml:"mode" xml:"mode" yaml:"mode"`
HTTPListen string `json:"http_listen" toml:"http_listen" xml:"http_listen" yaml:"http_listen"`
Namespace string `json:"namespace" toml:"namespace" xml:"namespace" yaml:"namespace"`
InfluxURL string `json:"influx_url,omitempty" toml:"influx_url,omitempty" xml:"influx_url" yaml:"influx_url"`
InfluxUser string `json:"influx_user,omitempty" toml:"influx_user,omitempty" xml:"influx_user" yaml:"influx_user"`
InfluxPass string `json:"influx_pass,omitempty" toml:"influx_pass,omitempty" xml:"influx_pass" yaml:"influx_pass"`
InfluxDB string `json:"influx_db,omitempty" toml:"influx_db,omitempty" xml:"influx_db" yaml:"influx_db"`
UnifiUser string `json:"unifi_user,omitempty" toml:"unifi_user,omitempty" xml:"unifi_user" yaml:"unifi_user"`
UnifiPass string `json:"unifi_pass,omitempty" toml:"unifi_pass,omitempty" xml:"unifi_pass" yaml:"unifi_pass"`
UnifiBase string `json:"unifi_url,omitempty" toml:"unifi_url,omitempty" xml:"unifi_url" yaml:"unifi_url"`
Sites []string `json:"sites,omitempty" toml:"sites,omitempty" xml:"sites" yaml:"sites"`
}
// Duration is used to UnmarshalTOML into a time.Duration value.
@ -119,7 +113,7 @@ func (c *Config) ParseENV() error {
t := reflect.TypeOf(Config{}) // Get tag names from the Config struct.
// Loop each Config struct member; get reflect tag & env var value; update config.
for i := 0; i < t.NumField(); i++ {
tag := t.Field(i).Tag.Get("env") // Get the ENV variable name from "env" struct tag
tag := strings.ToUpper(t.Field(i).Tag.Get("json")) // Get the ENV variable name from capitalized "json" struct tag
env := os.Getenv(ENVConfigPrefix + tag) // Then pull value from OS.
if tag == "" || env == "" {
continue // Skip if either are empty.
@ -153,5 +147,6 @@ func (c *Config) ParseENV() error {
c.SetBool(val)
}
}
return nil
}

View File

@ -1,4 +1,4 @@
package unifipoller
package poller
import (
"fmt"
@ -20,14 +20,17 @@ func (u *UnifiPoller) DumpJSONPayload() (err error) {
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "[INFO] Authenticated to UniFi Controller @ %v as user %v",
u.Config.UnifiBase, u.Config.UnifiUser)
if err := u.CheckSites(); err != nil {
return err
}
u.Unifi.ErrorLog = func(m string, v ...interface{}) {
fmt.Fprintf(os.Stderr, "[ERROR] "+m, v...)
} // Log all errors to stderr.
switch sites, err := u.GetFilteredSites(); {
case err != nil:
return err

View File

@ -1,4 +1,4 @@
package unifipoller
package poller
import (
"fmt"
@ -6,12 +6,14 @@ import (
"strings"
)
const callDepth = 2
// LogError logs an error and increments the error counter.
// Should be used in the poller loop.
func (u *UnifiPoller) LogError(err error, prefix string) {
if err != nil {
u.errorCount++
_ = log.Output(2, fmt.Sprintf("[ERROR] %v: %v", prefix, err))
_ = log.Output(callDepth, fmt.Sprintf("[ERROR] %v: %v", prefix, err))
}
}
@ -28,18 +30,18 @@ func StringInSlice(str string, slice []string) bool {
// Logf prints a log entry if quiet is false.
func (u *UnifiPoller) Logf(m string, v ...interface{}) {
if !u.Config.Quiet {
_ = log.Output(2, fmt.Sprintf("[INFO] "+m, v...))
_ = log.Output(callDepth, fmt.Sprintf("[INFO] "+m, v...))
}
}
// LogDebugf prints a debug log entry if debug is true and quite is false
func (u *UnifiPoller) LogDebugf(m string, v ...interface{}) {
if u.Config.Debug && !u.Config.Quiet {
_ = log.Output(2, fmt.Sprintf("[DEBUG] "+m, v...))
_ = log.Output(callDepth, fmt.Sprintf("[DEBUG] "+m, v...))
}
}
// LogErrorf prints an error log entry. This is used for external library logging.
func (u *UnifiPoller) LogErrorf(m string, v ...interface{}) {
_ = log.Output(2, fmt.Sprintf("[ERROR] "+m, v...))
_ = log.Output(callDepth, fmt.Sprintf("[ERROR] "+m, v...))
}

80
poller/influx.go Normal file
View File

@ -0,0 +1,80 @@
package poller
import (
"crypto/tls"
"fmt"
"github.com/davidnewhall/unifi-poller/influxunifi"
"github.com/davidnewhall/unifi-poller/metrics"
influx "github.com/influxdata/influxdb1-client/v2"
)
// GetInfluxDB returns an InfluxDB interface.
func (u *UnifiPoller) GetInfluxDB() (err error) {
u.Influx, err = influx.NewHTTPClient(influx.HTTPConfig{
Addr: u.Config.InfluxURL,
Username: u.Config.InfluxUser,
Password: u.Config.InfluxPass,
TLSConfig: &tls.Config{InsecureSkipVerify: u.Config.InfxBadSSL},
})
if err != nil {
return fmt.Errorf("influxdb: %v", err)
}
u.Logf("Logging Measurements to InfluxDB at %s as user %s", u.Config.InfluxURL, u.Config.InfluxUser)
return nil
}
// CollectAndProcess collects measurements and then reports them to InfluxDB
// Can be called once or in a ticker loop. This function and all the ones below
// handle their own logging. An error is returned so the calling function may
// determine if there was a read or write error and act on it. This is currently
// called in two places in this library. One returns an error, one does not.
func (u *UnifiPoller) CollectAndProcess() error {
metrics, err := u.CollectMetrics()
if err != nil {
return err
}
u.AugmentMetrics(metrics)
err = u.ReportMetrics(metrics)
u.LogError(err, "processing metrics")
return err
}
// ReportMetrics batches all the metrics and writes them to InfluxDB.
// This creates an InfluxDB writer, and returns an error if the write fails.
func (u *UnifiPoller) ReportMetrics(metrics *metrics.Metrics) error {
// Batch (and send) all the points.
m := &influxunifi.Metrics{Metrics: metrics}
// Make a new Influx Points Batcher.
var err error
m.BatchPoints, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.Config.InfluxDB})
if err != nil {
return fmt.Errorf("influx.NewBatchPoints: %v", err)
}
for _, err := range m.ProcessPoints() {
u.LogError(err, "influx.ProcessPoints")
}
if err = u.Influx.Write(m.BatchPoints); err != nil {
return fmt.Errorf("influxdb.Write(points): %v", err)
}
u.LogInfluxReport(m)
return nil
}
// LogInfluxReport writes a log message after exporting to influxdb.
func (u *UnifiPoller) LogInfluxReport(m *influxunifi.Metrics) {
var fields, points int
for _, p := range m.Points() {
points++
i, _ := p.Fields()
fields += len(i)
}
idsMsg := ""
if u.Config.CollectIDS {
idsMsg = fmt.Sprintf("IDS Events: %d, ", len(m.IDSList))
}
u.Logf("UniFi Measurements Recorded. Sites: %d, Clients: %d, "+
"Wireless APs: %d, Gateways: %d, Switches: %d, %sPoints: %d, Fields: %d",
len(m.Sites), len(m.Clients), len(m.UAPs),
len(m.UDMs)+len(m.USGs), len(m.USWs), idsMsg, points, fields)
}

69
poller/prometheus.go Normal file
View File

@ -0,0 +1,69 @@
package poller
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/davidnewhall/unifi-poller/metrics"
"github.com/davidnewhall/unifi-poller/promunifi"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const oneDecimalPoint = 10
// RunPrometheus starts the web server and registers the collector.
func (u *UnifiPoller) RunPrometheus() error {
u.Logf("Exporting Measurements at https://%s/metrics for Prometheus", u.Config.HTTPListen)
http.Handle("/metrics", promhttp.Handler())
prometheus.MustRegister(promunifi.NewUnifiCollector(promunifi.UnifiCollectorCnfg{
Namespace: strings.Replace(u.Config.Namespace, "-", "", -1),
CollectFn: u.ExportMetrics,
LoggingFn: u.LogExportReport,
ReportErrors: true, // XXX: Does this need to be configurable?
}))
return http.ListenAndServe(u.Config.HTTPListen, nil)
}
// ExportMetrics updates the internal metrics provided via
// HTTP at /metrics for prometheus collection.
// This is run by Prometheus as CollectFn.
func (u *UnifiPoller) ExportMetrics() (*metrics.Metrics, error) {
m, err := u.CollectMetrics()
if err != nil {
u.LogErrorf("collecting metrics: %v", err)
u.Logf("Re-authenticating to UniFi Controller")
if err := u.Unifi.Login(); err != nil {
u.LogError(err, "re-authenticating")
return nil, err
}
if m, err = u.CollectMetrics(); err != nil {
u.LogErrorf("collecting metrics: %v", err)
return nil, err
}
}
u.AugmentMetrics(m)
return m, nil
}
// LogExportReport is called after prometheus exports metrics.
// This is run by Prometheus as LoggingFn
func (u *UnifiPoller) LogExportReport(report *promunifi.Report) {
m := report.Metrics
idsMsg := ""
if u.Config.CollectIDS {
idsMsg = fmt.Sprintf(", IDS Events: %d, ", len(m.IDSList))
}
u.Logf("UniFi Measurements Exported. Site: %d, Client: %d, "+
"UAP: %d, USG/UDM: %d, USW: %d%s, Descs: %d, "+
"Metrics: %d, Errs: %d, 0s: %d, Reqs/Total: %v / %v",
len(m.Sites), len(m.Clients), len(m.UAPs), len(m.UDMs)+len(m.USGs), len(m.USWs),
idsMsg, report.Descs, report.Total, report.Errors, report.Zeros,
report.Fetch.Round(time.Millisecond/oneDecimalPoint),
report.Elapsed.Round(time.Millisecond/oneDecimalPoint))
}

144
poller/start.go Normal file
View File

@ -0,0 +1,144 @@
// Package poller provides the CLI interface to setup unifi-poller.
package poller
import (
"fmt"
"log"
"os"
"strings"
"time"
"github.com/spf13/pflag"
)
// New returns a new poller struct preloaded with default values.
// No need to call this if you call Start.c
func New() *UnifiPoller {
return &UnifiPoller{
Config: &Config{
InfluxURL: defaultInfluxURL,
InfluxUser: defaultInfluxUser,
InfluxPass: defaultInfluxPass,
InfluxDB: defaultInfluxDB,
UnifiUser: defaultUnifiUser,
UnifiPass: defaultUnifiUser,
UnifiBase: defaultUnifiURL,
Interval: Duration{defaultInterval},
Sites: []string{"all"},
HTTPListen: defaultHTTPListen,
Namespace: appName,
}, Flag: &Flag{},
}
}
// Start begins the application from a CLI.
// Parses cli flags, parses config file, parses env vars, sets up logging, then:
// - dumps a json payload OR - executes Run().
func (u *UnifiPoller) Start() error {
log.SetFlags(log.LstdFlags)
u.Flag.Parse(os.Args[1:])
if u.Flag.ShowVer {
fmt.Printf("%s v%s\n", appName, Version)
return nil // don't run anything else w/ version request.
}
if u.Flag.DumpJSON == "" { // do not print this when dumping JSON.
u.Logf("Loading Configuration File: %s", u.Flag.ConfigFile)
}
// Parse config file.
if err := u.Config.ParseFile(u.Flag.ConfigFile); err != nil {
u.Flag.Usage()
return err
}
// Update Config with ENV variable overrides.
if err := u.Config.ParseENV(); err != nil {
return err
}
if u.Flag.DumpJSON != "" {
return u.DumpJSONPayload()
}
if u.Config.Debug {
log.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate)
u.LogDebugf("Debug Logging Enabled")
}
log.Printf("[INFO] UniFi Poller v%v Starting Up! PID: %d", Version, os.Getpid())
return u.Run()
}
// Parse turns CLI arguments into data structures. Called by Start() on startup.
func (f *Flag) Parse(args []string) {
f.FlagSet = pflag.NewFlagSet(appName, pflag.ExitOnError)
f.Usage = func() {
fmt.Printf("Usage: %s [--config=/path/to/up.conf] [--version]", appName)
f.PrintDefaults()
}
f.StringVarP(&f.DumpJSON, "dumpjson", "j", "",
"This debug option prints a json payload and exits. See man page for more info.")
f.StringVarP(&f.ConfigFile, "config", "c", DefaultConfFile, "Poller config file path.")
f.BoolVarP(&f.ShowVer, "version", "v", false, "Print the version and exit.")
_ = f.FlagSet.Parse(args) // pflag.ExitOnError means this will never return error.
}
// Run picks a mode and executes the associated functions. This will do one of three things:
// 1. Start the collector routine that polls unifi and reports to influx on an interval. (default)
// 2. Run the collector one time and report the metrics to influxdb. (lambda)
// 3. Start a web server and wait for Prometheus to poll the application for metrics.
func (u *UnifiPoller) Run() error {
if err := u.GetUnifi(); err != nil {
return err
}
u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v",
u.Config.UnifiBase, u.Unifi.ServerVersion, u.Config.UnifiUser, u.Config.Sites)
switch strings.ToLower(u.Config.Mode) {
default:
if err := u.GetInfluxDB(); err != nil {
return err
}
return u.PollController()
case "influxlambda", "lambdainflux", "lambda_influx", "influx_lambda":
if err := u.GetInfluxDB(); err != nil {
return err
}
u.LastCheck = time.Now()
return u.CollectAndProcess()
case "prometheus", "exporter":
return u.RunPrometheus()
}
}
// PollController runs forever, polling UniFi and pushing to InfluxDB
// This is started by Run() after everything checks out.
func (u *UnifiPoller) PollController() error {
interval := u.Config.Interval.Round(time.Second)
log.Printf("[INFO] Everything checks out! Poller started, interval: %v", interval)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for u.LastCheck = range ticker.C {
var err error
if u.Config.ReAuth {
u.LogDebugf("Re-authenticating to UniFi Controller")
// Some users need to re-auth every interval because the cookie times out.
if err = u.Unifi.Login(); err != nil {
u.LogError(err, "re-authenticating")
}
}
if err == nil {
// Only run this if the authentication procedure didn't return error.
_ = u.CollectAndProcess()
}
if u.errorCount > 0 {
return fmt.Errorf("too many errors, stopping poller")
}
}
return nil
}

135
poller/unifi.go Normal file
View File

@ -0,0 +1,135 @@
package poller
import (
"fmt"
"strings"
"time"
"github.com/davidnewhall/unifi-poller/metrics"
"golift.io/unifi"
)
// GetUnifi returns a UniFi controller interface.
func (u *UnifiPoller) GetUnifi() (err error) {
// Create an authenticated session to the Unifi Controller.
u.Unifi, err = unifi.NewUnifi(&unifi.Config{
User: u.Config.UnifiUser,
Pass: u.Config.UnifiPass,
URL: u.Config.UnifiBase,
VerifySSL: u.Config.VerifySSL,
ErrorLog: u.LogErrorf, // Log all errors.
DebugLog: u.LogDebugf, // Log debug messages.
})
if err != nil {
return fmt.Errorf("unifi controller: %v", err)
}
u.LogDebugf("Authenticated with controller successfully")
return u.CheckSites()
}
// CheckSites makes sure the list of provided sites exists on the controller.
// This does not run in Lambda (run-once) mode.
func (u *UnifiPoller) CheckSites() error {
if strings.Contains(strings.ToLower(u.Config.Mode), "lambda") {
return nil // Skip this in lambda mode.
}
u.LogDebugf("Checking Controller Sites List")
sites, err := u.Unifi.GetSites()
if err != nil {
return err
}
msg := []string{}
for _, site := range sites {
msg = append(msg, site.Name+" ("+site.Desc+")")
}
u.Logf("Found %d site(s) on controller: %v", len(msg), strings.Join(msg, ", "))
if StringInSlice("all", u.Config.Sites) {
u.Config.Sites = []string{"all"}
return nil
}
FIRST:
for _, s := range u.Config.Sites {
for _, site := range sites {
if s == site.Name {
continue FIRST
}
}
// This is fine, it may get added later.
u.LogErrorf("configured site not found on controller: %v", s)
}
return nil
}
// CollectMetrics grabs all the measurements from a UniFi controller and returns them.
func (u *UnifiPoller) CollectMetrics() (*metrics.Metrics, error) {
m := &metrics.Metrics{TS: u.LastCheck} // At this point, it's the Current Check.
var err error
// Get the sites we care about.
m.Sites, err = u.GetFilteredSites()
u.LogError(err, "unifi.GetSites()")
if u.Config.CollectIDS {
m.IDSList, err = u.Unifi.GetIDS(m.Sites, time.Now().Add(u.Config.Interval.Duration), time.Now())
u.LogError(err, "unifi.GetIDS()")
}
// Get all the points.
m.Clients, err = u.Unifi.GetClients(m.Sites)
u.LogError(err, "unifi.GetClients()")
m.Devices, err = u.Unifi.GetDevices(m.Sites)
u.LogError(err, "unifi.GetDevices()")
return m, err
}
// AugmentMetrics is our middleware layer between collecting metrics and writing them.
// This is where we can manipuate the returned data or make arbitrary decisions.
// This function currently adds parent device names to client metrics.
func (u *UnifiPoller) AugmentMetrics(metrics *metrics.Metrics) {
if metrics == nil || metrics.Devices == nil || metrics.Clients == nil {
return
}
devices := make(map[string]string)
bssdIDs := make(map[string]string)
for _, r := range metrics.UAPs {
devices[r.Mac] = r.Name
for _, v := range r.VapTable {
bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName)
}
}
for _, r := range metrics.USGs {
devices[r.Mac] = r.Name
}
for _, r := range metrics.USWs {
devices[r.Mac] = r.Name
}
for _, r := range metrics.UDMs {
devices[r.Mac] = r.Name
}
// These come blank, so set them here.
for i, c := range metrics.Clients {
metrics.Clients[i].SwName = devices[c.SwMac]
metrics.Clients[i].ApName = devices[c.ApMac]
metrics.Clients[i].GwName = devices[c.GwMac]
metrics.Clients[i].RadioDescription = bssdIDs[metrics.Clients[i].Bssid] + metrics.Clients[i].RadioProto
}
}
// GetFilteredSites returns a list of sites to fetch data for.
// Omits requested but unconfigured sites. Grabs the full list from the
// controller and returns the sites provided in the config file.
func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) {
sites, err := u.Unifi.GetSites()
if err != nil {
return nil, err
} else if len(u.Config.Sites) < 1 || StringInSlice("all", u.Config.Sites) {
return sites, nil
}
var i int
for _, s := range sites {
// Only include valid sites in the request filter.
if StringInSlice(s.Name, u.Config.Sites) {
sites[i] = s
i++
}
}
return sites[:i], nil
}

4
promunifi/README.md Normal file
View File

@ -0,0 +1,4 @@
# prometheus
This package provides the interface to turn UniFi measurements into prometheus
exported metrics. Requires the poller package for actual UniFi data collection.

129
promunifi/clients.go Normal file
View File

@ -0,0 +1,129 @@
package promunifi
import (
"github.com/prometheus/client_golang/prometheus"
"golift.io/unifi"
)
type uclient struct {
Anomalies *prometheus.Desc
BytesR *prometheus.Desc
CCQ *prometheus.Desc
Satisfaction *prometheus.Desc
Noise *prometheus.Desc
RoamCount *prometheus.Desc
RSSI *prometheus.Desc
RxBytes *prometheus.Desc
RxBytesR *prometheus.Desc
RxPackets *prometheus.Desc
RxRate *prometheus.Desc
Signal *prometheus.Desc
TxBytes *prometheus.Desc
TxBytesR *prometheus.Desc
TxPackets *prometheus.Desc
TxRetries *prometheus.Desc
TxPower *prometheus.Desc
TxRate *prometheus.Desc
Uptime *prometheus.Desc
WifiTxAttempts *prometheus.Desc
WiredRxBytes *prometheus.Desc
WiredRxBytesR *prometheus.Desc
WiredRxPackets *prometheus.Desc
WiredTxBytes *prometheus.Desc
WiredTxBytesR *prometheus.Desc
WiredTxPackets *prometheus.Desc
DpiStatsApp *prometheus.Desc
DpiStatsCat *prometheus.Desc
DpiStatsRxBytes *prometheus.Desc
DpiStatsRxPackets *prometheus.Desc
DpiStatsTxBytes *prometheus.Desc
DpiStatsTxPackets *prometheus.Desc
}
func descClient(ns string) *uclient {
labels := []string{"name", "mac", "site_name", "gw_name", "sw_name", "vlan", "ip", "oui", "network", "sw_port", "ap_name", "wired"}
labelW := append([]string{"radio_name", "radio", "radio_proto", "channel", "essid", "bssid", "radio_desc"}, labels...)
return &uclient{
Anomalies: prometheus.NewDesc(ns+"anomalies", "Client Anomalies", labelW, nil),
BytesR: prometheus.NewDesc(ns+"transfer_rate_bytes", "Client Data Rate", labelW, nil),
CCQ: prometheus.NewDesc(ns+"ccq_percent", "Client Connection Quality", labelW, nil),
Satisfaction: prometheus.NewDesc(ns+"satisfaction_percent", "Client Satisfaction", labelW, nil),
Noise: prometheus.NewDesc(ns+"noise_db", "Client AP Noise", labelW, nil),
RoamCount: prometheus.NewDesc(ns+"roam_count_total", "Client Roam Counter", labelW, nil),
RSSI: prometheus.NewDesc(ns+"rssi_db", "Client RSSI", labelW, nil),
RxBytes: prometheus.NewDesc(ns+"receive_bytes_total", "Client Receive Bytes", labels, nil),
RxBytesR: prometheus.NewDesc(ns+"receive_rate_bytes", "Client Receive Data Rate", labels, nil),
RxPackets: prometheus.NewDesc(ns+"receive_packets_total", "Client Receive Packets", labels, nil),
RxRate: prometheus.NewDesc(ns+"radio_receive_rate_bps", "Client Receive Rate", labelW, nil),
Signal: prometheus.NewDesc(ns+"radio_signal_db", "Client Signal Strength", labelW, nil),
TxBytes: prometheus.NewDesc(ns+"transmit_bytes_total", "Client Transmit Bytes", labels, nil),
TxBytesR: prometheus.NewDesc(ns+"transmit_rate_bytes", "Client Transmit Data Rate", labels, nil),
TxPackets: prometheus.NewDesc(ns+"transmit_packets_total", "Client Transmit Packets", labels, nil),
TxRetries: prometheus.NewDesc(ns+"transmit_retries_total", "Client Transmit Retries", labels, nil),
TxPower: prometheus.NewDesc(ns+"radio_transmit_power_dbm", "Client Transmit Power", labelW, nil),
TxRate: prometheus.NewDesc(ns+"radio_transmit_rate_bps", "Client Transmit Rate", labelW, nil),
WifiTxAttempts: prometheus.NewDesc(ns+"wifi_attempts_transmit_total", "Client Wifi Transmit Attempts", labelW, nil),
Uptime: prometheus.NewDesc(ns+"uptime_seconds", "Client Uptime", labelW, nil),
/* needs more "looking into"
DpiStatsApp: prometheus.NewDesc(ns+"dpi_stats_app", "Client DPI Stats App", labels, nil),
DpiStatsCat: prometheus.NewDesc(ns+"dpi_stats_cat", "Client DPI Stats Cat", labels, nil),
DpiStatsRxBytes: prometheus.NewDesc(ns+"dpi_stats_receive_bytes_total", "Client DPI Stats Receive Bytes", labels, nil),
DpiStatsRxPackets: prometheus.NewDesc(ns+"dpi_stats_receive_packets_total", "Client DPI Stats Receive Packets", labels, nil),
DpiStatsTxBytes: prometheus.NewDesc(ns+"dpi_stats_transmit_bytes_total", "Client DPI Stats Transmit Bytes", labels, nil),
DpiStatsTxPackets: prometheus.NewDesc(ns+"dpi_stats_transmit_packets_total", "Client DPI Stats Transmit Packets", labels, nil),
*/
}
}
func (u *promUnifi) exportClient(r report, c *unifi.Client) {
labels := []string{c.Name, c.Mac, c.SiteName, c.GwName, c.SwName, c.Vlan.Txt, c.IP, c.Oui, c.Network, c.SwPort.Txt, c.ApName, ""}
labelW := append([]string{c.RadioName, c.Radio, c.RadioProto, c.Channel.Txt, c.Essid, c.Bssid, c.RadioDescription}, labels...)
if c.IsWired.Val {
labels[len(labels)-1] = "true"
labelW[len(labelW)-1] = "true"
r.send([]*metric{
{u.Client.RxBytes, prometheus.CounterValue, c.WiredRxBytes, labels},
{u.Client.RxBytesR, prometheus.GaugeValue, c.WiredRxBytesR, labels},
{u.Client.RxPackets, prometheus.CounterValue, c.WiredRxPackets, labels},
{u.Client.TxBytes, prometheus.CounterValue, c.WiredTxBytes, labels},
{u.Client.TxBytesR, prometheus.GaugeValue, c.WiredTxBytesR, labels},
{u.Client.TxPackets, prometheus.CounterValue, c.WiredTxPackets, labels},
})
} else {
labels[len(labels)-1] = "false"
labelW[len(labelW)-1] = "false"
r.send([]*metric{
{u.Client.Anomalies, prometheus.CounterValue, c.Anomalies, labelW},
{u.Client.CCQ, prometheus.GaugeValue, c.Ccq / 10, labelW},
{u.Client.Satisfaction, prometheus.GaugeValue, c.Satisfaction, labelW},
{u.Client.Noise, prometheus.GaugeValue, c.Noise, labelW},
{u.Client.RoamCount, prometheus.CounterValue, c.RoamCount, labelW},
{u.Client.RSSI, prometheus.GaugeValue, c.Rssi, labelW},
{u.Client.Signal, prometheus.GaugeValue, c.Signal, labelW},
{u.Client.TxPower, prometheus.GaugeValue, c.TxPower, labelW},
{u.Client.TxRate, prometheus.GaugeValue, c.TxRate * 1000, labelW},
{u.Client.WifiTxAttempts, prometheus.CounterValue, c.WifiTxAttempts, labelW},
{u.Client.RxRate, prometheus.GaugeValue, c.RxRate * 1000, labelW},
{u.Client.TxRetries, prometheus.CounterValue, c.TxRetries, labels},
{u.Client.TxBytes, prometheus.CounterValue, c.TxBytes, labels},
{u.Client.TxBytesR, prometheus.GaugeValue, c.TxBytesR, labels},
{u.Client.TxPackets, prometheus.CounterValue, c.TxPackets, labels},
{u.Client.RxBytes, prometheus.CounterValue, c.RxBytes, labels},
{u.Client.RxBytesR, prometheus.GaugeValue, c.RxBytesR, labels},
{u.Client.RxPackets, prometheus.CounterValue, c.RxPackets, labels},
{u.Client.BytesR, prometheus.GaugeValue, c.BytesR, labelW},
})
}
r.send([]*metric{
{u.Client.Uptime, prometheus.GaugeValue, c.Uptime, labelW},
/* needs more "looking into"
{u.Client.DpiStatsApp, prometheus.GaugeValue, c.DpiStats.App, labels},
{u.Client.DpiStatsCat, prometheus.GaugeValue, c.DpiStats.Cat, labels},
{u.Client.DpiStatsRxBytes, prometheus.CounterValue, c.DpiStats.RxBytes, labels},
{u.Client.DpiStatsRxPackets, prometheus.CounterValue, c.DpiStats.RxPackets, labels},
{u.Client.DpiStatsTxBytes, prometheus.CounterValue, c.DpiStats.TxBytes, labels},
{u.Client.DpiStatsTxPackets, prometheus.CounterValue, c.DpiStats.TxPackets, labels},
*/
})
}

150
promunifi/collector.go Normal file
View File

@ -0,0 +1,150 @@
// 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 promUnifi struct {
Config UnifiCollectorCnfg
Client *uclient
Device *unifiDevice
UAP *uap
USG *usg
USW *usw
Site *site
}
type metric 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 // Total count of metrics recorded.
Errors int // Total count of errors recording metrics.
Zeros int // Total count of metrics equal to zero.
Descs int // Total count of unique metrics descriptions.
Metrics *metrics.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
cf UnifiCollectorCnfg
}
// 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 &promUnifi{
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 *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 the input method to get
// the current metrics (from another package) then exports them for prometheus.
func (u *promUnifi) Collect(ch chan<- prometheus.Metric) {
var err error
r := &Report{cf: u.Config, ch: make(chan []*metric, buffer), Start: time.Now()}
defer r.close()
if r.Metrics, err = r.cf.CollectFn(); err != nil {
r.error(ch, prometheus.NewInvalidDesc(fmt.Errorf("metric fetch failed")), err)
return
}
r.Fetch = time.Since(r.Start)
if r.Metrics.Devices == nil {
r.Metrics.Devices = &unifi.Devices{}
}
// Pass Report interface into our collecting and reporting methods.
go u.exportMetrics(r, ch)
for _, f := range []func(report){u.loopClients, u.loopSites, u.loopUAPs, u.loopUSWs, u.loopUSGs, u.loopUDMs} {
r.add()
go f(r) // in loops.go.
}
}
// 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 *promUnifi) exportMetrics(r report, ch chan<- prometheus.Metric) {
descs := make(map[*prometheus.Desc]bool) // used as a counter
defer r.report(descs)
for newMetrics := range r.channel() {
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()
}
}

47
promunifi/loops.go Normal file
View File

@ -0,0 +1,47 @@
package promunifi
// This file contains all the loop methods for each device type, clients and sites.
// Moved them here to consolate clutter from the other files. Also, if these change,
// they usually all change at once since they're pretty much the same code.
func (u *promUnifi) loopSites(r report) {
defer r.done()
for _, s := range r.metrics().Sites {
u.exportSite(r, s)
}
}
func (u *promUnifi) loopUAPs(r report) {
defer r.done()
for _, d := range r.metrics().UAPs {
u.exportUAP(r, d)
}
}
func (u *promUnifi) loopUDMs(r report) {
defer r.done()
for _, d := range r.metrics().UDMs {
u.exportUDM(r, d)
}
}
func (u *promUnifi) loopUSGs(r report) {
defer r.done()
for _, d := range r.metrics().USGs {
u.exportUSG(r, d)
}
}
func (u *promUnifi) loopUSWs(r report) {
defer r.done()
for _, d := range r.metrics().USWs {
u.exportUSW(r, d)
}
}
func (u *promUnifi) loopClients(r report) {
defer r.done()
for _, c := range r.metrics().Clients {
u.exportClient(r, c)
}
}

78
promunifi/report.go Normal file
View File

@ -0,0 +1,78 @@
package promunifi
import (
"fmt"
"time"
"github.com/davidnewhall/unifi-poller/metrics"
"github.com/prometheus/client_golang/prometheus"
)
// This file contains the report interface.
// This interface can be mocked and overridden for tests.
// report is an internal interface used to "process metrics"
type report interface {
add()
done()
send([]*metric)
metrics() *metrics.Metrics
channel() chan []*metric
report(descs map[*prometheus.Desc]bool)
export(m *metric, v float64) prometheus.Metric
error(ch chan<- prometheus.Metric, d *prometheus.Desc, v interface{})
}
// satisfy gomnd
const one = 1
func (r *Report) add() {
r.wg.Add(one)
}
func (r *Report) done() {
r.wg.Add(-one)
}
func (r *Report) send(m []*metric) {
r.wg.Add(one)
r.ch <- m
}
func (r *Report) metrics() *metrics.Metrics {
return r.Metrics
}
func (r *Report) channel() chan []*metric {
return r.ch
}
func (r *Report) report(descs map[*prometheus.Desc]bool) {
if r.cf.LoggingFn == nil {
return
}
r.Descs = len(descs)
r.cf.LoggingFn(r)
}
func (r *Report) export(m *metric, v float64) prometheus.Metric {
r.Total++
if v == 0 {
r.Zeros++
}
return prometheus.MustNewConstMetric(m.Desc, m.ValueType, v, m.Labels...)
}
func (r *Report) error(ch chan<- prometheus.Metric, d *prometheus.Desc, v interface{}) {
r.Errors++
if r.cf.ReportErrors {
ch <- prometheus.NewInvalidMetric(d, fmt.Errorf("error: %v", v))
}
}
// close is not part of the interface.
func (r *Report) close() {
r.wg.Wait()
r.Elapsed = time.Since(r.Start)
close(r.ch)
}

132
promunifi/site.go Normal file
View File

@ -0,0 +1,132 @@
package promunifi
import (
"github.com/prometheus/client_golang/prometheus"
"golift.io/unifi"
)
type site struct {
NumUser *prometheus.Desc
NumGuest *prometheus.Desc
NumIot *prometheus.Desc
TxBytesR *prometheus.Desc
RxBytesR *prometheus.Desc
NumAp *prometheus.Desc
NumAdopted *prometheus.Desc
NumDisabled *prometheus.Desc
NumDisconnected *prometheus.Desc
NumPending *prometheus.Desc
NumGw *prometheus.Desc
NumSw *prometheus.Desc
NumSta *prometheus.Desc
Latency *prometheus.Desc
Drops *prometheus.Desc
Uptime *prometheus.Desc
XputUp *prometheus.Desc
XputDown *prometheus.Desc
SpeedtestPing *prometheus.Desc
RemoteUserNumActive *prometheus.Desc
RemoteUserNumInactive *prometheus.Desc
RemoteUserRxBytes *prometheus.Desc
RemoteUserTxBytes *prometheus.Desc
RemoteUserRxPackets *prometheus.Desc
RemoteUserTxPackets *prometheus.Desc
}
func descSite(ns string) *site {
labels := []string{"subsystem", "status", "site_name"}
return &site{
NumUser: prometheus.NewDesc(ns+"users", "Number of Users", labels, nil),
NumGuest: prometheus.NewDesc(ns+"guests", "Number of Guests", labels, nil),
NumIot: prometheus.NewDesc(ns+"iots", "Number of IoT Devices", labels, nil),
TxBytesR: prometheus.NewDesc(ns+"transmit_rate_bytes", "Bytes Transmit Rate", labels, nil),
RxBytesR: prometheus.NewDesc(ns+"receive_rate_bytes", "Bytes Receive Rate", labels, nil),
NumAp: prometheus.NewDesc(ns+"aps", "Access Point Count", labels, nil),
NumAdopted: prometheus.NewDesc(ns+"adopted", "Adoption Count", labels, nil),
NumDisabled: prometheus.NewDesc(ns+"disabled", "Disabled Count", labels, nil),
NumDisconnected: prometheus.NewDesc(ns+"disconnected", "Disconnected Count", labels, nil),
NumPending: prometheus.NewDesc(ns+"pending", "Pending Count", labels, nil),
NumGw: prometheus.NewDesc(ns+"gateways", "Gateway Count", labels, nil),
NumSw: prometheus.NewDesc(ns+"switches", "Switch Count", labels, nil),
NumSta: prometheus.NewDesc(ns+"stations", "Station Count", labels, nil),
Latency: prometheus.NewDesc(ns+"latency_seconds", "Latency", labels, nil),
Uptime: prometheus.NewDesc(ns+"uptime_seconds", "Uptime", labels, nil),
Drops: prometheus.NewDesc(ns+"intenet_drops_total", "Internet (WAN) Disconnections", labels, nil),
XputUp: prometheus.NewDesc(ns+"xput_up_rate", "Speedtest Upload", labels, nil),
XputDown: prometheus.NewDesc(ns+"xput_down_rate", "Speedtest Download", labels, nil),
SpeedtestPing: prometheus.NewDesc(ns+"speedtest_ping", "Speedtest Ping", labels, nil),
RemoteUserNumActive: prometheus.NewDesc(ns+"remote_user_active", "Remote Users Active", labels, nil),
RemoteUserNumInactive: prometheus.NewDesc(ns+"remote_user_inactive", "Remote Users Inactive", labels, nil),
RemoteUserRxBytes: prometheus.NewDesc(ns+"remote_user_receive_bytes_total", "Remote Users Receive Bytes", labels, nil),
RemoteUserTxBytes: prometheus.NewDesc(ns+"remote_user_transmit_bytes_total", "Remote Users Transmit Bytes", labels, nil),
RemoteUserRxPackets: prometheus.NewDesc(ns+"remote_user_receive_packets_total", "Remote Users Receive Packets", labels, nil),
RemoteUserTxPackets: prometheus.NewDesc(ns+"remote_user_transmit_packets_total", "Remote Users Transmit Packets", labels, nil),
}
}
func (u *promUnifi) exportSite(r report, s *unifi.Site) {
for _, h := range s.Health {
labels := []string{h.Subsystem, h.Status, s.SiteName}
switch h.Subsystem {
case "www":
r.send([]*metric{
{u.Site.TxBytesR, prometheus.GaugeValue, h.TxBytesR, labels},
{u.Site.RxBytesR, prometheus.GaugeValue, h.RxBytesR, labels},
{u.Site.Uptime, prometheus.GaugeValue, h.Latency, labels},
{u.Site.Latency, prometheus.GaugeValue, h.Latency.Val / 1000, labels},
{u.Site.XputUp, prometheus.GaugeValue, h.XputUp, labels},
{u.Site.XputDown, prometheus.GaugeValue, h.XputDown, labels},
{u.Site.SpeedtestPing, prometheus.GaugeValue, h.SpeedtestPing, labels},
{u.Site.Drops, prometheus.CounterValue, h.Drops, labels},
})
case "wlan":
r.send([]*metric{
{u.Site.TxBytesR, prometheus.GaugeValue, h.TxBytesR, labels},
{u.Site.RxBytesR, prometheus.GaugeValue, h.RxBytesR, labels},
{u.Site.NumAdopted, prometheus.GaugeValue, h.NumAdopted, labels},
{u.Site.NumDisconnected, prometheus.GaugeValue, h.NumDisconnected, labels},
{u.Site.NumPending, prometheus.GaugeValue, h.NumPending, labels},
{u.Site.NumUser, prometheus.GaugeValue, h.NumUser, labels},
{u.Site.NumGuest, prometheus.GaugeValue, h.NumGuest, labels},
{u.Site.NumIot, prometheus.GaugeValue, h.NumIot, labels},
{u.Site.NumAp, prometheus.GaugeValue, h.NumAp, labels},
{u.Site.NumDisabled, prometheus.GaugeValue, h.NumDisabled, labels},
})
case "wan":
r.send([]*metric{
{u.Site.TxBytesR, prometheus.GaugeValue, h.TxBytesR, labels},
{u.Site.RxBytesR, prometheus.GaugeValue, h.RxBytesR, labels},
{u.Site.NumAdopted, prometheus.GaugeValue, h.NumAdopted, labels},
{u.Site.NumDisconnected, prometheus.GaugeValue, h.NumDisconnected, labels},
{u.Site.NumPending, prometheus.GaugeValue, h.NumPending, labels},
{u.Site.NumGw, prometheus.GaugeValue, h.NumGw, labels},
{u.Site.NumSta, prometheus.GaugeValue, h.NumSta, labels},
})
case "lan":
r.send([]*metric{
{u.Site.TxBytesR, prometheus.GaugeValue, h.TxBytesR, labels},
{u.Site.RxBytesR, prometheus.GaugeValue, h.RxBytesR, labels},
{u.Site.NumAdopted, prometheus.GaugeValue, h.NumAdopted, labels},
{u.Site.NumDisconnected, prometheus.GaugeValue, h.NumDisconnected, labels},
{u.Site.NumPending, prometheus.GaugeValue, h.NumPending, labels},
{u.Site.NumUser, prometheus.GaugeValue, h.NumUser, labels},
{u.Site.NumGuest, prometheus.GaugeValue, h.NumGuest, labels},
{u.Site.NumIot, prometheus.GaugeValue, h.NumIot, labels},
{u.Site.NumSw, prometheus.GaugeValue, h.NumSw, labels},
})
case "vpn":
r.send([]*metric{
{u.Site.RemoteUserNumActive, prometheus.GaugeValue, h.RemoteUserNumActive, labels},
{u.Site.RemoteUserNumInactive, prometheus.GaugeValue, h.RemoteUserNumInactive, labels},
{u.Site.RemoteUserRxBytes, prometheus.CounterValue, h.RemoteUserRxBytes, labels},
{u.Site.RemoteUserTxBytes, prometheus.CounterValue, h.RemoteUserTxBytes, labels},
{u.Site.RemoteUserRxPackets, prometheus.CounterValue, h.RemoteUserRxPackets, labels},
{u.Site.RemoteUserTxPackets, prometheus.CounterValue, h.RemoteUserTxPackets, labels},
})
}
}
}

337
promunifi/uap.go Normal file
View File

@ -0,0 +1,337 @@
package promunifi
import (
"github.com/prometheus/client_golang/prometheus"
"golift.io/unifi"
)
type uap struct {
// Ap Traffic Stats
ApWifiTxDropped *prometheus.Desc
ApRxErrors *prometheus.Desc
ApRxDropped *prometheus.Desc
ApRxFrags *prometheus.Desc
ApRxCrypts *prometheus.Desc
ApTxPackets *prometheus.Desc
ApTxBytes *prometheus.Desc
ApTxErrors *prometheus.Desc
ApTxDropped *prometheus.Desc
ApTxRetries *prometheus.Desc
ApRxPackets *prometheus.Desc
ApRxBytes *prometheus.Desc
WifiTxAttempts *prometheus.Desc
MacFilterRejections *prometheus.Desc
// VAP Stats
VAPCcq *prometheus.Desc
VAPMacFilterRejections *prometheus.Desc
VAPNumSatisfactionSta *prometheus.Desc
VAPAvgClientSignal *prometheus.Desc
VAPSatisfaction *prometheus.Desc
VAPSatisfactionNow *prometheus.Desc
VAPDNSAvgLatency *prometheus.Desc
VAPRxBytes *prometheus.Desc
VAPRxCrypts *prometheus.Desc
VAPRxDropped *prometheus.Desc
VAPRxErrors *prometheus.Desc
VAPRxFrags *prometheus.Desc
VAPRxNwids *prometheus.Desc
VAPRxPackets *prometheus.Desc
VAPTxBytes *prometheus.Desc
VAPTxDropped *prometheus.Desc
VAPTxErrors *prometheus.Desc
VAPTxPackets *prometheus.Desc
VAPTxPower *prometheus.Desc
VAPTxRetries *prometheus.Desc
VAPTxCombinedRetries *prometheus.Desc
VAPTxDataMpduBytes *prometheus.Desc
VAPTxRtsRetries *prometheus.Desc
VAPTxSuccess *prometheus.Desc
VAPTxTotal *prometheus.Desc
VAPTxGoodbytes *prometheus.Desc
VAPTxLatAvg *prometheus.Desc
VAPTxLatMax *prometheus.Desc
VAPTxLatMin *prometheus.Desc
VAPRxGoodbytes *prometheus.Desc
VAPRxLatAvg *prometheus.Desc
VAPRxLatMax *prometheus.Desc
VAPRxLatMin *prometheus.Desc
VAPWifiTxLatencyMovAvg *prometheus.Desc
VAPWifiTxLatencyMovMax *prometheus.Desc
VAPWifiTxLatencyMovMin *prometheus.Desc
VAPWifiTxLatencyMovTotal *prometheus.Desc
VAPWifiTxLatencyMovCount *prometheus.Desc
// Radio Stats
RadioCurrentAntennaGain *prometheus.Desc
RadioHt *prometheus.Desc
RadioMaxTxpower *prometheus.Desc
RadioMinTxpower *prometheus.Desc
RadioNss *prometheus.Desc
RadioRadioCaps *prometheus.Desc
RadioTxPower *prometheus.Desc
RadioAstBeXmit *prometheus.Desc
RadioChannel *prometheus.Desc
RadioCuSelfRx *prometheus.Desc
RadioCuSelfTx *prometheus.Desc
RadioCuTotal *prometheus.Desc
RadioExtchannel *prometheus.Desc
RadioGain *prometheus.Desc
RadioGuestNumSta *prometheus.Desc
RadioNumSta *prometheus.Desc
RadioUserNumSta *prometheus.Desc
RadioTxPackets *prometheus.Desc
RadioTxRetries *prometheus.Desc
}
func descUAP(ns string) *uap {
// labels := []string{"ip", "version", "model", "serial", "type", "mac", "site_name", "name"}
labelA := []string{"stat", "site_name", "name"} // stat + labels[6:]
labelV := []string{"vap_name", "bssid", "radio", "radio_name", "essid", "usage", "site_name", "name"}
labelR := []string{"radio_name", "radio", "site_name", "name"}
return &uap{
// 3x each - stat table: total, guest, user
ApWifiTxDropped: prometheus.NewDesc(ns+"stat_wifi_transmt_dropped_total", "Wifi Transmissions Dropped", labelA, nil),
ApRxErrors: prometheus.NewDesc(ns+"stat_receive_errors_total", "Receive Errors", labelA, nil),
ApRxDropped: prometheus.NewDesc(ns+"stat_receive_dropped_total", "Receive Dropped", labelA, nil),
ApRxFrags: prometheus.NewDesc(ns+"stat_receive_frags_total", "Received Frags", labelA, nil),
ApRxCrypts: prometheus.NewDesc(ns+"stat_receive_crypts_total", "Receive Crypts", labelA, nil),
ApTxPackets: prometheus.NewDesc(ns+"stat_transmit_packets_total", "Transmit Packets", labelA, nil),
ApTxBytes: prometheus.NewDesc(ns+"stat_transmit_bytes_total", "Transmit Bytes", labelA, nil),
ApTxErrors: prometheus.NewDesc(ns+"stat_transmit_errors_total", "Transmit Errors", labelA, nil),
ApTxDropped: prometheus.NewDesc(ns+"stat_transmit_dropped_total", "Transmit Dropped", labelA, nil),
ApTxRetries: prometheus.NewDesc(ns+"stat_retries_tx_total", "Transmit Retries", labelA, nil),
ApRxPackets: prometheus.NewDesc(ns+"stat_receive_packets_total", "Receive Packets", labelA, nil),
ApRxBytes: prometheus.NewDesc(ns+"stat_receive_bytes_total", "Receive Bytes", labelA, nil),
WifiTxAttempts: prometheus.NewDesc(ns+"stat_wifi_transmit_attempts_total", "Wifi Transmission Attempts", labelA, nil),
MacFilterRejections: prometheus.NewDesc(ns+"stat_mac_filter_rejects_total", "MAC Filter Rejections", labelA, nil),
// N each - 1 per Virtual AP (VAP)
VAPCcq: prometheus.NewDesc(ns+"vap_ccq", "VAP Client Connection Quality", labelV, nil),
VAPMacFilterRejections: prometheus.NewDesc(ns+"vap_mac_filter_rejects_total", "VAP MAC Filter Rejections", labelV, nil),
VAPNumSatisfactionSta: prometheus.NewDesc(ns+"vap_satisfaction_stations", "VAP Number Satisifaction Stations", labelV, nil),
VAPAvgClientSignal: prometheus.NewDesc(ns+"vap_average_client_signal", "VAP Average Client Signal", labelV, nil),
VAPSatisfaction: prometheus.NewDesc(ns+"vap_satisfaction_percent", "VAP Satisfaction", labelV, nil),
VAPSatisfactionNow: prometheus.NewDesc(ns+"vap_satisfaction_now_percent", "VAP Satisfaction Now", labelV, nil),
VAPDNSAvgLatency: prometheus.NewDesc(ns+"vap_dns_latency_average_seconds", "VAP DNS Latency Average", labelV, nil),
VAPRxBytes: prometheus.NewDesc(ns+"vap_receive_bytes_total", "VAP Bytes Received", labelV, nil),
VAPRxCrypts: prometheus.NewDesc(ns+"vap_receive_crypts_total", "VAP Crypts Received", labelV, nil),
VAPRxDropped: prometheus.NewDesc(ns+"vap_receive_dropped_total", "VAP Dropped Received", labelV, nil),
VAPRxErrors: prometheus.NewDesc(ns+"vap_receive_errors_total", "VAP Errors Received", labelV, nil),
VAPRxFrags: prometheus.NewDesc(ns+"vap_receive_frags_total", "VAP Frags Received", labelV, nil),
VAPRxNwids: prometheus.NewDesc(ns+"vap_receive_nwids_total", "VAP Nwids Received", labelV, nil),
VAPRxPackets: prometheus.NewDesc(ns+"vap_receive_packets_total", "VAP Packets Received", labelV, nil),
VAPTxBytes: prometheus.NewDesc(ns+"vap_transmit_bytes_total", "VAP Bytes Transmitted", labelV, nil),
VAPTxDropped: prometheus.NewDesc(ns+"vap_transmit_dropped_total", "VAP Dropped Transmitted", labelV, nil),
VAPTxErrors: prometheus.NewDesc(ns+"vap_transmit_errors_total", "VAP Errors Transmitted", labelV, nil),
VAPTxPackets: prometheus.NewDesc(ns+"vap_transmit_packets_total", "VAP Packets Transmitted", labelV, nil),
VAPTxPower: prometheus.NewDesc(ns+"vap_transmit_power", "VAP Transmit Power", labelV, nil),
VAPTxRetries: prometheus.NewDesc(ns+"vap_transmit_retries_total", "VAP Retries Transmitted", labelV, nil),
VAPTxCombinedRetries: prometheus.NewDesc(ns+"vap_transmit_retries_combined_total", "VAP Retries Combined Transmitted", labelV, nil),
VAPTxDataMpduBytes: prometheus.NewDesc(ns+"vap_data_mpdu_transmit_bytes_total", "VAP Data MPDU Bytes Transmitted", labelV, nil),
VAPTxRtsRetries: prometheus.NewDesc(ns+"vap_transmit_rts_retries_total", "VAP RTS Retries Transmitted", labelV, nil),
VAPTxSuccess: prometheus.NewDesc(ns+"vap_transmit_success_total", "VAP Success Transmits", labelV, nil),
VAPTxTotal: prometheus.NewDesc(ns+"vap_transmit_total", "VAP Transmit Total", labelV, nil),
VAPTxGoodbytes: prometheus.NewDesc(ns+"vap_transmit_goodbyes", "VAP Goodbyes Transmitted", labelV, nil),
VAPTxLatAvg: prometheus.NewDesc(ns+"vap_transmit_latency_average_seconds", "VAP Latency Average Transmit", labelV, nil),
VAPTxLatMax: prometheus.NewDesc(ns+"vap_transmit_latency_maximum_seconds", "VAP Latency Maximum Transmit", labelV, nil),
VAPTxLatMin: prometheus.NewDesc(ns+"vap_transmit_latency_minimum_seconds", "VAP Latency Minimum Transmit", labelV, nil),
VAPRxGoodbytes: prometheus.NewDesc(ns+"vap_receive_goodbyes", "VAP Goodbyes Received", labelV, nil),
VAPRxLatAvg: prometheus.NewDesc(ns+"vap_receive_latency_average_seconds", "VAP Latency Average Receive", labelV, nil),
VAPRxLatMax: prometheus.NewDesc(ns+"vap_receive_latency_maximum_seconds", "VAP Latency Maximum Receive", labelV, nil),
VAPRxLatMin: prometheus.NewDesc(ns+"vap_receive_latency_minimum_seconds", "VAP Latency Minimum Receive", labelV, nil),
VAPWifiTxLatencyMovAvg: prometheus.NewDesc(ns+"vap_transmit_latency_moving_avg_seconds", "VAP Latency Moving Average Tramsit", labelV, nil),
VAPWifiTxLatencyMovMax: prometheus.NewDesc(ns+"vap_transmit_latency_moving_max_seconds", "VAP Latency Moving Maximum Tramsit", labelV, nil),
VAPWifiTxLatencyMovMin: prometheus.NewDesc(ns+"vap_transmit_latency_moving_min_seconds", "VAP Latency Moving Minimum Tramsit", labelV, nil),
VAPWifiTxLatencyMovTotal: prometheus.NewDesc(ns+"vap_transmit_latency_moving_total", "VAP Latency Moving Total Tramsit", labelV, nil),
VAPWifiTxLatencyMovCount: prometheus.NewDesc(ns+"vap_transmit_latency_moving_count", "VAP Latency Moving Count Tramsit", labelV, nil),
// N each - 1 per Radio. 1-4 radios per AP usually
RadioCurrentAntennaGain: prometheus.NewDesc(ns+"radio_current_antenna_gain", "Radio Current Antenna Gain", labelR, nil),
RadioHt: prometheus.NewDesc(ns+"radio_ht", "Radio HT", labelR, nil),
RadioMaxTxpower: prometheus.NewDesc(ns+"radio_max_transmit_power", "Radio Maximum Transmit Power", labelR, nil),
RadioMinTxpower: prometheus.NewDesc(ns+"radio_min_transmit_power", "Radio Minimum Transmit Power", labelR, nil),
RadioNss: prometheus.NewDesc(ns+"radio_nss", "Radio Nss", labelR, nil),
RadioRadioCaps: prometheus.NewDesc(ns+"radio_caps", "Radio Capabilities", labelR, nil),
RadioTxPower: prometheus.NewDesc(ns+"radio_transmit_power", "Radio Transmit Power", labelR, nil),
RadioAstBeXmit: prometheus.NewDesc(ns+"radio_ast_be_xmit", "Radio AstBe Transmit", labelR, nil),
RadioChannel: prometheus.NewDesc(ns+"radio_channel", "Radio Channel", labelR, nil),
RadioCuSelfRx: prometheus.NewDesc(ns+"radio_channel_utilization_receive", "Radio Channel Utilization Receive", labelR, nil),
RadioCuSelfTx: prometheus.NewDesc(ns+"radio_channel_utilization_transmit", "Radio Channel Utilization Transmit", labelR, nil),
RadioCuTotal: prometheus.NewDesc(ns+"radio_channel_utilization_total", "Radio Channel Utilization", labelR, nil),
RadioExtchannel: prometheus.NewDesc(ns+"radio_ext_channel", "Radio Ext Channel", labelR, nil),
RadioGain: prometheus.NewDesc(ns+"radio_gain", "Radio Gain", labelR, nil),
RadioGuestNumSta: prometheus.NewDesc(ns+"radio_guest_stations", "Radio Guest Station Count", labelR, nil),
RadioNumSta: prometheus.NewDesc(ns+"radio_stations", "Radio Total Station Count", labelR, nil),
RadioUserNumSta: prometheus.NewDesc(ns+"radio_user_stations", "Radio User Station Count", labelR, nil),
RadioTxPackets: prometheus.NewDesc(ns+"radio_transmit_packets_total", "Radio Transmitted Packets", labelR, nil),
RadioTxRetries: prometheus.NewDesc(ns+"radio_transmit_retries_total", "Radio Transmit Retries", labelR, nil),
}
}
func (u *promUnifi) exportUAP(r report, d *unifi.UAP) {
labels := []string{d.IP, d.Version, d.Model, d.Serial, d.Type, d.Mac, d.SiteName, d.Name}
// Wireless System Data.
r.send([]*metric{
{u.Device.Uptime, prometheus.GaugeValue, d.Uptime, labels},
{u.Device.TotalTxBytes, prometheus.CounterValue, d.TxBytes, labels},
{u.Device.TotalRxBytes, prometheus.CounterValue, d.RxBytes, labels},
{u.Device.TotalBytes, prometheus.CounterValue, d.Bytes, labels},
{u.Device.BytesD, prometheus.CounterValue, d.BytesD, labels}, // not sure if these 3 Ds are counters or gauges.
{u.Device.TxBytesD, prometheus.CounterValue, d.TxBytesD, labels}, // not sure if these 3 Ds are counters or gauges.
{u.Device.RxBytesD, prometheus.CounterValue, d.RxBytesD, labels}, // not sure if these 3 Ds are counters or gauges.
{u.Device.BytesR, prometheus.GaugeValue, d.BytesR, labels},
{u.Device.NumSta, prometheus.GaugeValue, d.NumSta, labels},
{u.Device.UserNumSta, prometheus.GaugeValue, d.UserNumSta, labels},
{u.Device.GuestNumSta, prometheus.GaugeValue, d.GuestNumSta, labels},
{u.Device.Loadavg1, prometheus.GaugeValue, d.SysStats.Loadavg1, labels},
{u.Device.Loadavg5, prometheus.GaugeValue, d.SysStats.Loadavg5, labels},
{u.Device.Loadavg15, prometheus.GaugeValue, d.SysStats.Loadavg15, labels},
{u.Device.MemUsed, prometheus.GaugeValue, d.SysStats.MemUsed, labels},
{u.Device.MemTotal, prometheus.GaugeValue, d.SysStats.MemTotal, labels},
{u.Device.MemBuffer, prometheus.GaugeValue, d.SysStats.MemBuffer, labels},
{u.Device.CPU, prometheus.GaugeValue, d.SystemStats.CPU, labels},
{u.Device.Mem, prometheus.GaugeValue, d.SystemStats.Mem, labels},
})
u.exportUAPstats(r, labels, d.Stat.Ap)
u.exportVAPtable(r, labels, d.VapTable)
u.exportRadtable(r, labels, d.RadioTable, d.RadioTableStats)
}
func (u *promUnifi) exportUAPstats(r report, labels []string, ap *unifi.Ap) {
// labelA := append([]string{"all"}, labels[2:]...)
labelU := append([]string{"user"}, labels[6:]...)
labelG := append([]string{"guest"}, labels[6:]...)
r.send([]*metric{
/* // all
{u.UAP.ApWifiTxDropped, prometheus.CounterValue, ap.WifiTxDropped, labelA},
{u.UAP.ApRxErrors, prometheus.CounterValue, ap.RxErrors, labelA},
{u.UAP.ApRxDropped, prometheus.CounterValue, ap.RxDropped, labelA},
{u.UAP.ApRxFrags, prometheus.CounterValue, ap.RxFrags, labelA},
{u.UAP.ApRxCrypts, prometheus.CounterValue, ap.RxCrypts, labelA},
{u.UAP.ApTxPackets, prometheus.CounterValue, ap.TxPackets, labelA},
{u.UAP.ApTxBytes, prometheus.CounterValue, ap.TxBytes, labelA},
{u.UAP.ApTxErrors, prometheus.CounterValue, ap.TxErrors, labelA},
{u.UAP.ApTxDropped, prometheus.CounterValue, ap.TxDropped, labelA},
{u.UAP.ApTxRetries, prometheus.CounterValue, ap.TxRetries, labelA},
{u.UAP.ApRxPackets, prometheus.CounterValue, ap.RxPackets, labelA},
{u.UAP.ApRxBytes, prometheus.CounterValue, ap.RxBytes, labelA},
{u.UAP.WifiTxAttempts, prometheus.CounterValue, ap.WifiTxAttempts, labelA},
{u.UAP.MacFilterRejections, prometheus.CounterValue, ap.MacFilterRejections, labelA},
*/
// user
{u.UAP.ApWifiTxDropped, prometheus.CounterValue, ap.UserWifiTxDropped, labelU},
{u.UAP.ApRxErrors, prometheus.CounterValue, ap.UserRxErrors, labelU},
{u.UAP.ApRxDropped, prometheus.CounterValue, ap.UserRxDropped, labelU},
{u.UAP.ApRxFrags, prometheus.CounterValue, ap.UserRxFrags, labelU},
{u.UAP.ApRxCrypts, prometheus.CounterValue, ap.UserRxCrypts, labelU},
{u.UAP.ApTxPackets, prometheus.CounterValue, ap.UserTxPackets, labelU},
{u.UAP.ApTxBytes, prometheus.CounterValue, ap.UserTxBytes, labelU},
{u.UAP.ApTxErrors, prometheus.CounterValue, ap.UserTxErrors, labelU},
{u.UAP.ApTxDropped, prometheus.CounterValue, ap.UserTxDropped, labelU},
{u.UAP.ApTxRetries, prometheus.CounterValue, ap.UserTxRetries, labelU},
{u.UAP.ApRxPackets, prometheus.CounterValue, ap.UserRxPackets, labelU},
{u.UAP.ApRxBytes, prometheus.CounterValue, ap.UserRxBytes, labelU},
{u.UAP.WifiTxAttempts, prometheus.CounterValue, ap.UserWifiTxAttempts, labelU},
{u.UAP.MacFilterRejections, prometheus.CounterValue, ap.UserMacFilterRejections, labelU},
// guest
{u.UAP.ApWifiTxDropped, prometheus.CounterValue, ap.GuestWifiTxDropped, labelG},
{u.UAP.ApRxErrors, prometheus.CounterValue, ap.GuestRxErrors, labelG},
{u.UAP.ApRxDropped, prometheus.CounterValue, ap.GuestRxDropped, labelG},
{u.UAP.ApRxFrags, prometheus.CounterValue, ap.GuestRxFrags, labelG},
{u.UAP.ApRxCrypts, prometheus.CounterValue, ap.GuestRxCrypts, labelG},
{u.UAP.ApTxPackets, prometheus.CounterValue, ap.GuestTxPackets, labelG},
{u.UAP.ApTxBytes, prometheus.CounterValue, ap.GuestTxBytes, labelG},
{u.UAP.ApTxErrors, prometheus.CounterValue, ap.GuestTxErrors, labelG},
{u.UAP.ApTxDropped, prometheus.CounterValue, ap.GuestTxDropped, labelG},
{u.UAP.ApTxRetries, prometheus.CounterValue, ap.GuestTxRetries, labelG},
{u.UAP.ApRxPackets, prometheus.CounterValue, ap.GuestRxPackets, labelG},
{u.UAP.ApRxBytes, prometheus.CounterValue, ap.GuestRxBytes, labelG},
{u.UAP.WifiTxAttempts, prometheus.CounterValue, ap.GuestWifiTxAttempts, labelG},
{u.UAP.MacFilterRejections, prometheus.CounterValue, ap.GuestMacFilterRejections, labelG},
})
}
func (u *promUnifi) exportVAPtable(r report, labels []string, vt unifi.VapTable) {
// vap table stats
for _, v := range vt {
if !v.Up.Val {
continue
}
labelV := append([]string{v.Name, v.Bssid, v.Radio, v.RadioName, v.Essid, v.Usage}, labels[6:]...)
r.send([]*metric{
{u.UAP.VAPCcq, prometheus.GaugeValue, v.Ccq / 10, labelV},
{u.UAP.VAPMacFilterRejections, prometheus.CounterValue, v.MacFilterRejections, labelV},
{u.UAP.VAPNumSatisfactionSta, prometheus.GaugeValue, v.NumSatisfactionSta, labelV},
{u.UAP.VAPAvgClientSignal, prometheus.GaugeValue, v.AvgClientSignal, labelV},
{u.UAP.VAPSatisfaction, prometheus.GaugeValue, v.Satisfaction, labelV},
{u.UAP.VAPSatisfactionNow, prometheus.GaugeValue, v.SatisfactionNow, labelV},
{u.UAP.VAPDNSAvgLatency, prometheus.GaugeValue, v.DNSAvgLatency.Val / 1000, labelV},
{u.UAP.VAPRxBytes, prometheus.CounterValue, v.RxBytes, labelV},
{u.UAP.VAPRxCrypts, prometheus.CounterValue, v.RxCrypts, labelV},
{u.UAP.VAPRxDropped, prometheus.CounterValue, v.RxDropped, labelV},
{u.UAP.VAPRxErrors, prometheus.CounterValue, v.RxErrors, labelV},
{u.UAP.VAPRxFrags, prometheus.CounterValue, v.RxFrags, labelV},
{u.UAP.VAPRxNwids, prometheus.CounterValue, v.RxNwids, labelV},
{u.UAP.VAPRxPackets, prometheus.CounterValue, v.RxPackets, labelV},
{u.UAP.VAPTxBytes, prometheus.CounterValue, v.TxBytes, labelV},
{u.UAP.VAPTxDropped, prometheus.CounterValue, v.TxDropped, labelV},
{u.UAP.VAPTxErrors, prometheus.CounterValue, v.TxErrors, labelV},
{u.UAP.VAPTxPackets, prometheus.CounterValue, v.TxPackets, labelV},
{u.UAP.VAPTxPower, prometheus.GaugeValue, v.TxPower, labelV},
{u.UAP.VAPTxRetries, prometheus.CounterValue, v.TxRetries, labelV},
{u.UAP.VAPTxCombinedRetries, prometheus.CounterValue, v.TxCombinedRetries, labelV},
{u.UAP.VAPTxDataMpduBytes, prometheus.CounterValue, v.TxDataMpduBytes, labelV},
{u.UAP.VAPTxRtsRetries, prometheus.CounterValue, v.TxRtsRetries, labelV},
{u.UAP.VAPTxTotal, prometheus.CounterValue, v.TxTotal, labelV},
{u.UAP.VAPTxGoodbytes, prometheus.CounterValue, v.TxTCPStats.Goodbytes, labelV},
{u.UAP.VAPTxLatAvg, prometheus.GaugeValue, v.TxTCPStats.LatAvg.Val / 1000, labelV},
{u.UAP.VAPTxLatMax, prometheus.GaugeValue, v.TxTCPStats.LatMax.Val / 1000, labelV},
{u.UAP.VAPTxLatMin, prometheus.GaugeValue, v.TxTCPStats.LatMin.Val / 1000, labelV},
{u.UAP.VAPRxGoodbytes, prometheus.CounterValue, v.RxTCPStats.Goodbytes, labelV},
{u.UAP.VAPRxLatAvg, prometheus.GaugeValue, v.RxTCPStats.LatAvg.Val / 1000, labelV},
{u.UAP.VAPRxLatMax, prometheus.GaugeValue, v.RxTCPStats.LatMax.Val / 1000, labelV},
{u.UAP.VAPRxLatMin, prometheus.GaugeValue, v.RxTCPStats.LatMin.Val / 1000, labelV},
{u.UAP.VAPWifiTxLatencyMovAvg, prometheus.GaugeValue, v.WifiTxLatencyMov.Avg.Val / 1000, labelV},
{u.UAP.VAPWifiTxLatencyMovMax, prometheus.GaugeValue, v.WifiTxLatencyMov.Max.Val / 1000, labelV},
{u.UAP.VAPWifiTxLatencyMovMin, prometheus.GaugeValue, v.WifiTxLatencyMov.Min.Val / 1000, labelV},
{u.UAP.VAPWifiTxLatencyMovTotal, prometheus.CounterValue, v.WifiTxLatencyMov.Total, labelV}, // not sure if gauge or counter.
{u.UAP.VAPWifiTxLatencyMovCount, prometheus.CounterValue, v.WifiTxLatencyMov.TotalCount, labelV}, // not sure if gauge or counter.
})
}
}
func (u *promUnifi) exportRadtable(r report, labels []string, rt unifi.RadioTable, rts unifi.RadioTableStats) {
// radio table
for _, p := range rt {
labelR := append([]string{p.Name, p.Radio}, labels[6:]...)
r.send([]*metric{
{u.UAP.RadioCurrentAntennaGain, prometheus.GaugeValue, p.CurrentAntennaGain, labelR},
{u.UAP.RadioHt, prometheus.GaugeValue, p.Ht, labelR},
{u.UAP.RadioMaxTxpower, prometheus.GaugeValue, p.MaxTxpower, labelR},
{u.UAP.RadioMinTxpower, prometheus.GaugeValue, p.MinTxpower, labelR},
{u.UAP.RadioNss, prometheus.GaugeValue, p.Nss, labelR},
{u.UAP.RadioRadioCaps, prometheus.GaugeValue, p.RadioCaps, labelR},
})
// combine radio table with radio stats table.
for _, t := range rts {
if t.Name != p.Name {
continue
}
r.send([]*metric{
{u.UAP.RadioTxPower, prometheus.GaugeValue, t.TxPower, labelR},
{u.UAP.RadioAstBeXmit, prometheus.GaugeValue, t.AstBeXmit, labelR},
{u.UAP.RadioChannel, prometheus.GaugeValue, t.Channel, labelR},
{u.UAP.RadioCuSelfRx, prometheus.GaugeValue, t.CuSelfRx, labelR},
{u.UAP.RadioCuSelfTx, prometheus.GaugeValue, t.CuSelfTx, labelR},
{u.UAP.RadioCuTotal, prometheus.GaugeValue, t.CuTotal, labelR},
{u.UAP.RadioExtchannel, prometheus.GaugeValue, t.Extchannel, labelR},
{u.UAP.RadioGain, prometheus.GaugeValue, t.Gain, labelR},
{u.UAP.RadioGuestNumSta, prometheus.GaugeValue, t.GuestNumSta, labelR},
{u.UAP.RadioNumSta, prometheus.GaugeValue, t.NumSta, labelR},
{u.UAP.RadioUserNumSta, prometheus.GaugeValue, t.UserNumSta, labelR},
{u.UAP.RadioTxPackets, prometheus.CounterValue, t.TxPackets, labelR},
{u.UAP.RadioTxRetries, prometheus.CounterValue, t.TxRetries, labelR},
})
}
}
}

107
promunifi/udm.go Normal file
View File

@ -0,0 +1,107 @@
package promunifi
import (
"github.com/prometheus/client_golang/prometheus"
"golift.io/unifi"
)
// These are shared by all four device types: UDM, UAP, USG, USW
type unifiDevice struct {
Uptime *prometheus.Desc
Temperature *prometheus.Desc // sw only
TotalMaxPower *prometheus.Desc // sw only
FanLevel *prometheus.Desc // sw only
TotalTxBytes *prometheus.Desc
TotalRxBytes *prometheus.Desc
TotalBytes *prometheus.Desc
BytesR *prometheus.Desc // ap only
BytesD *prometheus.Desc // ap only
Bytes *prometheus.Desc // ap only
TxBytesD *prometheus.Desc // ap only
RxBytesD *prometheus.Desc // ap only
NumSta *prometheus.Desc
UserNumSta *prometheus.Desc
GuestNumSta *prometheus.Desc
NumDesktop *prometheus.Desc // gw only
NumMobile *prometheus.Desc // gw only
NumHandheld *prometheus.Desc // gw only
Loadavg1 *prometheus.Desc
Loadavg5 *prometheus.Desc
Loadavg15 *prometheus.Desc
MemBuffer *prometheus.Desc
MemTotal *prometheus.Desc
MemUsed *prometheus.Desc
CPU *prometheus.Desc
Mem *prometheus.Desc
}
func descDevice(ns string) *unifiDevice {
labels := []string{"ip", "version", "model", "serial", "type", "mac", "site_name", "name"}
return &unifiDevice{
Uptime: prometheus.NewDesc(ns+"uptime", "Uptime", labels, nil),
Temperature: prometheus.NewDesc(ns+"temperature_celsius", "Temperature", labels, nil),
TotalMaxPower: prometheus.NewDesc(ns+"max_power_total", "Total Max Power", labels, nil),
FanLevel: prometheus.NewDesc(ns+"fan_level", "Fan Level", labels, nil),
TotalTxBytes: prometheus.NewDesc(ns+"transmit_bytes_total", "Total Transmitted Bytes", labels, nil),
TotalRxBytes: prometheus.NewDesc(ns+"receive_bytes_total", "Total Received Bytes", labels, nil),
TotalBytes: prometheus.NewDesc(ns+"bytes_total", "Total Bytes Transferred", labels, nil),
BytesR: prometheus.NewDesc(ns+"rate_bytes", "Transfer Rate", labels, nil),
BytesD: prometheus.NewDesc(ns+"d_bytes", "Total Bytes D???", labels, nil),
Bytes: prometheus.NewDesc(ns+"transferred_bytes_total", "Bytes Transferred", labels, nil),
TxBytesD: prometheus.NewDesc(ns+"d_tranmsit_bytes", "Transmit Bytes D???", labels, nil),
RxBytesD: prometheus.NewDesc(ns+"d_receive_bytes", "Receive Bytes D???", labels, nil),
NumSta: prometheus.NewDesc(ns+"stations", "Number of Stations", labels, nil),
UserNumSta: prometheus.NewDesc(ns+"user_stations", "Number of User Stations", labels, nil),
GuestNumSta: prometheus.NewDesc(ns+"guest_stations", "Number of Guest Stations", labels, nil),
NumDesktop: prometheus.NewDesc(ns+"desktops", "Number of Desktops", labels, nil),
NumMobile: prometheus.NewDesc(ns+"mobile", "Number of Mobiles", labels, nil),
NumHandheld: prometheus.NewDesc(ns+"handheld", "Number of Handhelds", labels, nil),
Loadavg1: prometheus.NewDesc(ns+"load_average_1", "System Load Average 1 Minute", labels, nil),
Loadavg5: prometheus.NewDesc(ns+"load_average_5", "System Load Average 5 Minutes", labels, nil),
Loadavg15: prometheus.NewDesc(ns+"load_average_15", "System Load Average 15 Minutes", labels, nil),
MemUsed: prometheus.NewDesc(ns+"memory_used_bytes", "System Memory Used", labels, nil),
MemTotal: prometheus.NewDesc(ns+"memory_installed_bytes", "System Installed Memory", labels, nil),
MemBuffer: prometheus.NewDesc(ns+"memory_buffer_bytes", "System Memory Buffer", labels, nil),
CPU: prometheus.NewDesc(ns+"cpu_utilization_percent", "System CPU % Utilized", labels, nil),
Mem: prometheus.NewDesc(ns+"memory_utilization_percent", "System Memory % Utilized", labels, nil),
}
}
// UDM is a collection of stats from USG, USW and UAP. It has no unique stats.
func (u *promUnifi) exportUDM(r report, d *unifi.UDM) {
labels := []string{d.IP, d.Version, d.Model, d.Serial, d.Type, d.Mac, d.SiteName, d.Name}
// Dream Machine System Data.
r.send([]*metric{
{u.Device.Uptime, prometheus.GaugeValue, d.Uptime, labels},
{u.Device.TotalTxBytes, prometheus.CounterValue, d.TxBytes, labels},
{u.Device.TotalRxBytes, prometheus.CounterValue, d.RxBytes, labels},
{u.Device.TotalBytes, prometheus.CounterValue, d.Bytes, labels},
{u.Device.NumSta, prometheus.GaugeValue, d.NumSta, labels},
{u.Device.UserNumSta, prometheus.GaugeValue, d.UserNumSta, labels},
{u.Device.GuestNumSta, prometheus.GaugeValue, d.GuestNumSta, labels},
{u.Device.NumDesktop, prometheus.GaugeValue, d.NumDesktop, labels},
{u.Device.NumMobile, prometheus.GaugeValue, d.NumMobile, labels},
{u.Device.NumHandheld, prometheus.GaugeValue, d.NumHandheld, labels},
{u.Device.Loadavg1, prometheus.GaugeValue, d.SysStats.Loadavg1, labels},
{u.Device.Loadavg5, prometheus.GaugeValue, d.SysStats.Loadavg5, labels},
{u.Device.Loadavg15, prometheus.GaugeValue, d.SysStats.Loadavg15, labels},
{u.Device.MemUsed, prometheus.GaugeValue, d.SysStats.MemUsed, labels},
{u.Device.MemTotal, prometheus.GaugeValue, d.SysStats.MemTotal, labels},
{u.Device.MemBuffer, prometheus.GaugeValue, d.SysStats.MemBuffer, labels},
{u.Device.CPU, prometheus.GaugeValue, d.SystemStats.CPU, labels},
{u.Device.Mem, prometheus.GaugeValue, d.SystemStats.Mem, labels},
})
// Switch Data
u.exportUSWstats(r, labels, d.Stat.Sw)
u.exportPortTable(r, labels, d.PortTable)
// Gateway Data
u.exportUSGstats(r, labels, d.Stat.Gw, d.SpeedtestStatus)
u.exportWANPorts(r, labels, d.Wan1, d.Wan2)
// Wireless Data - UDM (non-pro) only
if d.Stat.Ap != nil && d.VapTable != nil {
u.exportUAPstats(r, labels, d.Stat.Ap)
u.exportVAPtable(r, labels, *d.VapTable)
u.exportRadtable(r, labels, *d.RadioTable, *d.RadioTableStats)
}
}

145
promunifi/usg.go Normal file
View File

@ -0,0 +1,145 @@
package promunifi
import (
"github.com/prometheus/client_golang/prometheus"
"golift.io/unifi"
)
type usg struct {
WanRxPackets *prometheus.Desc
WanRxBytes *prometheus.Desc
WanRxDropped *prometheus.Desc
WanRxErrors *prometheus.Desc
WanTxPackets *prometheus.Desc
WanTxBytes *prometheus.Desc
LanRxPackets *prometheus.Desc
LanRxBytes *prometheus.Desc
LanRxDropped *prometheus.Desc
LanTxPackets *prometheus.Desc
LanTxBytes *prometheus.Desc
WanRxBroadcast *prometheus.Desc
WanRxBytesR *prometheus.Desc
WanRxMulticast *prometheus.Desc
WanSpeed *prometheus.Desc
WanTxBroadcast *prometheus.Desc
WanTxBytesR *prometheus.Desc
WanTxDropped *prometheus.Desc
WanTxErrors *prometheus.Desc
WanTxMulticast *prometheus.Desc
WanBytesR *prometheus.Desc
Latency *prometheus.Desc
Runtime *prometheus.Desc
XputDownload *prometheus.Desc
XputUpload *prometheus.Desc
}
func descUSG(ns string) *usg {
// labels := []string{"ip", "version", "model", "serial", "type", "mac", "site_name", "name"}
// labelWan := append([]string{"port"}, labels[6:]...)
labels := []string{"port", "site_name", "name"}
return &usg{
WanRxPackets: prometheus.NewDesc(ns+"wan_receive_packets_total", "WAN Receive Packets Total", labels, nil),
WanRxBytes: prometheus.NewDesc(ns+"wan_receive_bytes_total", "WAN Receive Bytes Total", labels, nil),
WanRxDropped: prometheus.NewDesc(ns+"wan_receive_dropped_total", "WAN Receive Dropped Total", labels, nil),
WanRxErrors: prometheus.NewDesc(ns+"wan_receive_errors_total", "WAN Receive Errors Total", labels, nil),
WanTxPackets: prometheus.NewDesc(ns+"wan_transmit_packets_total", "WAN Transmit Packets Total", labels, nil),
WanTxBytes: prometheus.NewDesc(ns+"wan_transmit_bytes_total", "WAN Transmit Bytes Total", labels, nil),
WanRxBroadcast: prometheus.NewDesc(ns+"wan_receive_broadcast_total", "WAN Receive Broadcast Total", labels, nil),
WanRxBytesR: prometheus.NewDesc(ns+"wan_receive_rate_bytes", "WAN Receive Bytes Rate", labels, nil),
WanRxMulticast: prometheus.NewDesc(ns+"wan_receive_multicast_total", "WAN Receive Multicast Total", labels, nil),
WanSpeed: prometheus.NewDesc(ns+"wan_speed_bps", "WAN Speed", labels, nil),
WanTxBroadcast: prometheus.NewDesc(ns+"wan_transmit_broadcast_total", "WAN Transmit Broadcast Total", labels, nil),
WanTxBytesR: prometheus.NewDesc(ns+"wan_transmit_rate_bytes", "WAN Transmit Bytes Rate", labels, nil),
WanTxDropped: prometheus.NewDesc(ns+"wan_transmit_dropped_total", "WAN Transmit Dropped Total", labels, nil),
WanTxErrors: prometheus.NewDesc(ns+"wan_transmit_errors_total", "WAN Transmit Errors Total", labels, nil),
WanTxMulticast: prometheus.NewDesc(ns+"wan_transmit_multicast_total", "WAN Transmit Multicast Total", labels, nil),
WanBytesR: prometheus.NewDesc(ns+"wan_rate_bytes", "WAN Transfer Rate", labels, nil),
LanRxPackets: prometheus.NewDesc(ns+"lan_receive_packets_total", "LAN Receive Packets Total", labels, nil),
LanRxBytes: prometheus.NewDesc(ns+"lan_receive_bytes_total", "LAN Receive Bytes Total", labels, nil),
LanRxDropped: prometheus.NewDesc(ns+"lan_receive_dropped_total", "LAN Receive Dropped Total", labels, nil),
LanTxPackets: prometheus.NewDesc(ns+"lan_transmit_packets_total", "LAN Transmit Packets Total", labels, nil),
LanTxBytes: prometheus.NewDesc(ns+"lan_transmit_bytes_total", "LAN Transmit Bytes Total", labels, nil),
Latency: prometheus.NewDesc(ns+"speedtest_latency_seconds", "Speedtest Latency", labels, nil),
Runtime: prometheus.NewDesc(ns+"speedtest_runtime", "Speedtest Run Time", labels, nil),
XputDownload: prometheus.NewDesc(ns+"speedtest_download", "Speedtest Download Rate", labels, nil),
XputUpload: prometheus.NewDesc(ns+"speedtest_upload", "Speedtest Upload Rate", labels, nil),
}
}
func (u *promUnifi) exportUSG(r report, d *unifi.USG) {
labels := []string{d.IP, d.Version, d.Model, d.Serial, d.Type, d.Mac, d.SiteName, d.Name}
// Gateway System Data.
r.send([]*metric{
{u.Device.Uptime, prometheus.GaugeValue, d.Uptime, labels},
{u.Device.TotalTxBytes, prometheus.CounterValue, d.TxBytes, labels},
{u.Device.TotalRxBytes, prometheus.CounterValue, d.RxBytes, labels},
{u.Device.TotalBytes, prometheus.CounterValue, d.Bytes, labels},
{u.Device.NumSta, prometheus.GaugeValue, d.NumSta, labels},
{u.Device.UserNumSta, prometheus.GaugeValue, d.UserNumSta, labels},
{u.Device.GuestNumSta, prometheus.GaugeValue, d.GuestNumSta, labels},
{u.Device.NumDesktop, prometheus.GaugeValue, d.NumDesktop, labels},
{u.Device.NumMobile, prometheus.GaugeValue, d.NumMobile, labels},
{u.Device.NumHandheld, prometheus.GaugeValue, d.NumHandheld, labels},
{u.Device.Loadavg1, prometheus.GaugeValue, d.SysStats.Loadavg1, labels},
{u.Device.Loadavg5, prometheus.GaugeValue, d.SysStats.Loadavg5, labels},
{u.Device.Loadavg15, prometheus.GaugeValue, d.SysStats.Loadavg15, labels},
{u.Device.MemUsed, prometheus.GaugeValue, d.SysStats.MemUsed, labels},
{u.Device.MemTotal, prometheus.GaugeValue, d.SysStats.MemTotal, labels},
{u.Device.MemBuffer, prometheus.GaugeValue, d.SysStats.MemBuffer, labels},
{u.Device.CPU, prometheus.GaugeValue, d.SystemStats.CPU, labels},
{u.Device.Mem, prometheus.GaugeValue, d.SystemStats.Mem, labels},
})
u.exportWANPorts(r, labels, d.Wan1, d.Wan2)
u.exportUSGstats(r, labels, d.Stat.Gw, d.SpeedtestStatus)
}
func (u *promUnifi) exportUSGstats(r report, labels []string, gw *unifi.Gw, st unifi.SpeedtestStatus) {
labelLan := []string{"lan", labels[6], labels[7]}
labelWan := []string{"all", labels[6], labels[7]}
r.send([]*metric{
/* // Combined Port Stats - not really needed. sum() the others instead.
{u.USG.WanRxPackets, prometheus.CounterValue, gw.WanRxPackets, labelWan},
{u.USG.WanRxBytes, prometheus.CounterValue, gw.WanRxBytes, labelWan},
{u.USG.WanRxDropped, prometheus.CounterValue, gw.WanRxDropped, labelWan},
{u.USG.WanTxPackets, prometheus.CounterValue, gw.WanTxPackets, labelWan},
{u.USG.WanTxBytes, prometheus.CounterValue, gw.WanTxBytes, labelWan},
{u.USG.WanRxErrors, prometheus.CounterValue, gw.WanRxErrors, labelWan},
*/
{u.USG.LanRxPackets, prometheus.CounterValue, gw.LanRxPackets, labelLan},
{u.USG.LanRxBytes, prometheus.CounterValue, gw.LanRxBytes, labelLan},
{u.USG.LanTxPackets, prometheus.CounterValue, gw.LanTxPackets, labelLan},
{u.USG.LanTxBytes, prometheus.CounterValue, gw.LanTxBytes, labelLan},
{u.USG.LanRxDropped, prometheus.CounterValue, gw.LanRxDropped, labelLan},
// Speed Test Stats
{u.USG.Latency, prometheus.GaugeValue, st.Latency.Val / 1000, labelWan},
{u.USG.Runtime, prometheus.GaugeValue, st.Runtime, labelWan},
{u.USG.XputDownload, prometheus.GaugeValue, st.XputDownload, labelWan},
{u.USG.XputUpload, prometheus.GaugeValue, st.XputUpload, labelWan},
})
}
func (u *promUnifi) exportWANPorts(r report, labels []string, wans ...unifi.Wan) {
for _, wan := range wans {
if !wan.Up.Val {
continue // only record UP interfaces.
}
labelWan := []string{wan.Name, labels[6], labels[7]}
r.send([]*metric{
{u.USG.WanRxPackets, prometheus.CounterValue, wan.RxPackets, labelWan},
{u.USG.WanRxBytes, prometheus.CounterValue, wan.RxBytes, labelWan},
{u.USG.WanRxDropped, prometheus.CounterValue, wan.RxDropped, labelWan},
{u.USG.WanRxErrors, prometheus.CounterValue, wan.RxErrors, labelWan},
{u.USG.WanTxPackets, prometheus.CounterValue, wan.TxPackets, labelWan},
{u.USG.WanTxBytes, prometheus.CounterValue, wan.TxBytes, labelWan},
{u.USG.WanRxBroadcast, prometheus.CounterValue, wan.RxBroadcast, labelWan},
{u.USG.WanRxMulticast, prometheus.CounterValue, wan.RxMulticast, labelWan},
{u.USG.WanSpeed, prometheus.CounterValue, wan.Speed.Val * 1000000, labelWan},
{u.USG.WanTxBroadcast, prometheus.CounterValue, wan.TxBroadcast, labelWan},
{u.USG.WanTxBytesR, prometheus.CounterValue, wan.TxBytesR, labelWan},
{u.USG.WanTxDropped, prometheus.CounterValue, wan.TxDropped, labelWan},
{u.USG.WanTxErrors, prometheus.CounterValue, wan.TxErrors, labelWan},
{u.USG.WanTxMulticast, prometheus.CounterValue, wan.TxMulticast, labelWan},
{u.USG.WanBytesR, prometheus.GaugeValue, wan.BytesR, labelWan},
})
}
}

181
promunifi/usw.go Normal file
View File

@ -0,0 +1,181 @@
package promunifi
import (
"github.com/prometheus/client_golang/prometheus"
"golift.io/unifi"
)
type usw struct {
// Switch "total" traffic stats
SwRxPackets *prometheus.Desc
SwRxBytes *prometheus.Desc
SwRxErrors *prometheus.Desc
SwRxDropped *prometheus.Desc
SwRxCrypts *prometheus.Desc
SwRxFrags *prometheus.Desc
SwTxPackets *prometheus.Desc
SwTxBytes *prometheus.Desc
SwTxErrors *prometheus.Desc
SwTxDropped *prometheus.Desc
SwTxRetries *prometheus.Desc
SwRxMulticast *prometheus.Desc
SwRxBroadcast *prometheus.Desc
SwTxMulticast *prometheus.Desc
SwTxBroadcast *prometheus.Desc
SwBytes *prometheus.Desc
// Port data.
PoeCurrent *prometheus.Desc
PoePower *prometheus.Desc
PoeVoltage *prometheus.Desc
RxBroadcast *prometheus.Desc
RxBytes *prometheus.Desc
RxBytesR *prometheus.Desc
RxDropped *prometheus.Desc
RxErrors *prometheus.Desc
RxMulticast *prometheus.Desc
RxPackets *prometheus.Desc
Satisfaction *prometheus.Desc
Speed *prometheus.Desc
TxBroadcast *prometheus.Desc
TxBytes *prometheus.Desc
TxBytesR *prometheus.Desc
TxDropped *prometheus.Desc
TxErrors *prometheus.Desc
TxMulticast *prometheus.Desc
TxPackets *prometheus.Desc
}
func descUSW(ns string) *usw {
pns := ns + "port_"
// labels := []string{"ip", "version", "model", "serial", "type", "mac", "site_name", "name"}
labelS := []string{"site_name", "name"} // labels[6:]
labelP := []string{"port_num", "port_name", "port_mac", "port_ip", "site_name", "name"}
return &usw{
SwRxPackets: prometheus.NewDesc(ns+"switch_receive_packets_total", "Switch Packets Received Total", labelS, nil),
SwRxBytes: prometheus.NewDesc(ns+"switch_receive_bytes_total", "Switch Bytes Received Total", labelS, nil),
SwRxErrors: prometheus.NewDesc(ns+"switch_receive_errors_total", "Switch Errors Received Total", labelS, nil),
SwRxDropped: prometheus.NewDesc(ns+"switch_receive_dropped_total", "Switch Dropped Received Total", labelS, nil),
SwRxCrypts: prometheus.NewDesc(ns+"switch_receive_crypts_total", "Switch Crypts Received Total", labelS, nil),
SwRxFrags: prometheus.NewDesc(ns+"switch_receive_frags_total", "Switch Frags Received Total", labelS, nil),
SwTxPackets: prometheus.NewDesc(ns+"switch_transmit_packets_total", "Switch Packets Transmit Total", labelS, nil),
SwTxBytes: prometheus.NewDesc(ns+"switch_transmit_bytes_total", "Switch Bytes Transmit Total", labelS, nil),
SwTxErrors: prometheus.NewDesc(ns+"switch_transmit_errors_total", "Switch Errors Transmit Total", labelS, nil),
SwTxDropped: prometheus.NewDesc(ns+"switch_transmit_dropped_total", "Switch Dropped Transmit Total", labelS, nil),
SwTxRetries: prometheus.NewDesc(ns+"switch_transmit_retries_total", "Switch Retries Transmit Total", labelS, nil),
SwRxMulticast: prometheus.NewDesc(ns+"switch_receive_multicast_total", "Switch Multicast Receive Total", labelS, nil),
SwRxBroadcast: prometheus.NewDesc(ns+"switch_receive_broadcast_total", "Switch Broadcast Receive Total", labelS, nil),
SwTxMulticast: prometheus.NewDesc(ns+"switch_transmit_multicast_total", "Switch Multicast Transmit Total", labelS, nil),
SwTxBroadcast: prometheus.NewDesc(ns+"switch_transmit_broadcast_total", "Switch Broadcast Transmit Total", labelS, nil),
SwBytes: prometheus.NewDesc(ns+"switch_bytes_total", "Switch Bytes Transferred Total", labelS, nil),
// per-port data
PoeCurrent: prometheus.NewDesc(pns+"poe_amperes", "POE Current", labelP, nil),
PoePower: prometheus.NewDesc(pns+"poe_watts", "POE Power", labelP, nil),
PoeVoltage: prometheus.NewDesc(pns+"poe_volts", "POE Voltage", labelP, nil),
RxBroadcast: prometheus.NewDesc(pns+"receive_broadcast_total", "Receive Broadcast", labelP, nil),
RxBytes: prometheus.NewDesc(pns+"receive_bytes_total", "Total Receive Bytes", labelP, nil),
RxBytesR: prometheus.NewDesc(pns+"receive_rate_bytes", "Receive Bytes Rate", labelP, nil),
RxDropped: prometheus.NewDesc(pns+"receive_dropped_total", "Total Receive Dropped", labelP, nil),
RxErrors: prometheus.NewDesc(pns+"receive_errors_total", "Total Receive Errors", labelP, nil),
RxMulticast: prometheus.NewDesc(pns+"receive_multicast_total", "Total Receive Multicast", labelP, nil),
RxPackets: prometheus.NewDesc(pns+"receive_packets_total", "Total Receive Packets", labelP, nil),
Satisfaction: prometheus.NewDesc(pns+"satisfaction_percent", "Satisfaction", labelP, nil),
Speed: prometheus.NewDesc(pns+"port_speed_bps", "Speed", labelP, nil),
TxBroadcast: prometheus.NewDesc(pns+"transmit_broadcast_total", "Total Transmit Broadcast", labelP, nil),
TxBytes: prometheus.NewDesc(pns+"transmit_bytes_total", "Total Transmit Bytes", labelP, nil),
TxBytesR: prometheus.NewDesc(pns+"transmit_rate_bytes", "Transmit Bytes Rate", labelP, nil),
TxDropped: prometheus.NewDesc(pns+"transmit_dropped_total", "Total Transmit Dropped", labelP, nil),
TxErrors: prometheus.NewDesc(pns+"transmit_errors_total", "Total Transmit Errors", labelP, nil),
TxMulticast: prometheus.NewDesc(pns+"transmit_multicast_total", "Total Tranmist Multicast", labelP, nil),
TxPackets: prometheus.NewDesc(pns+"transmit_packets_total", "Total Transmit Packets", labelP, nil),
}
}
func (u *promUnifi) exportUSW(r report, d *unifi.USW) {
labels := []string{d.IP, d.Version, d.Model, d.Serial, d.Type, d.Mac, d.SiteName, d.Name}
if d.HasTemperature.Val {
r.send([]*metric{{u.Device.Temperature, prometheus.GaugeValue, d.GeneralTemperature, labels}})
}
if d.HasFan.Val {
r.send([]*metric{{u.Device.FanLevel, prometheus.GaugeValue, d.FanLevel, labels}})
}
// Switch System Data.
r.send([]*metric{
{u.Device.Uptime, prometheus.GaugeValue, d.Uptime, labels},
{u.Device.TotalMaxPower, prometheus.GaugeValue, d.TotalMaxPower, labels},
{u.Device.TotalTxBytes, prometheus.CounterValue, d.TxBytes, labels},
{u.Device.TotalRxBytes, prometheus.CounterValue, d.RxBytes, labels},
{u.Device.TotalBytes, prometheus.CounterValue, d.Bytes, labels},
{u.Device.NumSta, prometheus.GaugeValue, d.NumSta, labels},
{u.Device.UserNumSta, prometheus.GaugeValue, d.UserNumSta, labels},
{u.Device.GuestNumSta, prometheus.GaugeValue, d.GuestNumSta, labels},
{u.Device.Loadavg1, prometheus.GaugeValue, d.SysStats.Loadavg1, labels},
{u.Device.Loadavg5, prometheus.GaugeValue, d.SysStats.Loadavg5, labels},
{u.Device.Loadavg15, prometheus.GaugeValue, d.SysStats.Loadavg15, labels},
{u.Device.MemUsed, prometheus.GaugeValue, d.SysStats.MemUsed, labels},
{u.Device.MemTotal, prometheus.GaugeValue, d.SysStats.MemTotal, labels},
{u.Device.MemBuffer, prometheus.GaugeValue, d.SysStats.MemBuffer, labels},
{u.Device.CPU, prometheus.GaugeValue, d.SystemStats.CPU, labels},
{u.Device.Mem, prometheus.GaugeValue, d.SystemStats.Mem, labels},
})
u.exportPortTable(r, labels, d.PortTable)
u.exportUSWstats(r, labels, d.Stat.Sw)
}
func (u *promUnifi) exportUSWstats(r report, labels []string, sw *unifi.Sw) {
labelS := labels[6:]
r.send([]*metric{
{u.USW.SwRxPackets, prometheus.CounterValue, sw.RxPackets, labelS},
{u.USW.SwRxBytes, prometheus.CounterValue, sw.RxBytes, labelS},
{u.USW.SwRxErrors, prometheus.CounterValue, sw.RxErrors, labelS},
{u.USW.SwRxDropped, prometheus.CounterValue, sw.RxDropped, labelS},
{u.USW.SwRxCrypts, prometheus.CounterValue, sw.RxCrypts, labelS},
{u.USW.SwRxFrags, prometheus.CounterValue, sw.RxFrags, labelS},
{u.USW.SwTxPackets, prometheus.CounterValue, sw.TxPackets, labelS},
{u.USW.SwTxBytes, prometheus.CounterValue, sw.TxBytes, labelS},
{u.USW.SwTxErrors, prometheus.CounterValue, sw.TxErrors, labelS},
{u.USW.SwTxDropped, prometheus.CounterValue, sw.TxDropped, labelS},
{u.USW.SwTxRetries, prometheus.CounterValue, sw.TxRetries, labelS},
{u.USW.SwRxMulticast, prometheus.CounterValue, sw.RxMulticast, labelS},
{u.USW.SwRxBroadcast, prometheus.CounterValue, sw.RxBroadcast, labelS},
{u.USW.SwTxMulticast, prometheus.CounterValue, sw.TxMulticast, labelS},
{u.USW.SwTxBroadcast, prometheus.CounterValue, sw.TxBroadcast, labelS},
{u.USW.SwBytes, prometheus.CounterValue, sw.Bytes, labelS},
})
}
func (u *promUnifi) exportPortTable(r report, labels []string, pt []unifi.Port) {
// Per-port data on a switch
for _, p := range pt {
if !p.Up.Val {
continue
}
// Copy labels, and add four new ones.
labelP := []string{p.PortIdx.Txt, p.Name, p.Mac, p.IP, labels[6], labels[7]}
if p.PoeEnable.Val && p.PortPoe.Val {
r.send([]*metric{
{u.USW.PoeCurrent, prometheus.GaugeValue, p.PoeCurrent, labelP},
{u.USW.PoePower, prometheus.GaugeValue, p.PoePower, labelP},
{u.USW.PoeVoltage, prometheus.GaugeValue, p.PoeVoltage, labelP},
})
}
r.send([]*metric{
{u.USW.RxBroadcast, prometheus.CounterValue, p.RxBroadcast, labelP},
{u.USW.RxBytes, prometheus.CounterValue, p.RxBytes, labelP},
{u.USW.RxBytesR, prometheus.GaugeValue, p.RxBytesR, labelP},
{u.USW.RxDropped, prometheus.CounterValue, p.RxDropped, labelP},
{u.USW.RxErrors, prometheus.CounterValue, p.RxErrors, labelP},
{u.USW.RxMulticast, prometheus.CounterValue, p.RxMulticast, labelP},
{u.USW.RxPackets, prometheus.CounterValue, p.RxPackets, labelP},
{u.USW.Satisfaction, prometheus.GaugeValue, p.Satisfaction, labelP},
{u.USW.Speed, prometheus.GaugeValue, p.Speed.Val * 1000000, labelP},
{u.USW.TxBroadcast, prometheus.CounterValue, p.TxBroadcast, labelP},
{u.USW.TxBytes, prometheus.CounterValue, p.TxBytes, labelP},
{u.USW.TxBytesR, prometheus.GaugeValue, p.TxBytesR, labelP},
{u.USW.TxDropped, prometheus.CounterValue, p.TxDropped, labelP},
{u.USW.TxErrors, prometheus.CounterValue, p.TxErrors, labelP},
{u.USW.TxMulticast, prometheus.CounterValue, p.TxMulticast, labelP},
})
}
}

View File

@ -1,127 +0,0 @@
package unifipoller
import (
"crypto/tls"
"fmt"
"log"
"os"
"strings"
"time"
influx "github.com/influxdata/influxdb1-client/v2"
"github.com/spf13/pflag"
"golift.io/unifi"
)
// Start begins the application from a CLI.
// Parses flags, parses config and executes Run().
func Start() error {
log.SetFlags(log.LstdFlags)
up := &UnifiPoller{
Flag: &Flag{},
Config: &Config{
// Preload our defaults.
InfluxURL: defaultInfluxURL,
InfluxUser: defaultInfluxUser,
InfluxPass: defaultInfluxPass,
InfluxDB: defaultInfluxDB,
UnifiUser: defaultUnifiUser,
UnifiPass: os.Getenv("UNIFI_PASSWORD"), // deprecated name.
UnifiBase: defaultUnifiURL,
Interval: Duration{defaultInterval},
Sites: []string{"all"},
}}
up.Flag.Parse(os.Args[1:])
if up.Flag.ShowVer {
fmt.Printf("unifi-poller v%s\n", Version)
return nil // don't run anything else w/ version request.
}
if up.Flag.DumpJSON == "" { // do not print this when dumping JSON.
up.Logf("Loading Configuration File: %s", up.Flag.ConfigFile)
}
// Parse config file.
if err := up.Config.ParseFile(up.Flag.ConfigFile); err != nil {
up.Flag.Usage()
return err
}
// Update Config with ENV variable overrides.
if err := up.Config.ParseENV(); err != nil {
return err
}
return up.Run()
}
// Parse turns CLI arguments into data structures. Called by Start() on startup.
func (f *Flag) Parse(args []string) {
f.FlagSet = pflag.NewFlagSet("unifi-poller", pflag.ExitOnError)
f.Usage = func() {
fmt.Println("Usage: unifi-poller [--config=/path/to/up.conf] [--version]")
f.PrintDefaults()
}
f.StringVarP(&f.DumpJSON, "dumpjson", "j", "",
"This debug option prints a json payload and exits. See man page for more info.")
f.StringVarP(&f.ConfigFile, "config", "c", DefaultConfFile, "Poller config file path.")
f.BoolVarP(&f.ShowVer, "version", "v", false, "Print the version and exit.")
_ = f.FlagSet.Parse(args) // pflag.ExitOnError means this will never return error.
}
// Run invokes all the application logic and routines.
func (u *UnifiPoller) Run() (err error) {
if u.Flag.DumpJSON != "" {
return u.DumpJSONPayload()
}
if u.Config.Debug {
log.SetFlags(log.Lshortfile | log.Lmicroseconds | log.Ldate)
u.LogDebugf("Debug Logging Enabled")
}
log.Printf("[INFO] UniFi Poller v%v Starting Up! PID: %d", Version, os.Getpid())
if err = u.GetUnifi(); err != nil {
return err
}
u.Logf("Polling UniFi Controller at %s v%s as user %s. Sites: %v",
u.Config.UnifiBase, u.Unifi.ServerVersion, u.Config.UnifiUser, u.Config.Sites)
if err = u.GetInfluxDB(); err != nil {
return err
}
u.Logf("Logging Measurements to InfluxDB at %s as user %s", u.Config.InfluxURL, u.Config.InfluxUser)
switch strings.ToLower(u.Config.Mode) {
case "influxlambda", "lambdainflux", "lambda_influx", "influx_lambda":
u.LogDebugf("Lambda Mode Enabled")
u.LastCheck = time.Now()
return u.CollectAndReport()
default:
return u.PollController()
}
}
// GetInfluxDB returns an InfluxDB interface.
func (u *UnifiPoller) GetInfluxDB() (err error) {
u.Influx, err = influx.NewHTTPClient(influx.HTTPConfig{
Addr: u.Config.InfluxURL,
Username: u.Config.InfluxUser,
Password: u.Config.InfluxPass,
TLSConfig: &tls.Config{InsecureSkipVerify: u.Config.InfxBadSSL},
})
if err != nil {
return fmt.Errorf("influxdb: %v", err)
}
return nil
}
// GetUnifi returns a UniFi controller interface.
func (u *UnifiPoller) GetUnifi() (err error) {
// Create an authenticated session to the Unifi Controller.
u.Unifi, err = unifi.NewUnifi(&unifi.Config{
User: u.Config.UnifiUser,
Pass: u.Config.UnifiPass,
URL: u.Config.UnifiBase,
VerifySSL: u.Config.VerifySSL,
ErrorLog: u.LogErrorf, // Log all errors.
DebugLog: u.LogDebugf, // Log debug messages.
})
if err != nil {
return fmt.Errorf("unifi controller: %v", err)
}
u.LogDebugf("Authenticated with controller successfully")
return u.CheckSites()
}

View File

@ -1,250 +0,0 @@
package unifipoller
import (
"fmt"
"log"
"strings"
"time"
influx "github.com/influxdata/influxdb1-client/v2"
"golift.io/unifi"
)
// CheckSites makes sure the list of provided sites exists on the controller.
// This does not run in Lambda (run-once) mode.
func (u *UnifiPoller) CheckSites() error {
if strings.Contains(strings.ToLower(u.Config.Mode), "lambda") {
return nil // Skip this in lambda mode.
}
u.LogDebugf("Checking Controller Sites List")
sites, err := u.Unifi.GetSites()
if err != nil {
return err
}
msg := []string{}
for _, site := range sites {
msg = append(msg, site.Name+" ("+site.Desc+")")
}
u.Logf("Found %d site(s) on controller: %v", len(msg), strings.Join(msg, ", "))
if StringInSlice("all", u.Config.Sites) {
u.Config.Sites = []string{"all"}
return nil
}
FIRST:
for _, s := range u.Config.Sites {
for _, site := range sites {
if s == site.Name {
continue FIRST
}
}
// This is fine, it may get added later.
u.LogErrorf("configured site not found on controller: %v", s)
}
return nil
}
// PollController runs forever, polling UniFi, and pushing to influx.
// This is started by Run() after everything checks out.
func (u *UnifiPoller) PollController() error {
interval := u.Config.Interval.Round(time.Second)
log.Println("[INFO] Everything checks out! Poller started, interval:", interval)
ticker := time.NewTicker(interval)
for u.LastCheck = range ticker.C {
var err error
if u.Config.ReAuth {
u.LogDebugf("Re-authenticating to UniFi Controller")
// Some users need to re-auth every interval because the cookie times out.
if err = u.Unifi.Login(); err != nil {
u.LogError(err, "re-authenticating")
}
}
if err == nil {
// Only run this if the authentication procedure didn't return error.
_ = u.CollectAndReport()
}
if u.errorCount > 0 {
return fmt.Errorf("controller or influxdb errors, stopping poller")
}
}
return nil
}
// CollectAndReport collects measurements and reports them to influxdb.
// Can be called once or in a ticker loop. This function and all the ones below
// handle their own logging. An error is returned so the calling function may
// determine if there was a read or write error and act on it. This is currently
// called in two places in this library. One returns an error, one does not.
func (u *UnifiPoller) CollectAndReport() error {
metrics, err := u.CollectMetrics()
if err != nil {
return err
}
if err := u.AugmentMetrics(metrics); err != nil {
return err
}
err = u.ReportMetrics(metrics)
u.LogError(err, "reporting metrics")
return err
}
// CollectMetrics grabs all the measurements from a UniFi controller and returns them.
// This also creates an InfluxDB writer, and returns an error if that fails.
func (u *UnifiPoller) CollectMetrics() (*Metrics, error) {
m := &Metrics{TS: u.LastCheck} // At this point, it's the Current Check.
var err error
// Get the sites we care about.
m.Sites, err = u.GetFilteredSites()
u.LogError(err, "unifi.GetSites()")
if u.Config.CollectIDS {
// Check back in time since twice the interval. Dups are discarded by InfluxDB.
m.IDSList, err = u.Unifi.GetIDS(m.Sites, time.Now().Add(2*u.Config.Interval.Duration), time.Now())
u.LogError(err, "unifi.GetIDS()")
}
// Get all the points.
m.Clients, err = u.Unifi.GetClients(m.Sites)
u.LogError(err, "unifi.GetClients()")
m.Devices, err = u.Unifi.GetDevices(m.Sites)
u.LogError(err, "unifi.GetDevices()")
// Make a new Influx Points Batcher.
m.BatchPoints, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.Config.InfluxDB})
u.LogError(err, "influx.NewBatchPoints")
return m, err
}
// AugmentMetrics is our middleware layer between collecting metrics and writing them.
// This is where we can manipuate the returned data or make arbitrary decisions.
// This function currently adds parent device names to client metrics.
func (u *UnifiPoller) AugmentMetrics(metrics *Metrics) error {
if metrics == nil || metrics.Devices == nil || metrics.Clients == nil {
return fmt.Errorf("nil metrics, augment impossible")
}
devices := make(map[string]string)
bssdIDs := make(map[string]string)
for _, r := range metrics.UAPs {
devices[r.Mac] = r.Name
for _, v := range r.VapTable {
bssdIDs[v.Bssid] = fmt.Sprintf("%s %s %s:", r.Name, v.Radio, v.RadioName)
}
}
for _, r := range metrics.USGs {
devices[r.Mac] = r.Name
}
for _, r := range metrics.USWs {
devices[r.Mac] = r.Name
}
for _, r := range metrics.UDMs {
devices[r.Mac] = r.Name
}
// These come blank, so set them here.
for i, c := range metrics.Clients {
metrics.Clients[i].SwName = devices[c.SwMac]
metrics.Clients[i].ApName = devices[c.ApMac]
metrics.Clients[i].GwName = devices[c.GwMac]
metrics.Clients[i].RadioDescription = bssdIDs[metrics.Clients[i].Bssid] + metrics.Clients[i].RadioProto
}
return nil
}
// ReportMetrics batches all the metrics and writes them to InfluxDB.
// Returns an error if the write to influx fails.
func (u *UnifiPoller) ReportMetrics(metrics *Metrics) error {
// Batch (and send) all the points.
for _, err := range metrics.ProcessPoints() {
u.LogError(err, "asset.Points()")
}
err := u.Influx.Write(metrics.BatchPoints)
if err != nil {
return fmt.Errorf("influxdb.Write(points): %v", err)
}
var fields, points int
for _, p := range metrics.Points() {
points++
i, _ := p.Fields()
fields += len(i)
}
idsMsg := ""
if u.Config.CollectIDS {
idsMsg = fmt.Sprintf("IDS Events: %d, ", len(metrics.IDSList))
}
u.Logf("UniFi Measurements Recorded. Sites: %d, Clients: %d, "+
"Wireless APs: %d, Gateways: %d, Switches: %d, %sPoints: %d, Fields: %d",
len(metrics.Sites), len(metrics.Clients), len(metrics.UAPs),
len(metrics.UDMs)+len(metrics.USGs), len(metrics.USWs), idsMsg, points, fields)
return nil
}
// ProcessPoints batches all device and client data into influxdb data points.
// Call this after you've collected all the data you care about.
// This function is sorta weird and returns a slice of errors. The reasoning is
// that some points may process while others fail, so we attempt to process them
// all. This is (usually) run in a loop, so we can't really exit on error,
// we just log the errors and tally them on a counter. In reality, this never
// returns any errors because we control the data going in; cool right? But we
// still check&log it in case the data going is skewed up and causes errors!
func (m *Metrics) ProcessPoints() []error {
errs := []error{}
processPoints := func(m *Metrics, p []*influx.Point, err error) {
switch {
case err != nil:
errs = append(errs, err)
case p == nil:
default:
m.BatchPoints.AddPoints(p)
}
}
for _, asset := range m.Sites {
pts, err := SitePoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.Clients {
pts, err := ClientPoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.IDSList {
pts, err := IDSPoints(asset) // no m.TS.
processPoints(m, pts, err)
}
if m.Devices == nil {
return errs
}
for _, asset := range m.Devices.UAPs {
pts, err := UAPPoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.Devices.USGs {
pts, err := USGPoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.Devices.USWs {
pts, err := USWPoints(asset, m.TS)
processPoints(m, pts, err)
}
for _, asset := range m.Devices.UDMs {
pts, err := UDMPoints(asset, m.TS)
processPoints(m, pts, err)
}
return errs
}
// GetFilteredSites returns a list of sites to fetch data for.
// Omits requested but unconfigured sites. Grabs the full list from the
// controller and returns the sites provided in the config file.
func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) {
sites, err := u.Unifi.GetSites()
if err != nil {
return nil, err
} else if len(u.Config.Sites) < 1 || StringInSlice("all", u.Config.Sites) {
return sites, nil
}
var i int
for _, s := range sites {
// Only include valid sites in the request filter.
if StringInSlice(s.Name, u.Config.Sites) {
sites[i] = s
i++
}
}
return sites[:i], nil
}