diff --git a/integrations/inputunifi/.metadata.sh b/integrations/inputunifi/.metadata.sh index 4b7241e8..a6c7f5a4 100755 --- a/integrations/inputunifi/.metadata.sh +++ b/integrations/inputunifi/.metadata.sh @@ -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)) diff --git a/integrations/inputunifi/Gopkg.lock b/integrations/inputunifi/Gopkg.lock index ebe365dd..9bc268e6 100644 --- a/integrations/inputunifi/Gopkg.lock +++ b/integrations/inputunifi/Gopkg.lock @@ -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", diff --git a/integrations/inputunifi/examples/MANUAL.md b/integrations/inputunifi/examples/MANUAL.md index c9ecd0de..4648bc4c 100644 --- a/integrations/inputunifi/examples/MANUAL.md +++ b/integrations/inputunifi/examples/MANUAL.md @@ -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. diff --git a/integrations/inputunifi/examples/up.conf.example b/integrations/inputunifi/examples/up.conf.example index ae6497e7..0f1b3041 100644 --- a/integrations/inputunifi/examples/up.conf.example +++ b/integrations/inputunifi/examples/up.conf.example @@ -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" diff --git a/integrations/inputunifi/examples/up.json.example b/integrations/inputunifi/examples/up.json.example index e59ec7c3..e9961505 100644 --- a/integrations/inputunifi/examples/up.json.example +++ b/integrations/inputunifi/examples/up.json.example @@ -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", diff --git a/integrations/inputunifi/examples/up.xml.example b/integrations/inputunifi/examples/up.xml.example index 87f1cac5..3022948b 100644 --- a/integrations/inputunifi/examples/up.xml.example +++ b/integrations/inputunifi/examples/up.xml.example @@ -18,7 +18,8 @@ 30s @@ -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! --> influx + + 0.0.0.0:9130 + diff --git a/integrations/inputunifi/examples/up.yaml.example b/integrations/inputunifi/examples/up.yaml.example index 1042c4e1..4dfe2e67 100644 --- a/integrations/inputunifi/examples/up.yaml.example +++ b/integrations/inputunifi/examples/up.yaml.example @@ -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" diff --git a/integrations/inputunifi/influxunifi/README.md b/integrations/inputunifi/influxunifi/README.md new file mode 100644 index 00000000..cbc606c0 --- /dev/null +++ b/integrations/inputunifi/influxunifi/README.md @@ -0,0 +1,4 @@ +# influx + +This package provides the methods to turn UniFi measurements into influx +data-points with appropriate tags and fields. diff --git a/integrations/inputunifi/unifipoller/influx_clients.go b/integrations/inputunifi/influxunifi/clients.go similarity index 99% rename from integrations/inputunifi/unifipoller/influx_clients.go rename to integrations/inputunifi/influxunifi/clients.go index 3ecb8ce0..5a57331b 100644 --- a/integrations/inputunifi/unifipoller/influx_clients.go +++ b/integrations/inputunifi/influxunifi/clients.go @@ -1,4 +1,4 @@ -package unifipoller +package influxunifi import ( "time" diff --git a/integrations/inputunifi/unifipoller/influx_ids.go b/integrations/inputunifi/influxunifi/ids.go similarity index 98% rename from integrations/inputunifi/unifipoller/influx_ids.go rename to integrations/inputunifi/influxunifi/ids.go index 2e78cc28..bf444f5f 100644 --- a/integrations/inputunifi/unifipoller/influx_ids.go +++ b/integrations/inputunifi/influxunifi/ids.go @@ -1,4 +1,4 @@ -package unifipoller +package influxunifi import ( influx "github.com/influxdata/influxdb1-client/v2" diff --git a/integrations/inputunifi/influxunifi/metrics.go b/integrations/inputunifi/influxunifi/metrics.go new file mode 100644 index 00000000..3919c8c7 --- /dev/null +++ b/integrations/inputunifi/influxunifi/metrics.go @@ -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 +} diff --git a/integrations/inputunifi/unifipoller/influx_site.go b/integrations/inputunifi/influxunifi/site.go similarity index 99% rename from integrations/inputunifi/unifipoller/influx_site.go rename to integrations/inputunifi/influxunifi/site.go index 5cd45964..0bd647ac 100644 --- a/integrations/inputunifi/unifipoller/influx_site.go +++ b/integrations/inputunifi/influxunifi/site.go @@ -1,4 +1,4 @@ -package unifipoller +package influxunifi import ( "strings" diff --git a/integrations/inputunifi/unifipoller/influx_uap.go b/integrations/inputunifi/influxunifi/uap.go similarity index 99% rename from integrations/inputunifi/unifipoller/influx_uap.go rename to integrations/inputunifi/influxunifi/uap.go index 968682f6..1744b0c0 100644 --- a/integrations/inputunifi/unifipoller/influx_uap.go +++ b/integrations/inputunifi/influxunifi/uap.go @@ -1,4 +1,4 @@ -package unifipoller +package influxunifi import ( "time" diff --git a/integrations/inputunifi/unifipoller/influx_udm.go b/integrations/inputunifi/influxunifi/udm.go similarity index 99% rename from integrations/inputunifi/unifipoller/influx_udm.go rename to integrations/inputunifi/influxunifi/udm.go index 2062c32c..fa968115 100644 --- a/integrations/inputunifi/unifipoller/influx_udm.go +++ b/integrations/inputunifi/influxunifi/udm.go @@ -1,4 +1,4 @@ -package unifipoller +package influxunifi import ( "time" diff --git a/integrations/inputunifi/unifipoller/influx_usg.go b/integrations/inputunifi/influxunifi/usg.go similarity index 99% rename from integrations/inputunifi/unifipoller/influx_usg.go rename to integrations/inputunifi/influxunifi/usg.go index 4549247a..606011c0 100644 --- a/integrations/inputunifi/unifipoller/influx_usg.go +++ b/integrations/inputunifi/influxunifi/usg.go @@ -1,4 +1,4 @@ -package unifipoller +package influxunifi import ( "strings" diff --git a/integrations/inputunifi/unifipoller/influx_usw.go b/integrations/inputunifi/influxunifi/usw.go similarity index 99% rename from integrations/inputunifi/unifipoller/influx_usw.go rename to integrations/inputunifi/influxunifi/usw.go index f4c743f1..225a4340 100644 --- a/integrations/inputunifi/unifipoller/influx_usw.go +++ b/integrations/inputunifi/influxunifi/usw.go @@ -1,4 +1,4 @@ -package unifipoller +package influxunifi import ( "time" diff --git a/integrations/inputunifi/init/docker/docker-compose.env.example b/integrations/inputunifi/init/docker/docker-compose.env.example index 6c48b3cf..1222c0c8 100644 --- a/integrations/inputunifi/init/docker/docker-compose.env.example +++ b/integrations/inputunifi/init/docker/docker-compose.env.example @@ -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 diff --git a/integrations/inputunifi/init/docker/docker-compose.yml b/integrations/inputunifi/init/docker/docker-compose.yml index f9f6a77e..fcdbdddf 100644 --- a/integrations/inputunifi/init/docker/docker-compose.yml +++ b/integrations/inputunifi/init/docker/docker-compose.yml @@ -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: diff --git a/integrations/inputunifi/main.go b/integrations/inputunifi/main.go index 768d4af6..f98e65c4 100644 --- a/integrations/inputunifi/main.go +++ b/integrations/inputunifi/main.go @@ -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) } } diff --git a/integrations/inputunifi/metrics/metrics.go b/integrations/inputunifi/metrics/metrics.go new file mode 100644 index 00000000..2d38f54e --- /dev/null +++ b/integrations/inputunifi/metrics/metrics.go @@ -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 +} diff --git a/integrations/inputunifi/unifipoller/build_macos.go b/integrations/inputunifi/poller/build_macos.go similarity index 88% rename from integrations/inputunifi/unifipoller/build_macos.go rename to integrations/inputunifi/poller/build_macos.go index d3eb3f50..1ab32471 100644 --- a/integrations/inputunifi/unifipoller/build_macos.go +++ b/integrations/inputunifi/poller/build_macos.go @@ -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" diff --git a/integrations/inputunifi/unifipoller/build_unix.go b/integrations/inputunifi/poller/build_unix.go similarity index 88% rename from integrations/inputunifi/unifipoller/build_unix.go rename to integrations/inputunifi/poller/build_unix.go index 5fd6bdca..c1001ac9 100644 --- a/integrations/inputunifi/unifipoller/build_unix.go +++ b/integrations/inputunifi/poller/build_unix.go @@ -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" diff --git a/integrations/inputunifi/unifipoller/build_windows.go b/integrations/inputunifi/poller/build_windows.go similarity index 88% rename from integrations/inputunifi/unifipoller/build_windows.go rename to integrations/inputunifi/poller/build_windows.go index 529a6570..5c31504f 100644 --- a/integrations/inputunifi/unifipoller/build_windows.go +++ b/integrations/inputunifi/poller/build_windows.go @@ -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` diff --git a/integrations/inputunifi/unifipoller/config.go b/integrations/inputunifi/poller/config.go similarity index 78% rename from integrations/inputunifi/unifipoller/config.go rename to integrations/inputunifi/poller/config.go index 56516dda..743d3996 100644 --- a/integrations/inputunifi/unifipoller/config.go +++ b/integrations/inputunifi/poller/config.go @@ -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,8 +113,8 @@ 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 - env := os.Getenv(ENVConfigPrefix + tag) // Then pull value from OS. + 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 } diff --git a/integrations/inputunifi/unifipoller/dumper.go b/integrations/inputunifi/poller/dumper.go similarity index 98% rename from integrations/inputunifi/unifipoller/dumper.go rename to integrations/inputunifi/poller/dumper.go index 01d821ef..da825485 100644 --- a/integrations/inputunifi/unifipoller/dumper.go +++ b/integrations/inputunifi/poller/dumper.go @@ -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 diff --git a/integrations/inputunifi/unifipoller/helpers.go b/integrations/inputunifi/poller/helpers.go similarity index 75% rename from integrations/inputunifi/unifipoller/helpers.go rename to integrations/inputunifi/poller/helpers.go index 8adf54be..71421b16 100644 --- a/integrations/inputunifi/unifipoller/helpers.go +++ b/integrations/inputunifi/poller/helpers.go @@ -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...)) } diff --git a/integrations/inputunifi/poller/influx.go b/integrations/inputunifi/poller/influx.go new file mode 100644 index 00000000..d428bdb8 --- /dev/null +++ b/integrations/inputunifi/poller/influx.go @@ -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) +} diff --git a/integrations/inputunifi/poller/prometheus.go b/integrations/inputunifi/poller/prometheus.go new file mode 100644 index 00000000..ae2daa63 --- /dev/null +++ b/integrations/inputunifi/poller/prometheus.go @@ -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)) +} diff --git a/integrations/inputunifi/poller/start.go b/integrations/inputunifi/poller/start.go new file mode 100644 index 00000000..11d70228 --- /dev/null +++ b/integrations/inputunifi/poller/start.go @@ -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 +} diff --git a/integrations/inputunifi/poller/unifi.go b/integrations/inputunifi/poller/unifi.go new file mode 100644 index 00000000..54135923 --- /dev/null +++ b/integrations/inputunifi/poller/unifi.go @@ -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 +} diff --git a/integrations/inputunifi/promunifi/README.md b/integrations/inputunifi/promunifi/README.md new file mode 100644 index 00000000..d0ca905d --- /dev/null +++ b/integrations/inputunifi/promunifi/README.md @@ -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. diff --git a/integrations/inputunifi/promunifi/clients.go b/integrations/inputunifi/promunifi/clients.go new file mode 100644 index 00000000..ac12bdf8 --- /dev/null +++ b/integrations/inputunifi/promunifi/clients.go @@ -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}, + */ + }) +} diff --git a/integrations/inputunifi/promunifi/collector.go b/integrations/inputunifi/promunifi/collector.go new file mode 100644 index 00000000..25cfd370 --- /dev/null +++ b/integrations/inputunifi/promunifi/collector.go @@ -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() + } +} diff --git a/integrations/inputunifi/promunifi/loops.go b/integrations/inputunifi/promunifi/loops.go new file mode 100644 index 00000000..9e086ec3 --- /dev/null +++ b/integrations/inputunifi/promunifi/loops.go @@ -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) + } +} diff --git a/integrations/inputunifi/promunifi/report.go b/integrations/inputunifi/promunifi/report.go new file mode 100644 index 00000000..016d3db8 --- /dev/null +++ b/integrations/inputunifi/promunifi/report.go @@ -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) +} diff --git a/integrations/inputunifi/promunifi/site.go b/integrations/inputunifi/promunifi/site.go new file mode 100644 index 00000000..0acc3cec --- /dev/null +++ b/integrations/inputunifi/promunifi/site.go @@ -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}, + }) + } + } +} diff --git a/integrations/inputunifi/promunifi/uap.go b/integrations/inputunifi/promunifi/uap.go new file mode 100644 index 00000000..ed8507df --- /dev/null +++ b/integrations/inputunifi/promunifi/uap.go @@ -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}, + }) + } + } +} diff --git a/integrations/inputunifi/promunifi/udm.go b/integrations/inputunifi/promunifi/udm.go new file mode 100644 index 00000000..9a17b41b --- /dev/null +++ b/integrations/inputunifi/promunifi/udm.go @@ -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) + } +} diff --git a/integrations/inputunifi/promunifi/usg.go b/integrations/inputunifi/promunifi/usg.go new file mode 100644 index 00000000..b8764522 --- /dev/null +++ b/integrations/inputunifi/promunifi/usg.go @@ -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}, + }) + } +} diff --git a/integrations/inputunifi/promunifi/usw.go b/integrations/inputunifi/promunifi/usw.go new file mode 100644 index 00000000..88f7e190 --- /dev/null +++ b/integrations/inputunifi/promunifi/usw.go @@ -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}, + }) + } +} diff --git a/integrations/inputunifi/unifipoller/start.go b/integrations/inputunifi/unifipoller/start.go deleted file mode 100644 index 334119a1..00000000 --- a/integrations/inputunifi/unifipoller/start.go +++ /dev/null @@ -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() -} diff --git a/integrations/inputunifi/unifipoller/unifi.go b/integrations/inputunifi/unifipoller/unifi.go deleted file mode 100644 index 57fd8c51..00000000 --- a/integrations/inputunifi/unifipoller/unifi.go +++ /dev/null @@ -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 -}