initial
This commit is contained in:
		
							parent
							
								
									f5830d903d
								
							
						
					
					
						commit
						5e203701b5
					
				
							
								
								
									
										14
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										14
									
								
								Makefile
								
								
								
								
							|  | @ -97,39 +97,39 @@ README.html: md2roff | |||
| # Binaries
 | ||||
| 
 | ||||
| build: $(BINARY) | ||||
| $(BINARY): main.go pkg/*/*.go | ||||
| $(BINARY): main.go | ||||
| 	go build -o $(BINARY) -ldflags "-w -s $(VERSION_LDFLAGS)" | ||||
| 
 | ||||
| linux: $(BINARY).amd64.linux | ||||
| $(BINARY).amd64.linux: main.go pkg/*/*.go | ||||
| $(BINARY).amd64.linux: main.go | ||||
| 	# Building linux 64-bit x86 binary. | ||||
| 	GOOS=linux GOARCH=amd64 go build -o $@ -ldflags "-w -s $(VERSION_LDFLAGS)" | ||||
| 
 | ||||
| linux386: $(BINARY).i386.linux | ||||
| $(BINARY).i386.linux: main.go pkg/*/*.go | ||||
| $(BINARY).i386.linux: main.go | ||||
| 	# Building linux 32-bit x86 binary. | ||||
| 	GOOS=linux GOARCH=386 go build -o $@ -ldflags "-w -s $(VERSION_LDFLAGS)" | ||||
| 
 | ||||
| arm: arm64 armhf | ||||
| 
 | ||||
| arm64: $(BINARY).arm64.linux | ||||
| $(BINARY).arm64.linux: main.go pkg/*/*.go | ||||
| $(BINARY).arm64.linux: main.go | ||||
| 	# Building linux 64-bit ARM binary. | ||||
| 	GOOS=linux GOARCH=arm64 go build -o $@ -ldflags "-w -s $(VERSION_LDFLAGS)" | ||||
| 
 | ||||
| armhf: $(BINARY).armhf.linux | ||||
| $(BINARY).armhf.linux: main.go pkg/*/*.go | ||||
| $(BINARY).armhf.linux: main.go | ||||
| 	# Building linux 32-bit ARM binary. | ||||
| 	GOOS=linux GOARCH=arm GOARM=6 go build -o $@ -ldflags "-w -s $(VERSION_LDFLAGS)" | ||||
| 
 | ||||
| macos: $(BINARY).amd64.macos | ||||
| $(BINARY).amd64.macos: main.go pkg/*/*.go | ||||
| $(BINARY).amd64.macos: main.go | ||||
| 	# Building darwin 64-bit x86 binary. | ||||
| 	GOOS=darwin GOARCH=amd64 go build -o $@ -ldflags "-w -s $(VERSION_LDFLAGS)" | ||||
| 
 | ||||
| exe: $(BINARY).amd64.exe | ||||
| windows: $(BINARY).amd64.exe | ||||
| $(BINARY).amd64.exe: main.go pkg/*/*.go | ||||
| $(BINARY).amd64.exe: main.go  | ||||
| 	# Building windows 64-bit x86 binary. | ||||
| 	GOOS=windows GOARCH=amd64 go build -o $@ -ldflags "-w -s $(VERSION_LDFLAGS)" | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										15
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										15
									
								
								go.mod
								
								
								
								
							|  | @ -1,13 +1,12 @@ | |||
| module github.com/davidnewhall/unifi-poller | ||||
| module github.com/unifi-poller/unifi-poller | ||||
| 
 | ||||
| go 1.13 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/BurntSushi/toml v0.3.1 // indirect | ||||
| 	github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d | ||||
| 	github.com/prometheus/client_golang v1.3.0 | ||||
| 	github.com/prometheus/common v0.7.0 | ||||
| 	github.com/spf13/pflag v1.0.5 | ||||
| 	golift.io/cnfg v0.0.5 | ||||
| 	golift.io/unifi v0.0.400 | ||||
| 	github.com/unifi-poller/influxunifi v0.0.1 | ||||
| 	github.com/unifi-poller/inputunifi v0.0.1 | ||||
| 	github.com/unifi-poller/poller v0.0.1 | ||||
| 	github.com/unifi-poller/promunifi v0.0.1 | ||||
| 	golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 // indirect | ||||
| 	gopkg.in/yaml.v2 v2.2.7 // indirect | ||||
| ) | ||||
|  |  | |||
							
								
								
									
										19
									
								
								go.sum
								
								
								
								
							
							
						
						
									
										19
									
								
								go.sum
								
								
								
								
							|  | @ -65,6 +65,20 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ | |||
| github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/unifi-poller/influxunifi v0.0.0-20191229010055-eac7cb2786a8 h1:6tLYxh52e01ZiL+qxBRssEzXHqKjryog9Ez08hnRdbI= | ||||
| github.com/unifi-poller/influxunifi v0.0.0-20191229010055-eac7cb2786a8/go.mod h1:wYuSwHJnuYHMQyLs9ZnDSoZkAj/otg229+8PytZ6lJw= | ||||
| github.com/unifi-poller/influxunifi v0.0.1 h1:zHTa1Wf+2bke+qoLoRmgtFjWq/Yr0Cr+ZjtrtawefRM= | ||||
| github.com/unifi-poller/influxunifi v0.0.1/go.mod h1:wYuSwHJnuYHMQyLs9ZnDSoZkAj/otg229+8PytZ6lJw= | ||||
| github.com/unifi-poller/inputunifi v0.0.0-20191229005859-343b6711d445 h1:bsEkBa6xK1M9/g/rBrIc6qLeel4kqvtqnNyXLDhu4Uw= | ||||
| github.com/unifi-poller/inputunifi v0.0.0-20191229005859-343b6711d445/go.mod h1:gmgiDi8RbaAJvtGf9ybDB+eguJzl/xyrpoqH5JUdWEk= | ||||
| github.com/unifi-poller/inputunifi v0.0.1 h1:97s6pneYypvYV+RPgI5CgsRsrCYJyqqsVtaBoDprgsk= | ||||
| github.com/unifi-poller/inputunifi v0.0.1/go.mod h1:gmgiDi8RbaAJvtGf9ybDB+eguJzl/xyrpoqH5JUdWEk= | ||||
| github.com/unifi-poller/poller v0.0.1 h1:/SIsahlUEVJ+v9+C94spjV58+MIqR5DucVZqOstj2vM= | ||||
| github.com/unifi-poller/poller v0.0.1/go.mod h1:sZfDL7wcVwenlkrm/92bsSuoKKUnjj0bwcSUCT+aA2s= | ||||
| github.com/unifi-poller/promunifi v0.0.0-20191229005654-36c9f9b67ca7 h1:82q6vD+Ij8RmLoGng5/exRrnFRYm2/tpkKOVhEUH864= | ||||
| github.com/unifi-poller/promunifi v0.0.0-20191229005654-36c9f9b67ca7/go.mod h1:U1fEJ/lCYTjkHmFhDBdEBMzIECo5Jz2G7ZBKtM7zkAw= | ||||
| github.com/unifi-poller/promunifi v0.0.1 h1:KMZPE73VyA/BQDuL3Oo6m5+hAU0solGoZ/9m7dAJtoI= | ||||
| github.com/unifi-poller/promunifi v0.0.1/go.mod h1:U1fEJ/lCYTjkHmFhDBdEBMzIECo5Jz2G7ZBKtM7zkAw= | ||||
| golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
|  | @ -76,7 +90,10 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h | |||
| golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191220142924-d4481acd189f h1:68K/z8GLUxV76xGSqwTWw2gyk/jwn79LUL43rES2g8o= | ||||
| golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM= | ||||
| golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golift.io/cnfg v0.0.5 h1:HnMU8Z9C/igKvir1dqaHx5BPuNGZrp99FCtdJyP2Z4I= | ||||
| golift.io/cnfg v0.0.5/go.mod h1:ScFDIJg/rJGHbRaed/i7g1lBhywEjB0JiP2uZr3xC3A= | ||||
|  | @ -89,3 +106,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 | |||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= | ||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= | ||||
| gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
|  |  | |||
							
								
								
									
										8
									
								
								main.go
								
								
								
								
							
							
						
						
									
										8
									
								
								main.go
								
								
								
								
							|  | @ -3,12 +3,12 @@ package main | |||
| import ( | ||||
| 	"log" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	"github.com/unifi-poller/poller" | ||||
| 	// Load input plugins!
 | ||||
| 	_ "github.com/davidnewhall/unifi-poller/pkg/inputunifi" | ||||
| 	_ "github.com/unifi-poller/inputunifi" | ||||
| 	// Load output plugins!
 | ||||
| 	_ "github.com/davidnewhall/unifi-poller/pkg/influxunifi" | ||||
| 	_ "github.com/davidnewhall/unifi-poller/pkg/promunifi" | ||||
| 	_ "github.com/unifi-poller/influxunifi" | ||||
| 	_ "github.com/unifi-poller/promunifi" | ||||
| ) | ||||
| 
 | ||||
| // Keep it simple.
 | ||||
|  |  | |||
|  | @ -1,4 +0,0 @@ | |||
| # influx | ||||
| 
 | ||||
| This package provides the methods to turn UniFi measurements into influx | ||||
| data-points with appropriate tags and fields. | ||||
|  | @ -1,95 +0,0 @@ | |||
| package influxunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // batchClient generates Unifi Client datapoints for InfluxDB.
 | ||||
| // These points can be passed directly to influx.
 | ||||
| func (u *InfluxUnifi) batchClient(r report, s *unifi.Client) { | ||||
| 	tags := map[string]string{ | ||||
| 		"mac":         s.Mac, | ||||
| 		"site_name":   s.SiteName, | ||||
| 		"source":      s.SourceName, | ||||
| 		"ap_name":     s.ApName, | ||||
| 		"gw_name":     s.GwName, | ||||
| 		"sw_name":     s.SwName, | ||||
| 		"oui":         s.Oui, | ||||
| 		"radio_name":  s.RadioName, | ||||
| 		"radio":       s.Radio, | ||||
| 		"radio_proto": s.RadioProto, | ||||
| 		"name":        s.Name, | ||||
| 		"fixed_ip":    s.FixedIP, | ||||
| 		"sw_port":     s.SwPort.Txt, | ||||
| 		"os_class":    s.OsClass.Txt, | ||||
| 		"os_name":     s.OsName.Txt, | ||||
| 		"dev_cat":     s.DevCat.Txt, | ||||
| 		"dev_id":      s.DevID.Txt, | ||||
| 		"dev_vendor":  s.DevVendor.Txt, | ||||
| 		"dev_family":  s.DevFamily.Txt, | ||||
| 		"is_wired":    s.IsWired.Txt, | ||||
| 		"is_guest":    s.IsGuest.Txt, | ||||
| 		"use_fixedip": s.UseFixedIP.Txt, | ||||
| 		"channel":     s.Channel.Txt, | ||||
| 		"vlan":        s.Vlan.Txt, | ||||
| 	} | ||||
| 	fields := map[string]interface{}{ | ||||
| 		"anomalies":        s.Anomalies, | ||||
| 		"ip":               s.IP, | ||||
| 		"essid":            s.Essid, | ||||
| 		"bssid":            s.Bssid, | ||||
| 		"channel":          s.Channel.Val, | ||||
| 		"hostname":         s.Name, | ||||
| 		"radio_desc":       s.RadioDescription, | ||||
| 		"satisfaction":     s.Satisfaction.Val, | ||||
| 		"bytes_r":          s.BytesR, | ||||
| 		"ccq":              s.Ccq, | ||||
| 		"noise":            s.Noise, | ||||
| 		"note":             s.Note, | ||||
| 		"roam_count":       s.RoamCount, | ||||
| 		"rssi":             s.Rssi, | ||||
| 		"rx_bytes":         s.RxBytes, | ||||
| 		"rx_bytes_r":       s.RxBytesR, | ||||
| 		"rx_packets":       s.RxPackets, | ||||
| 		"rx_rate":          s.RxRate, | ||||
| 		"signal":           s.Signal, | ||||
| 		"tx_bytes":         s.TxBytes, | ||||
| 		"tx_bytes_r":       s.TxBytesR, | ||||
| 		"tx_packets":       s.TxPackets, | ||||
| 		"tx_retries":       s.TxRetries, | ||||
| 		"tx_power":         s.TxPower, | ||||
| 		"tx_rate":          s.TxRate, | ||||
| 		"uptime":           s.Uptime, | ||||
| 		"wifi_tx_attempts": s.WifiTxAttempts, | ||||
| 		"wired-rx_bytes":   s.WiredRxBytes, | ||||
| 		"wired-rx_bytes-r": s.WiredRxBytesR, | ||||
| 		"wired-rx_packets": s.WiredRxPackets, | ||||
| 		"wired-tx_bytes":   s.WiredTxBytes, | ||||
| 		"wired-tx_bytes-r": s.WiredTxBytesR, | ||||
| 		"wired-tx_packets": s.WiredTxPackets, | ||||
| 	} | ||||
| 
 | ||||
| 	r.send(&metric{Table: "clients", Tags: tags, Fields: fields}) | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) batchClientDPI(r report, s *unifi.DPITable) { | ||||
| 	for _, dpi := range s.ByApp { | ||||
| 		r.send(&metric{ | ||||
| 			Table: "clientdpi", | ||||
| 			Tags: map[string]string{ | ||||
| 				"category":    unifi.DPICats.Get(dpi.Cat), | ||||
| 				"application": unifi.DPIApps.GetApp(dpi.Cat, dpi.App), | ||||
| 				"name":        s.Name, | ||||
| 				"mac":         s.MAC, | ||||
| 				"site_name":   s.SiteName, | ||||
| 				"source":      s.SourceName, | ||||
| 			}, | ||||
| 			Fields: map[string]interface{}{ | ||||
| 				"tx_packets": dpi.TxPackets, | ||||
| 				"rx_packets": dpi.RxPackets, | ||||
| 				"tx_bytes":   dpi.TxBytes, | ||||
| 				"rx_bytes":   dpi.RxBytes, | ||||
| 			}}, | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,42 +0,0 @@ | |||
| package influxunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // batchIDS generates intrusion detection datapoints for InfluxDB.
 | ||||
| // These points can be passed directly to influx.
 | ||||
| func (u *InfluxUnifi) batchIDS(r report, i *unifi.IDS) { | ||||
| 	tags := map[string]string{ | ||||
| 		"site_name":      i.SiteName, | ||||
| 		"source":         i.SourceName, | ||||
| 		"in_iface":       i.InIface, | ||||
| 		"event_type":     i.EventType, | ||||
| 		"proto":          i.Proto, | ||||
| 		"app_proto":      i.AppProto, | ||||
| 		"usgip":          i.Usgip, | ||||
| 		"country_code":   i.SrcipGeo.CountryCode, | ||||
| 		"country_name":   i.SrcipGeo.CountryName, | ||||
| 		"region":         i.SrcipGeo.Region, | ||||
| 		"city":           i.SrcipGeo.City, | ||||
| 		"postal_code":    i.SrcipGeo.PostalCode, | ||||
| 		"srcipASN":       i.SrcipASN, | ||||
| 		"usgipASN":       i.UsgipASN, | ||||
| 		"alert_category": i.InnerAlertCategory, | ||||
| 		"subsystem":      i.Subsystem, | ||||
| 		"catname":        i.Catname, | ||||
| 	} | ||||
| 	fields := map[string]interface{}{ | ||||
| 		"event_type":   i.EventType, | ||||
| 		"proto":        i.Proto, | ||||
| 		"app_proto":    i.AppProto, | ||||
| 		"usgip":        i.Usgip, | ||||
| 		"country_name": i.SrcipGeo.CountryName, | ||||
| 		"city":         i.SrcipGeo.City, | ||||
| 		"postal_code":  i.SrcipGeo.PostalCode, | ||||
| 		"srcipASN":     i.SrcipASN, | ||||
| 		"usgipASN":     i.UsgipASN, | ||||
| 	} | ||||
| 
 | ||||
| 	r.send(&metric{Table: "intrusion_detect", Tags: tags, Fields: fields}) | ||||
| } | ||||
|  | @ -1,295 +0,0 @@ | |||
| // Package influxunifi provides the methods to turn UniFi measurements into influx
 | ||||
| // data-points with appropriate tags and fields.
 | ||||
| package influxunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/tls" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	influx "github.com/influxdata/influxdb1-client/v2" | ||||
| 	"golift.io/cnfg" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	defaultInterval   = 30 * time.Second | ||||
| 	minimumInterval   = 10 * time.Second | ||||
| 	defaultInfluxDB   = "unifi" | ||||
| 	defaultInfluxUser = "unifipoller" | ||||
| 	defaultInfluxURL  = "http://127.0.0.1:8086" | ||||
| ) | ||||
| 
 | ||||
| // Config defines the data needed to store metrics in InfluxDB
 | ||||
| type Config struct { | ||||
| 	Interval  cnfg.Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"` | ||||
| 	Disable   bool          `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"` | ||||
| 	VerifySSL bool          `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` | ||||
| 	URL       string        `json:"url,omitempty" toml:"url,omitempty" xml:"url" yaml:"url"` | ||||
| 	User      string        `json:"user,omitempty" toml:"user,omitempty" xml:"user" yaml:"user"` | ||||
| 	Pass      string        `json:"pass,omitempty" toml:"pass,omitempty" xml:"pass" yaml:"pass"` | ||||
| 	DB        string        `json:"db,omitempty" toml:"db,omitempty" xml:"db" yaml:"db"` | ||||
| } | ||||
| 
 | ||||
| // InfluxDB allows the data to be nested in the config file.
 | ||||
| type InfluxDB struct { | ||||
| 	Config Config `json:"influxdb" toml:"influxdb" xml:"influxdb" yaml:"influxdb"` | ||||
| } | ||||
| 
 | ||||
| // InfluxUnifi is returned by New() after you provide a Config.
 | ||||
| type InfluxUnifi struct { | ||||
| 	Collector poller.Collect | ||||
| 	influx    influx.Client | ||||
| 	LastCheck time.Time | ||||
| 	*InfluxDB | ||||
| } | ||||
| 
 | ||||
| type metric struct { | ||||
| 	Table  string | ||||
| 	Tags   map[string]string | ||||
| 	Fields map[string]interface{} | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	u := &InfluxUnifi{InfluxDB: &InfluxDB{}, LastCheck: time.Now()} | ||||
| 
 | ||||
| 	poller.NewOutput(&poller.Output{ | ||||
| 		Name:   "influxdb", | ||||
| 		Config: u.InfluxDB, | ||||
| 		Method: u.Run, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // PollController runs forever, polling UniFi and pushing to InfluxDB
 | ||||
| // This is started by Run() or RunBoth() after everything checks out.
 | ||||
| func (u *InfluxUnifi) PollController() { | ||||
| 	interval := u.Config.Interval.Round(time.Second) | ||||
| 	ticker := time.NewTicker(interval) | ||||
| 	log.Printf("[INFO] Everything checks out! Poller started, InfluxDB interval: %v", interval) | ||||
| 
 | ||||
| 	for u.LastCheck = range ticker.C { | ||||
| 		metrics, ok, err := u.Collector.Metrics() | ||||
| 		if err != nil { | ||||
| 			u.Collector.LogErrorf("%v", err) | ||||
| 
 | ||||
| 			if !ok { | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		report, err := u.ReportMetrics(metrics) | ||||
| 		if err != nil { | ||||
| 			// XXX: reset and re-auth? not sure..
 | ||||
| 			u.Collector.LogErrorf("%v", err) | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		u.LogInfluxReport(report) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Run runs a ticker to poll the unifi server and update influxdb.
 | ||||
| func (u *InfluxUnifi) Run(c poller.Collect) error { | ||||
| 	var err error | ||||
| 
 | ||||
| 	if u.Config.Disable { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	u.Collector = c | ||||
| 	u.setConfigDefaults() | ||||
| 
 | ||||
| 	u.influx, err = influx.NewHTTPClient(influx.HTTPConfig{ | ||||
| 		Addr:      u.Config.URL, | ||||
| 		Username:  u.Config.User, | ||||
| 		Password:  u.Config.Pass, | ||||
| 		TLSConfig: &tls.Config{InsecureSkipVerify: !u.Config.VerifySSL}, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	u.PollController() | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) setConfigDefaults() { | ||||
| 	if u.Config.URL == "" { | ||||
| 		u.Config.URL = defaultInfluxURL | ||||
| 	} | ||||
| 
 | ||||
| 	if u.Config.User == "" { | ||||
| 		u.Config.User = defaultInfluxUser | ||||
| 	} | ||||
| 
 | ||||
| 	if u.Config.Pass == "" { | ||||
| 		u.Config.Pass = defaultInfluxUser | ||||
| 	} | ||||
| 
 | ||||
| 	if u.Config.DB == "" { | ||||
| 		u.Config.DB = defaultInfluxDB | ||||
| 	} | ||||
| 
 | ||||
| 	if u.Config.Interval.Duration == 0 { | ||||
| 		u.Config.Interval = cnfg.Duration{Duration: defaultInterval} | ||||
| 	} else if u.Config.Interval.Duration < minimumInterval { | ||||
| 		u.Config.Interval = cnfg.Duration{Duration: minimumInterval} | ||||
| 	} | ||||
| 
 | ||||
| 	u.Config.Interval = cnfg.Duration{Duration: u.Config.Interval.Duration.Round(time.Second)} | ||||
| } | ||||
| 
 | ||||
| // ReportMetrics batches all device and client data into influxdb data points.
 | ||||
| // Call this after you've collected all the data you care about.
 | ||||
| // Returns an error if influxdb calls fail, otherwise returns a report.
 | ||||
| func (u *InfluxUnifi) ReportMetrics(m *poller.Metrics) (*Report, error) { | ||||
| 	r := &Report{Metrics: m, ch: make(chan *metric), Start: time.Now()} | ||||
| 	defer close(r.ch) | ||||
| 
 | ||||
| 	var err error | ||||
| 
 | ||||
| 	// Make a new Influx Points Batcher.
 | ||||
| 	r.bp, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.Config.DB}) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("influx.NewBatchPoints: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	go u.collect(r, r.ch) | ||||
| 	// Batch all the points.
 | ||||
| 	u.loopPoints(r) | ||||
| 	r.wg.Wait() // wait for all points to finish batching!
 | ||||
| 
 | ||||
| 	// Send all the points.
 | ||||
| 	if err = u.influx.Write(r.bp); err != nil { | ||||
| 		return nil, fmt.Errorf("influxdb.Write(points): %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	r.Elapsed = time.Since(r.Start) | ||||
| 
 | ||||
| 	return r, nil | ||||
| } | ||||
| 
 | ||||
| // collect runs in a go routine and batches all the points.
 | ||||
| func (u *InfluxUnifi) collect(r report, ch chan *metric) { | ||||
| 	for m := range ch { | ||||
| 		pt, err := influx.NewPoint(m.Table, m.Tags, m.Fields, r.metrics().TS) | ||||
| 		if err != nil { | ||||
| 			r.error(err) | ||||
| 		} else { | ||||
| 			r.batch(m, pt) | ||||
| 		} | ||||
| 
 | ||||
| 		r.done() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // loopPoints kicks off 3 or 7 go routines to process metrics and send them
 | ||||
| // to the collect routine through the metric channel.
 | ||||
| func (u *InfluxUnifi) loopPoints(r report) { | ||||
| 	m := r.metrics() | ||||
| 
 | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.SitesDPI { | ||||
| 			u.batchSiteDPI(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.Sites { | ||||
| 			u.batchSite(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.ClientsDPI { | ||||
| 			u.batchClientDPI(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.Clients { | ||||
| 			u.batchClient(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.IDSList { | ||||
| 			u.batchIDS(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	u.loopDevicePoints(r) | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) loopDevicePoints(r report) { | ||||
| 	m := r.metrics() | ||||
| 	if m.Devices == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.UAPs { | ||||
| 			u.batchUAP(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.USGs { | ||||
| 			u.batchUSG(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.USWs { | ||||
| 			u.batchUSW(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.UDMs { | ||||
| 			u.batchUDM(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
| 
 | ||||
| // LogInfluxReport writes a log message after exporting to influxdb.
 | ||||
| func (u *InfluxUnifi) LogInfluxReport(r *Report) { | ||||
| 	idsMsg := fmt.Sprintf("IDS Events: %d, ", len(r.Metrics.IDSList)) | ||||
| 	u.Collector.Logf("UniFi Metrics Recorded. Sites: %d, Clients: %d, "+ | ||||
| 		"UAP: %d, USG/UDM: %d, USW: %d, %sPoints: %d, Fields: %d, Errs: %d, Elapsed: %v", | ||||
| 		len(r.Metrics.Sites), len(r.Metrics.Clients), len(r.Metrics.UAPs), | ||||
| 		len(r.Metrics.UDMs)+len(r.Metrics.USGs), len(r.Metrics.USWs), idsMsg, r.Total, | ||||
| 		r.Fields, len(r.Errors), r.Elapsed.Round(time.Millisecond)) | ||||
| } | ||||
|  | @ -1,64 +0,0 @@ | |||
| package influxunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	influx "github.com/influxdata/influxdb1-client/v2" | ||||
| ) | ||||
| 
 | ||||
| // Report is returned to the calling procedure after everything is processed.
 | ||||
| type Report struct { | ||||
| 	Metrics *poller.Metrics | ||||
| 	Errors  []error | ||||
| 	Total   int | ||||
| 	Fields  int | ||||
| 	Start   time.Time | ||||
| 	Elapsed time.Duration | ||||
| 	ch      chan *metric | ||||
| 	wg      sync.WaitGroup | ||||
| 	bp      influx.BatchPoints | ||||
| } | ||||
| 
 | ||||
| // report is an internal interface that can be mocked and overrridden for tests.
 | ||||
| type report interface { | ||||
| 	add() | ||||
| 	done() | ||||
| 	send(m *metric) | ||||
| 	error(err error) | ||||
| 	batch(m *metric, pt *influx.Point) | ||||
| 	metrics() *poller.Metrics | ||||
| } | ||||
| 
 | ||||
| func (r *Report) metrics() *poller.Metrics { | ||||
| 	return r.Metrics | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| } | ||||
| 
 | ||||
| /* The following methods are not thread safe. */ | ||||
| 
 | ||||
| func (r *Report) error(err error) { | ||||
| 	r.Errors = append(r.Errors, err) | ||||
| } | ||||
| 
 | ||||
| func (r *Report) batch(m *metric, p *influx.Point) { | ||||
| 	r.Total++ | ||||
| 	r.Fields += len(m.Fields) | ||||
| 	r.bp.AddPoint(p) | ||||
| } | ||||
|  | @ -1,78 +0,0 @@ | |||
| package influxunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // batchSite generates Unifi Sites' datapoints for InfluxDB.
 | ||||
| // These points can be passed directly to influx.
 | ||||
| func (u *InfluxUnifi) batchSite(r report, s *unifi.Site) { | ||||
| 	for _, h := range s.Health { | ||||
| 		tags := map[string]string{ | ||||
| 			"name":      s.Name, | ||||
| 			"site_name": s.SiteName, | ||||
| 			"source":    s.SourceName, | ||||
| 			"desc":      s.Desc, | ||||
| 			"status":    h.Status, | ||||
| 			"subsystem": h.Subsystem, | ||||
| 			"wan_ip":    h.WanIP, | ||||
| 			"gw_name":   h.GwName, | ||||
| 			"lan_ip":    h.LanIP, | ||||
| 		} | ||||
| 		fields := map[string]interface{}{ | ||||
| 			"num_user":                 h.NumUser.Val, | ||||
| 			"num_guest":                h.NumGuest.Val, | ||||
| 			"num_iot":                  h.NumIot.Val, | ||||
| 			"tx_bytes-r":               h.TxBytesR.Val, | ||||
| 			"rx_bytes-r":               h.RxBytesR.Val, | ||||
| 			"num_ap":                   h.NumAp.Val, | ||||
| 			"num_adopted":              h.NumAdopted.Val, | ||||
| 			"num_disabled":             h.NumDisabled.Val, | ||||
| 			"num_disconnected":         h.NumDisconnected.Val, | ||||
| 			"num_pending":              h.NumPending.Val, | ||||
| 			"num_gw":                   h.NumGw.Val, | ||||
| 			"wan_ip":                   h.WanIP, | ||||
| 			"num_sta":                  h.NumSta.Val, | ||||
| 			"gw_cpu":                   h.GwSystemStats.CPU.Val, | ||||
| 			"gw_mem":                   h.GwSystemStats.Mem.Val, | ||||
| 			"gw_uptime":                h.GwSystemStats.Uptime.Val, | ||||
| 			"latency":                  h.Latency.Val, | ||||
| 			"uptime":                   h.Uptime.Val, | ||||
| 			"drops":                    h.Drops.Val, | ||||
| 			"xput_up":                  h.XputUp.Val, | ||||
| 			"xput_down":                h.XputDown.Val, | ||||
| 			"speedtest_ping":           h.SpeedtestPing.Val, | ||||
| 			"speedtest_lastrun":        h.SpeedtestLastrun.Val, | ||||
| 			"num_sw":                   h.NumSw.Val, | ||||
| 			"remote_user_num_active":   h.RemoteUserNumActive.Val, | ||||
| 			"remote_user_num_inactive": h.RemoteUserNumInactive.Val, | ||||
| 			"remote_user_rx_bytes":     h.RemoteUserRxBytes.Val, | ||||
| 			"remote_user_tx_bytes":     h.RemoteUserTxBytes.Val, | ||||
| 			"remote_user_rx_packets":   h.RemoteUserRxPackets.Val, | ||||
| 			"remote_user_tx_packets":   h.RemoteUserTxPackets.Val, | ||||
| 			"num_new_alarms":           s.NumNewAlarms.Val, | ||||
| 		} | ||||
| 
 | ||||
| 		r.send(&metric{Table: "subsystems", Tags: tags, Fields: fields}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) batchSiteDPI(r report, s *unifi.DPITable) { | ||||
| 	for _, dpi := range s.ByApp { | ||||
| 		r.send(&metric{ | ||||
| 			Table: "sitedpi", | ||||
| 			Tags: map[string]string{ | ||||
| 				"category":    unifi.DPICats.Get(dpi.Cat), | ||||
| 				"application": unifi.DPIApps.GetApp(dpi.Cat, dpi.App), | ||||
| 				"site_name":   s.SiteName, | ||||
| 				"source":      s.SourceName, | ||||
| 			}, | ||||
| 			Fields: map[string]interface{}{ | ||||
| 				"tx_packets": dpi.TxPackets, | ||||
| 				"rx_packets": dpi.RxPackets, | ||||
| 				"tx_bytes":   dpi.TxBytes, | ||||
| 				"rx_bytes":   dpi.RxBytes, | ||||
| 			}}, | ||||
| 		) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,189 +0,0 @@ | |||
| package influxunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // batchUAP generates Wireless-Access-Point datapoints for InfluxDB.
 | ||||
| // These points can be passed directly to influx.
 | ||||
| func (u *InfluxUnifi) batchUAP(r report, s *unifi.UAP) { | ||||
| 	if !s.Adopted.Val || s.Locating.Val { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	tags := map[string]string{ | ||||
| 		"mac":       s.Mac, | ||||
| 		"site_name": s.SiteName, | ||||
| 		"source":    s.SourceName, | ||||
| 		"name":      s.Name, | ||||
| 		"version":   s.Version, | ||||
| 		"model":     s.Model, | ||||
| 		"serial":    s.Serial, | ||||
| 		"type":      s.Type, | ||||
| 	} | ||||
| 	fields := Combine(u.processUAPstats(s.Stat.Ap), u.batchSysStats(s.SysStats, s.SystemStats)) | ||||
| 	fields["ip"] = s.IP | ||||
| 	fields["bytes"] = s.Bytes.Val | ||||
| 	fields["last_seen"] = s.LastSeen.Val | ||||
| 	fields["rx_bytes"] = s.RxBytes.Val | ||||
| 	fields["tx_bytes"] = s.TxBytes.Val | ||||
| 	fields["uptime"] = s.Uptime.Val | ||||
| 	fields["state"] = s.State | ||||
| 	fields["user-num_sta"] = int(s.UserNumSta.Val) | ||||
| 	fields["guest-num_sta"] = int(s.GuestNumSta.Val) | ||||
| 	fields["num_sta"] = s.NumSta.Val | ||||
| 
 | ||||
| 	r.send(&metric{Table: "uap", Tags: tags, Fields: fields}) | ||||
| 	u.processRadTable(r, tags, s.RadioTable, s.RadioTableStats) | ||||
| 	u.processVAPTable(r, tags, s.VapTable) | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) processUAPstats(ap *unifi.Ap) map[string]interface{} { | ||||
| 	if ap == nil { | ||||
| 		return map[string]interface{}{} | ||||
| 	} | ||||
| 
 | ||||
| 	// Accumulative Statistics.
 | ||||
| 	return map[string]interface{}{ | ||||
| 		"stat_user-rx_packets":  ap.UserRxPackets.Val, | ||||
| 		"stat_guest-rx_packets": ap.GuestRxPackets.Val, | ||||
| 		"stat_rx_packets":       ap.RxPackets.Val, | ||||
| 		"stat_user-rx_bytes":    ap.UserRxBytes.Val, | ||||
| 		"stat_guest-rx_bytes":   ap.GuestRxBytes.Val, | ||||
| 		"stat_rx_bytes":         ap.RxBytes.Val, | ||||
| 		"stat_user-rx_errors":   ap.UserRxErrors.Val, | ||||
| 		"stat_guest-rx_errors":  ap.GuestRxErrors.Val, | ||||
| 		"stat_rx_errors":        ap.RxErrors.Val, | ||||
| 		"stat_user-rx_dropped":  ap.UserRxDropped.Val, | ||||
| 		"stat_guest-rx_dropped": ap.GuestRxDropped.Val, | ||||
| 		"stat_rx_dropped":       ap.RxDropped.Val, | ||||
| 		"stat_user-rx_crypts":   ap.UserRxCrypts.Val, | ||||
| 		"stat_guest-rx_crypts":  ap.GuestRxCrypts.Val, | ||||
| 		"stat_rx_crypts":        ap.RxCrypts.Val, | ||||
| 		"stat_user-rx_frags":    ap.UserRxFrags.Val, | ||||
| 		"stat_guest-rx_frags":   ap.GuestRxFrags.Val, | ||||
| 		"stat_rx_frags":         ap.RxFrags.Val, | ||||
| 		"stat_user-tx_packets":  ap.UserTxPackets.Val, | ||||
| 		"stat_guest-tx_packets": ap.GuestTxPackets.Val, | ||||
| 		"stat_tx_packets":       ap.TxPackets.Val, | ||||
| 		"stat_user-tx_bytes":    ap.UserTxBytes.Val, | ||||
| 		"stat_guest-tx_bytes":   ap.GuestTxBytes.Val, | ||||
| 		"stat_tx_bytes":         ap.TxBytes.Val, | ||||
| 		"stat_user-tx_errors":   ap.UserTxErrors.Val, | ||||
| 		"stat_guest-tx_errors":  ap.GuestTxErrors.Val, | ||||
| 		"stat_tx_errors":        ap.TxErrors.Val, | ||||
| 		"stat_user-tx_dropped":  ap.UserTxDropped.Val, | ||||
| 		"stat_guest-tx_dropped": ap.GuestTxDropped.Val, | ||||
| 		"stat_tx_dropped":       ap.TxDropped.Val, | ||||
| 		"stat_user-tx_retries":  ap.UserTxRetries.Val, | ||||
| 		"stat_guest-tx_retries": ap.GuestTxRetries.Val, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // processVAPTable creates points for Wifi Radios. This works with several types of UAP-capable devices.
 | ||||
| func (u *InfluxUnifi) processVAPTable(r report, t map[string]string, vt unifi.VapTable) { | ||||
| 	for _, s := range vt { | ||||
| 		tags := map[string]string{ | ||||
| 			"device_name": t["name"], | ||||
| 			"site_name":   t["site_name"], | ||||
| 			"source":      t["source"], | ||||
| 			"ap_mac":      s.ApMac, | ||||
| 			"bssid":       s.Bssid, | ||||
| 			"id":          s.ID, | ||||
| 			"name":        s.Name, | ||||
| 			"radio_name":  s.RadioName, | ||||
| 			"radio":       s.Radio, | ||||
| 			"essid":       s.Essid, | ||||
| 			"site_id":     s.SiteID, | ||||
| 			"usage":       s.Usage, | ||||
| 			"state":       s.State, | ||||
| 			"is_guest":    s.IsGuest.Txt, | ||||
| 		} | ||||
| 		fields := map[string]interface{}{ | ||||
| 			"ccq":                       s.Ccq, | ||||
| 			"mac_filter_rejections":     s.MacFilterRejections, | ||||
| 			"num_satisfaction_sta":      s.NumSatisfactionSta.Val, | ||||
| 			"avg_client_signal":         s.AvgClientSignal.Val, | ||||
| 			"satisfaction":              s.Satisfaction.Val, | ||||
| 			"satisfaction_now":          s.SatisfactionNow.Val, | ||||
| 			"num_sta":                   s.NumSta, | ||||
| 			"channel":                   s.Channel.Val, | ||||
| 			"rx_bytes":                  s.RxBytes.Val, | ||||
| 			"rx_crypts":                 s.RxCrypts.Val, | ||||
| 			"rx_dropped":                s.RxDropped.Val, | ||||
| 			"rx_errors":                 s.RxErrors.Val, | ||||
| 			"rx_frags":                  s.RxFrags.Val, | ||||
| 			"rx_nwids":                  s.RxNwids.Val, | ||||
| 			"rx_packets":                s.RxPackets.Val, | ||||
| 			"tx_bytes":                  s.TxBytes.Val, | ||||
| 			"tx_dropped":                s.TxDropped.Val, | ||||
| 			"tx_errors":                 s.TxErrors.Val, | ||||
| 			"tx_packets":                s.TxPackets.Val, | ||||
| 			"tx_power":                  s.TxPower.Val, | ||||
| 			"tx_retries":                s.TxRetries.Val, | ||||
| 			"tx_combined_retries":       s.TxCombinedRetries.Val, | ||||
| 			"tx_data_mpdu_bytes":        s.TxDataMpduBytes.Val, | ||||
| 			"tx_rts_retries":            s.TxRtsRetries.Val, | ||||
| 			"tx_success":                s.TxSuccess.Val, | ||||
| 			"tx_total":                  s.TxTotal.Val, | ||||
| 			"tx_tcp_goodbytes":          s.TxTCPStats.Goodbytes.Val, | ||||
| 			"tx_tcp_lat_avg":            s.TxTCPStats.LatAvg.Val, | ||||
| 			"tx_tcp_lat_max":            s.TxTCPStats.LatMax.Val, | ||||
| 			"tx_tcp_lat_min":            s.TxTCPStats.LatMin.Val, | ||||
| 			"rx_tcp_goodbytes":          s.RxTCPStats.Goodbytes.Val, | ||||
| 			"rx_tcp_lat_avg":            s.RxTCPStats.LatAvg.Val, | ||||
| 			"rx_tcp_lat_max":            s.RxTCPStats.LatMax.Val, | ||||
| 			"rx_tcp_lat_min":            s.RxTCPStats.LatMin.Val, | ||||
| 			"wifi_tx_latency_mov_avg":   s.WifiTxLatencyMov.Avg.Val, | ||||
| 			"wifi_tx_latency_mov_max":   s.WifiTxLatencyMov.Max.Val, | ||||
| 			"wifi_tx_latency_mov_min":   s.WifiTxLatencyMov.Min.Val, | ||||
| 			"wifi_tx_latency_mov_total": s.WifiTxLatencyMov.Total.Val, | ||||
| 			"wifi_tx_latency_mov_cuont": s.WifiTxLatencyMov.TotalCount.Val, | ||||
| 		} | ||||
| 
 | ||||
| 		r.send(&metric{Table: "uap_vaps", Tags: tags, Fields: fields}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) processRadTable(r report, t map[string]string, rt unifi.RadioTable, rts unifi.RadioTableStats) { | ||||
| 	for _, p := range rt { | ||||
| 		tags := map[string]string{ | ||||
| 			"device_name": t["name"], | ||||
| 			"site_name":   t["site_name"], | ||||
| 			"source":      t["source"], | ||||
| 			"channel":     p.Channel.Txt, | ||||
| 			"radio":       p.Radio, | ||||
| 		} | ||||
| 		fields := map[string]interface{}{ | ||||
| 			"current_antenna_gain": p.CurrentAntennaGain.Val, | ||||
| 			"ht":                   p.Ht.Txt, | ||||
| 			"max_txpower":          p.MaxTxpower.Val, | ||||
| 			"min_txpower":          p.MinTxpower.Val, | ||||
| 			"nss":                  p.Nss.Val, | ||||
| 			"radio_caps":           p.RadioCaps.Val, | ||||
| 		} | ||||
| 
 | ||||
| 		for _, t := range rts { | ||||
| 			if t.Name == p.Name { | ||||
| 				fields["ast_be_xmit"] = t.AstBeXmit.Val | ||||
| 				fields["channel"] = t.Channel.Val | ||||
| 				fields["cu_self_rx"] = t.CuSelfRx.Val | ||||
| 				fields["cu_self_tx"] = t.CuSelfTx.Val | ||||
| 				fields["cu_total"] = t.CuTotal.Val | ||||
| 				fields["extchannel"] = t.Extchannel.Val | ||||
| 				fields["gain"] = t.Gain.Val | ||||
| 				fields["guest-num_sta"] = t.GuestNumSta.Val | ||||
| 				fields["num_sta"] = t.NumSta.Val | ||||
| 				fields["radio"] = t.Radio | ||||
| 				fields["tx_packets"] = t.TxPackets.Val | ||||
| 				fields["tx_power"] = t.TxPower.Val | ||||
| 				fields["tx_retries"] = t.TxRetries.Val | ||||
| 				fields["user-num_sta"] = t.UserNumSta.Val | ||||
| 
 | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		r.send(&metric{Table: "uap_radios", Tags: tags, Fields: fields}) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,133 +0,0 @@ | |||
| package influxunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // Combine concatenates N maps. This will delete things if not used with caution.
 | ||||
| func Combine(in ...map[string]interface{}) map[string]interface{} { | ||||
| 	out := make(map[string]interface{}) | ||||
| 
 | ||||
| 	for i := range in { | ||||
| 		for k := range in[i] { | ||||
| 			out[k] = in[i][k] | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return out | ||||
| } | ||||
| 
 | ||||
| // batchSysStats is used by all device types.
 | ||||
| func (u *InfluxUnifi) batchSysStats(s unifi.SysStats, ss unifi.SystemStats) map[string]interface{} { | ||||
| 	return map[string]interface{}{ | ||||
| 		"loadavg_1":     s.Loadavg1.Val, | ||||
| 		"loadavg_5":     s.Loadavg5.Val, | ||||
| 		"loadavg_15":    s.Loadavg15.Val, | ||||
| 		"mem_used":      s.MemUsed.Val, | ||||
| 		"mem_buffer":    s.MemBuffer.Val, | ||||
| 		"mem_total":     s.MemTotal.Val, | ||||
| 		"cpu":           ss.CPU.Val, | ||||
| 		"mem":           ss.Mem.Val, | ||||
| 		"system_uptime": ss.Uptime.Val, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // batchUDM generates Unifi Gateway datapoints for InfluxDB.
 | ||||
| // These points can be passed directly to influx.
 | ||||
| func (u *InfluxUnifi) batchUDM(r report, s *unifi.UDM) { | ||||
| 	if !s.Adopted.Val || s.Locating.Val { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	tags := map[string]string{ | ||||
| 		"source":    s.SourceName, | ||||
| 		"mac":       s.Mac, | ||||
| 		"site_name": s.SiteName, | ||||
| 		"name":      s.Name, | ||||
| 		"version":   s.Version, | ||||
| 		"model":     s.Model, | ||||
| 		"serial":    s.Serial, | ||||
| 		"type":      s.Type, | ||||
| 	} | ||||
| 	fields := Combine( | ||||
| 		u.batchUSGstat(s.SpeedtestStatus, s.Stat.Gw, s.Uplink), | ||||
| 		u.batchSysStats(s.SysStats, s.SystemStats), | ||||
| 		map[string]interface{}{ | ||||
| 			"source":        s.SourceName, | ||||
| 			"ip":            s.IP, | ||||
| 			"bytes":         s.Bytes.Val, | ||||
| 			"last_seen":     s.LastSeen.Val, | ||||
| 			"license_state": s.LicenseState, | ||||
| 			"guest-num_sta": s.GuestNumSta.Val, | ||||
| 			"rx_bytes":      s.RxBytes.Val, | ||||
| 			"tx_bytes":      s.TxBytes.Val, | ||||
| 			"uptime":        s.Uptime.Val, | ||||
| 			"state":         s.State.Val, | ||||
| 			"user-num_sta":  s.UserNumSta.Val, | ||||
| 			"version":       s.Version, | ||||
| 			"num_desktop":   s.NumDesktop.Val, | ||||
| 			"num_handheld":  s.NumHandheld.Val, | ||||
| 			"num_mobile":    s.NumMobile.Val, | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	r.send(&metric{Table: "usg", Tags: tags, Fields: fields}) | ||||
| 	u.batchNetTable(r, tags, s.NetworkTable) | ||||
| 	u.batchUSGwans(r, tags, s.Wan1, s.Wan2) | ||||
| 
 | ||||
| 	tags = map[string]string{ | ||||
| 		"mac":       s.Mac, | ||||
| 		"site_name": s.SiteName, | ||||
| 		"source":    s.SourceName, | ||||
| 		"name":      s.Name, | ||||
| 		"version":   s.Version, | ||||
| 		"model":     s.Model, | ||||
| 		"serial":    s.Serial, | ||||
| 		"type":      s.Type, | ||||
| 	} | ||||
| 	fields = Combine( | ||||
| 		u.batchUSWstat(s.Stat.Sw), | ||||
| 		map[string]interface{}{ | ||||
| 			"guest-num_sta": s.GuestNumSta.Val, | ||||
| 			"ip":            s.IP, | ||||
| 			"bytes":         s.Bytes.Val, | ||||
| 			"last_seen":     s.LastSeen.Val, | ||||
| 			"rx_bytes":      s.RxBytes.Val, | ||||
| 			"tx_bytes":      s.TxBytes.Val, | ||||
| 			"uptime":        s.Uptime.Val, | ||||
| 			"state":         s.State.Val, | ||||
| 		}) | ||||
| 
 | ||||
| 	r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) | ||||
| 	u.batchPortTable(r, tags, s.PortTable) | ||||
| 
 | ||||
| 	if s.Stat.Ap == nil { | ||||
| 		return // we're done now. the following code process UDM (non-pro) UAP data.
 | ||||
| 	} | ||||
| 
 | ||||
| 	tags = map[string]string{ | ||||
| 		"mac":       s.Mac, | ||||
| 		"site_name": s.SiteName, | ||||
| 		"source":    s.SourceName, | ||||
| 		"name":      s.Name, | ||||
| 		"version":   s.Version, | ||||
| 		"model":     s.Model, | ||||
| 		"serial":    s.Serial, | ||||
| 		"type":      s.Type, | ||||
| 	} | ||||
| 	fields = u.processUAPstats(s.Stat.Ap) | ||||
| 	fields["ip"] = s.IP | ||||
| 	fields["bytes"] = s.Bytes.Val | ||||
| 	fields["last_seen"] = s.LastSeen.Val | ||||
| 	fields["rx_bytes"] = s.RxBytes.Val | ||||
| 	fields["tx_bytes"] = s.TxBytes.Val | ||||
| 	fields["uptime"] = s.Uptime.Val | ||||
| 	fields["state"] = s.State | ||||
| 	fields["user-num_sta"] = int(s.UserNumSta.Val) | ||||
| 	fields["guest-num_sta"] = int(s.GuestNumSta.Val) | ||||
| 	fields["num_sta"] = s.NumSta.Val | ||||
| 
 | ||||
| 	r.send(&metric{Table: "uap", Tags: tags, Fields: fields}) | ||||
| 	u.processRadTable(r, tags, *s.RadioTable, *s.RadioTableStats) | ||||
| 	u.processVAPTable(r, tags, *s.VapTable) | ||||
| } | ||||
|  | @ -1,140 +0,0 @@ | |||
| package influxunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // batchUSG generates Unifi Gateway datapoints for InfluxDB.
 | ||||
| // These points can be passed directly to influx.
 | ||||
| func (u *InfluxUnifi) batchUSG(r report, s *unifi.USG) { | ||||
| 	if !s.Adopted.Val || s.Locating.Val { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	tags := map[string]string{ | ||||
| 		"mac":       s.Mac, | ||||
| 		"site_name": s.SiteName, | ||||
| 		"source":    s.SourceName, | ||||
| 		"name":      s.Name, | ||||
| 		"version":   s.Version, | ||||
| 		"model":     s.Model, | ||||
| 		"serial":    s.Serial, | ||||
| 		"type":      s.Type, | ||||
| 	} | ||||
| 	fields := Combine( | ||||
| 		u.batchSysStats(s.SysStats, s.SystemStats), | ||||
| 		u.batchUSGstat(s.SpeedtestStatus, s.Stat.Gw, s.Uplink), | ||||
| 		map[string]interface{}{ | ||||
| 			"ip":            s.IP, | ||||
| 			"bytes":         s.Bytes.Val, | ||||
| 			"last_seen":     s.LastSeen.Val, | ||||
| 			"license_state": s.LicenseState, | ||||
| 			"guest-num_sta": s.GuestNumSta.Val, | ||||
| 			"rx_bytes":      s.RxBytes.Val, | ||||
| 			"tx_bytes":      s.TxBytes.Val, | ||||
| 			"uptime":        s.Uptime.Val, | ||||
| 			"state":         s.State.Val, | ||||
| 			"user-num_sta":  s.UserNumSta.Val, | ||||
| 			"version":       s.Version, | ||||
| 			"num_desktop":   s.NumDesktop.Val, | ||||
| 			"num_handheld":  s.NumHandheld.Val, | ||||
| 			"num_mobile":    s.NumMobile.Val, | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	r.send(&metric{Table: "usg", Tags: tags, Fields: fields}) | ||||
| 	u.batchNetTable(r, tags, s.NetworkTable) | ||||
| 	u.batchUSGwans(r, tags, s.Wan1, s.Wan2) | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) batchUSGstat(ss unifi.SpeedtestStatus, gw *unifi.Gw, ul unifi.Uplink) map[string]interface{} { | ||||
| 	if gw == nil { | ||||
| 		return map[string]interface{}{} | ||||
| 	} | ||||
| 
 | ||||
| 	return map[string]interface{}{ | ||||
| 		"uplink_latency":                 ul.Latency.Val, | ||||
| 		"uplink_speed":                   ul.Speed.Val, | ||||
| 		"speedtest-status_latency":       ss.Latency.Val, | ||||
| 		"speedtest-status_runtime":       ss.Runtime.Val, | ||||
| 		"speedtest-status_ping":          ss.StatusPing.Val, | ||||
| 		"speedtest-status_xput_download": ss.XputDownload.Val, | ||||
| 		"speedtest-status_xput_upload":   ss.XputUpload.Val, | ||||
| 		"lan-rx_bytes":                   gw.LanRxBytes.Val, | ||||
| 		"lan-rx_packets":                 gw.LanRxPackets.Val, | ||||
| 		"lan-tx_bytes":                   gw.LanTxBytes.Val, | ||||
| 		"lan-tx_packets":                 gw.LanTxPackets.Val, | ||||
| 		"lan-rx_dropped":                 gw.LanRxDropped.Val, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) batchUSGwans(r report, tags map[string]string, wans ...unifi.Wan) { | ||||
| 	for _, wan := range wans { | ||||
| 		if !wan.Up.Val { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		tags := map[string]string{ | ||||
| 			"device_name": tags["name"], | ||||
| 			"site_name":   tags["site_name"], | ||||
| 			"source":      tags["source"], | ||||
| 			"ip":          wan.IP, | ||||
| 			"purpose":     wan.Name, | ||||
| 			"mac":         wan.Mac, | ||||
| 			"ifname":      wan.Ifname, | ||||
| 			"type":        wan.Type, | ||||
| 			"up":          wan.Up.Txt, | ||||
| 			"enabled":     wan.Enable.Txt, | ||||
| 		} | ||||
| 		fields := map[string]interface{}{ | ||||
| 			"bytes-r":      wan.BytesR.Val, | ||||
| 			"full_duplex":  wan.FullDuplex.Val, | ||||
| 			"gateway":      wan.Gateway, | ||||
| 			"max_speed":    wan.MaxSpeed.Val, | ||||
| 			"rx_bytes":     wan.RxBytes.Val, | ||||
| 			"rx_bytes-r":   wan.RxBytesR.Val, | ||||
| 			"rx_dropped":   wan.RxDropped.Val, | ||||
| 			"rx_errors":    wan.RxErrors.Val, | ||||
| 			"rx_broadcast": wan.RxBroadcast.Val, | ||||
| 			"rx_multicast": wan.RxMulticast.Val, | ||||
| 			"rx_packets":   wan.RxPackets.Val, | ||||
| 			"speed":        wan.Speed.Val, | ||||
| 			"tx_bytes":     wan.TxBytes.Val, | ||||
| 			"tx_bytes-r":   wan.TxBytesR.Val, | ||||
| 			"tx_dropped":   wan.TxDropped.Val, | ||||
| 			"tx_errors":    wan.TxErrors.Val, | ||||
| 			"tx_packets":   wan.TxPackets.Val, | ||||
| 			"tx_broadcast": wan.TxBroadcast.Val, | ||||
| 			"tx_multicast": wan.TxMulticast.Val, | ||||
| 		} | ||||
| 
 | ||||
| 		r.send(&metric{Table: "usg_wan_ports", Tags: tags, Fields: fields}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) batchNetTable(r report, tags map[string]string, nt unifi.NetworkTable) { | ||||
| 	for _, p := range nt { | ||||
| 		tags := map[string]string{ | ||||
| 			"device_name": tags["name"], | ||||
| 			"site_name":   tags["site_name"], | ||||
| 			"source":      tags["source"], | ||||
| 			"up":          p.Up.Txt, | ||||
| 			"enabled":     p.Enabled.Txt, | ||||
| 			"ip":          p.IP, | ||||
| 			"mac":         p.Mac, | ||||
| 			"name":        p.Name, | ||||
| 			"domain_name": p.DomainName, | ||||
| 			"purpose":     p.Purpose, | ||||
| 			"is_guest":    p.IsGuest.Txt, | ||||
| 		} | ||||
| 		fields := map[string]interface{}{ | ||||
| 			"num_sta":    p.NumSta.Val, | ||||
| 			"rx_bytes":   p.RxBytes.Val, | ||||
| 			"rx_packets": p.RxPackets.Val, | ||||
| 			"tx_bytes":   p.TxBytes.Val, | ||||
| 			"tx_packets": p.TxPackets.Val, | ||||
| 		} | ||||
| 
 | ||||
| 		r.send(&metric{Table: "usg_networks", Tags: tags, Fields: fields}) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,114 +0,0 @@ | |||
| package influxunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // batchUSW generates Unifi Switch datapoints for InfluxDB.
 | ||||
| // These points can be passed directly to influx.
 | ||||
| func (u *InfluxUnifi) batchUSW(r report, s *unifi.USW) { | ||||
| 	if !s.Adopted.Val || s.Locating.Val { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	tags := map[string]string{ | ||||
| 		"mac":       s.Mac, | ||||
| 		"site_name": s.SiteName, | ||||
| 		"source":    s.SourceName, | ||||
| 		"name":      s.Name, | ||||
| 		"version":   s.Version, | ||||
| 		"model":     s.Model, | ||||
| 		"serial":    s.Serial, | ||||
| 		"type":      s.Type, | ||||
| 	} | ||||
| 	fields := Combine( | ||||
| 		u.batchUSWstat(s.Stat.Sw), | ||||
| 		u.batchSysStats(s.SysStats, s.SystemStats), | ||||
| 		map[string]interface{}{ | ||||
| 			"guest-num_sta":       s.GuestNumSta.Val, | ||||
| 			"ip":                  s.IP, | ||||
| 			"bytes":               s.Bytes.Val, | ||||
| 			"fan_level":           s.FanLevel.Val, | ||||
| 			"general_temperature": s.GeneralTemperature.Val, | ||||
| 			"last_seen":           s.LastSeen.Val, | ||||
| 			"rx_bytes":            s.RxBytes.Val, | ||||
| 			"tx_bytes":            s.TxBytes.Val, | ||||
| 			"uptime":              s.Uptime.Val, | ||||
| 			"state":               s.State.Val, | ||||
| 			"user-num_sta":        s.UserNumSta.Val, | ||||
| 		}) | ||||
| 
 | ||||
| 	r.send(&metric{Table: "usw", Tags: tags, Fields: fields}) | ||||
| 	u.batchPortTable(r, tags, s.PortTable) | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) batchUSWstat(sw *unifi.Sw) map[string]interface{} { | ||||
| 	if sw == nil { | ||||
| 		return map[string]interface{}{} | ||||
| 	} | ||||
| 
 | ||||
| 	return map[string]interface{}{ | ||||
| 		"stat_bytes":      sw.Bytes.Val, | ||||
| 		"stat_rx_bytes":   sw.RxBytes.Val, | ||||
| 		"stat_rx_crypts":  sw.RxCrypts.Val, | ||||
| 		"stat_rx_dropped": sw.RxDropped.Val, | ||||
| 		"stat_rx_errors":  sw.RxErrors.Val, | ||||
| 		"stat_rx_frags":   sw.RxFrags.Val, | ||||
| 		"stat_rx_packets": sw.TxPackets.Val, | ||||
| 		"stat_tx_bytes":   sw.TxBytes.Val, | ||||
| 		"stat_tx_dropped": sw.TxDropped.Val, | ||||
| 		"stat_tx_errors":  sw.TxErrors.Val, | ||||
| 		"stat_tx_packets": sw.TxPackets.Val, | ||||
| 		"stat_tx_retries": sw.TxRetries.Val, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *InfluxUnifi) batchPortTable(r report, t map[string]string, pt []unifi.Port) { | ||||
| 	for _, p := range pt { | ||||
| 		if !p.Up.Val || !p.Enable.Val { | ||||
| 			continue // only record UP ports.
 | ||||
| 		} | ||||
| 
 | ||||
| 		tags := map[string]string{ | ||||
| 			"site_name":   t["site_name"], | ||||
| 			"device_name": t["name"], | ||||
| 			"source":      t["source"], | ||||
| 			"name":        p.Name, | ||||
| 			"poe_mode":    p.PoeMode, | ||||
| 			"port_poe":    p.PortPoe.Txt, | ||||
| 			"port_idx":    p.PortIdx.Txt, | ||||
| 			"port_id":     t["name"] + " Port " + p.PortIdx.Txt, | ||||
| 			"poe_enable":  p.PoeEnable.Txt, | ||||
| 			"flowctrl_rx": p.FlowctrlRx.Txt, | ||||
| 			"flowctrl_tx": p.FlowctrlTx.Txt, | ||||
| 			"media":       p.Media, | ||||
| 		} | ||||
| 		fields := map[string]interface{}{ | ||||
| 			"dbytes_r":     p.BytesR.Val, | ||||
| 			"rx_broadcast": p.RxBroadcast.Val, | ||||
| 			"rx_bytes":     p.RxBytes.Val, | ||||
| 			"rx_bytes-r":   p.RxBytesR.Val, | ||||
| 			"rx_dropped":   p.RxDropped.Val, | ||||
| 			"rx_errors":    p.RxErrors.Val, | ||||
| 			"rx_multicast": p.RxMulticast.Val, | ||||
| 			"rx_packets":   p.RxPackets.Val, | ||||
| 			"speed":        p.Speed.Val, | ||||
| 			"stp_pathcost": p.StpPathcost.Val, | ||||
| 			"tx_broadcast": p.TxBroadcast.Val, | ||||
| 			"tx_bytes":     p.TxBytes.Val, | ||||
| 			"tx_bytes-r":   p.TxBytesR.Val, | ||||
| 			"tx_dropped":   p.TxDropped.Val, | ||||
| 			"tx_errors":    p.TxErrors.Val, | ||||
| 			"tx_multicast": p.TxMulticast.Val, | ||||
| 			"tx_packets":   p.TxPackets.Val, | ||||
| 		} | ||||
| 
 | ||||
| 		if p.PoeEnable.Val && p.PortPoe.Val { | ||||
| 			fields["poe_current"] = p.PoeCurrent.Val | ||||
| 			fields["poe_power"] = p.PoePower.Val | ||||
| 			fields["poe_voltage"] = p.PoeVoltage.Val | ||||
| 		} | ||||
| 
 | ||||
| 		r.send(&metric{Table: "usw_ports", Tags: tags, Fields: fields}) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,195 +0,0 @@ | |||
| package inputunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| func (u *InputUnifi) isNill(c *Controller) bool { | ||||
| 	u.RLock() | ||||
| 	defer u.RUnlock() | ||||
| 
 | ||||
| 	return c.Unifi == nil | ||||
| } | ||||
| 
 | ||||
| // newDynamicCntrlr creates and saves a controller (with auth cookie) for repeated use.
 | ||||
| // This is called when an unconfigured controller is requested.
 | ||||
| func (u *InputUnifi) newDynamicCntrlr(url string) (bool, *Controller) { | ||||
| 	u.Lock() | ||||
| 	defer u.Unlock() | ||||
| 
 | ||||
| 	c := u.dynamic[url] | ||||
| 	if c != nil { | ||||
| 		// it already exists.
 | ||||
| 		return false, c | ||||
| 	} | ||||
| 
 | ||||
| 	ccopy := u.Default // copy defaults into new controller
 | ||||
| 	c = &ccopy | ||||
| 	u.dynamic[url] = c | ||||
| 	c.Role = url | ||||
| 	c.URL = url | ||||
| 
 | ||||
| 	return true, c | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) dynamicController(url string) (*poller.Metrics, error) { | ||||
| 	if !strings.HasPrefix(url, "http") { | ||||
| 		return nil, fmt.Errorf("scrape filter match failed, and filter is not http URL") | ||||
| 	} | ||||
| 
 | ||||
| 	new, c := u.newDynamicCntrlr(url) | ||||
| 
 | ||||
| 	if new { | ||||
| 		u.Logf("Authenticating to Dynamic UniFi Controller: %s", url) | ||||
| 
 | ||||
| 		if err := u.getUnifi(c); err != nil { | ||||
| 			return nil, fmt.Errorf("authenticating to %s: %v", url, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return u.collectController(c) | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) collectController(c *Controller) (*poller.Metrics, error) { | ||||
| 	if u.isNill(c) { | ||||
| 		u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) | ||||
| 
 | ||||
| 		if err := u.getUnifi(c); err != nil { | ||||
| 			return nil, fmt.Errorf("re-authenticating to %s: %v", c.Role, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return u.pollController(c) | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) { | ||||
| 	var err error | ||||
| 
 | ||||
| 	u.RLock() | ||||
| 	defer u.RUnlock() | ||||
| 
 | ||||
| 	m := &poller.Metrics{TS: time.Now()} // At this point, it's the Current Check.
 | ||||
| 
 | ||||
| 	// Get the sites we care about.
 | ||||
| 	if m.Sites, err = u.getFilteredSites(c); err != nil { | ||||
| 		return m, fmt.Errorf("unifi.GetSites(%v): %v", c.URL, err) | ||||
| 	} | ||||
| 
 | ||||
| 	if c.SaveDPI { | ||||
| 		if m.SitesDPI, err = c.Unifi.GetSiteDPI(m.Sites); err != nil { | ||||
| 			return m, fmt.Errorf("unifi.GetSiteDPI(%v): %v", c.URL, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if m.ClientsDPI, err = c.Unifi.GetClientsDPI(m.Sites); err != nil { | ||||
| 			return m, fmt.Errorf("unifi.GetClientsDPI(%v): %v", c.URL, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if c.SaveIDS { | ||||
| 		m.IDSList, err = c.Unifi.GetIDS(m.Sites, time.Now().Add(2*time.Minute), time.Now()) | ||||
| 		if err != nil { | ||||
| 			return m, fmt.Errorf("unifi.GetIDS(%v): %v", c.URL, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Get all the points.
 | ||||
| 	if m.Clients, err = c.Unifi.GetClients(m.Sites); err != nil { | ||||
| 		return m, fmt.Errorf("unifi.GetClients(%v): %v", c.URL, err) | ||||
| 	} | ||||
| 
 | ||||
| 	if m.Devices, err = c.Unifi.GetDevices(m.Sites); err != nil { | ||||
| 		return m, fmt.Errorf("unifi.GetDevices(%v): %v", c.URL, err) | ||||
| 	} | ||||
| 
 | ||||
| 	return u.augmentMetrics(c, m), nil | ||||
| } | ||||
| 
 | ||||
| // 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 *InputUnifi) augmentMetrics(c *Controller, metrics *poller.Metrics) *poller.Metrics { | ||||
| 	if metrics == nil || metrics.Devices == nil || metrics.Clients == nil { | ||||
| 		return metrics | ||||
| 	} | ||||
| 
 | ||||
| 	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 { | ||||
| 		if devices[c.Mac] = c.Name; c.Name == "" { | ||||
| 			devices[c.Mac] = c.Hostname | ||||
| 		} | ||||
| 
 | ||||
| 		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 | ||||
| 	} | ||||
| 
 | ||||
| 	for i := range metrics.ClientsDPI { | ||||
| 		// Name on Client DPI data also comes blank, find it based on MAC address.
 | ||||
| 		metrics.ClientsDPI[i].Name = devices[metrics.ClientsDPI[i].MAC] | ||||
| 		if metrics.ClientsDPI[i].Name == "" { | ||||
| 			metrics.ClientsDPI[i].Name = metrics.ClientsDPI[i].MAC | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if !*c.SaveSites { | ||||
| 		metrics.Sites = nil | ||||
| 	} | ||||
| 
 | ||||
| 	return metrics | ||||
| } | ||||
| 
 | ||||
| // 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 *InputUnifi) getFilteredSites(c *Controller) (unifi.Sites, error) { | ||||
| 	u.RLock() | ||||
| 	defer u.RUnlock() | ||||
| 
 | ||||
| 	sites, err := c.Unifi.GetSites() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} else if len(c.Sites) < 1 || StringInSlice("all", c.Sites) { | ||||
| 		return sites, nil | ||||
| 	} | ||||
| 
 | ||||
| 	i := 0 | ||||
| 
 | ||||
| 	for _, s := range sites { | ||||
| 		// Only include valid sites in the request filter.
 | ||||
| 		if StringInSlice(s.Name, c.Sites) { | ||||
| 			sites[i] = s | ||||
| 			i++ | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return sites[:i], nil | ||||
| } | ||||
|  | @ -1,198 +0,0 @@ | |||
| // Package inputunifi implements the poller.Input interface and bridges the gap between
 | ||||
| // metrics from the unifi library, and the augments required to pump them into unifi-poller.
 | ||||
| package inputunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	defaultURL  = "https://127.0.0.1:8443" | ||||
| 	defaultUser = "unifipoller" | ||||
| 	defaultPass = "unifipoller" | ||||
| 	defaultSite = "all" | ||||
| ) | ||||
| 
 | ||||
| // InputUnifi contains the running data.
 | ||||
| type InputUnifi struct { | ||||
| 	*Config    `json:"unifi" toml:"unifi" xml:"unifi" yaml:"unifi"` | ||||
| 	dynamic    map[string]*Controller | ||||
| 	sync.Mutex // to lock the map above.
 | ||||
| 	poller.Logger | ||||
| } | ||||
| 
 | ||||
| // Controller represents the configuration for a UniFi Controller.
 | ||||
| // Each polled controller may have its own configuration.
 | ||||
| type Controller struct { | ||||
| 	VerifySSL bool         `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` | ||||
| 	SaveIDS   bool         `json:"save_ids" toml:"save_ids" xml:"save_ids" yaml:"save_ids"` | ||||
| 	SaveDPI   bool         `json:"save_dpi" toml:"save_dpi" xml:"save_dpi" yaml:"save_dpi"` | ||||
| 	SaveSites *bool        `json:"save_sites" toml:"save_sites" xml:"save_sites" yaml:"save_sites"` | ||||
| 	Role      string       `json:"role" toml:"role" xml:"role,attr" yaml:"role"` | ||||
| 	User      string       `json:"user" toml:"user" xml:"user" yaml:"user"` | ||||
| 	Pass      string       `json:"pass" toml:"pass" xml:"pass" yaml:"pass"` | ||||
| 	URL       string       `json:"url" toml:"url" xml:"url" yaml:"url"` | ||||
| 	Sites     []string     `json:"sites,omitempty" toml:"sites,omitempty" xml:"site" yaml:"sites"` | ||||
| 	Unifi     *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"` | ||||
| } | ||||
| 
 | ||||
| // Config contains our configuration data
 | ||||
| type Config struct { | ||||
| 	sync.RWMutex               // locks the Unifi struct member when re-authing to unifi.
 | ||||
| 	Default      Controller    `json:"defaults" toml:"defaults" xml:"default" yaml:"defaults"` | ||||
| 	Disable      bool          `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"` | ||||
| 	Dynamic      bool          `json:"dynamic" toml:"dynamic" xml:"dynamic,attr" yaml:"dynamic"` | ||||
| 	Controllers  []*Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"` | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	u := &InputUnifi{} | ||||
| 
 | ||||
| 	poller.NewInput(&poller.InputPlugin{ | ||||
| 		Name:   "unifi", | ||||
| 		Input:  u, // this library implements poller.Input interface for Metrics().
 | ||||
| 		Config: u, // Defines our config data interface.
 | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // getUnifi (re-)authenticates to a unifi controller.
 | ||||
| func (u *InputUnifi) getUnifi(c *Controller) error { | ||||
| 	var err error | ||||
| 
 | ||||
| 	u.Lock() | ||||
| 	defer u.Unlock() | ||||
| 
 | ||||
| 	if c.Unifi != nil { | ||||
| 		c.Unifi.CloseIdleConnections() | ||||
| 	} | ||||
| 
 | ||||
| 	// Create an authenticated session to the Unifi Controller.
 | ||||
| 	c.Unifi, err = unifi.NewUnifi(&unifi.Config{ | ||||
| 		User:      c.User, | ||||
| 		Pass:      c.Pass, | ||||
| 		URL:       c.URL, | ||||
| 		VerifySSL: c.VerifySSL, | ||||
| 		ErrorLog:  u.LogErrorf, // Log all errors.
 | ||||
| 		DebugLog:  u.LogDebugf, // Log debug messages.
 | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		c.Unifi = nil | ||||
| 		return fmt.Errorf("unifi controller: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	u.LogDebugf("Authenticated with controller successfully, %s", c.URL) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // checkSites makes sure the list of provided sites exists on the controller.
 | ||||
| // This only runs once during initialization.
 | ||||
| func (u *InputUnifi) checkSites(c *Controller) error { | ||||
| 	u.RLock() | ||||
| 	defer u.RUnlock() | ||||
| 
 | ||||
| 	if len(c.Sites) < 1 || c.Sites[0] == "" { | ||||
| 		c.Sites = []string{"all"} | ||||
| 	} | ||||
| 
 | ||||
| 	u.LogDebugf("Checking Controller Sites List") | ||||
| 
 | ||||
| 	sites, err := c.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 %s: %v", len(msg), c.Role, strings.Join(msg, ", ")) | ||||
| 
 | ||||
| 	if StringInSlice("all", c.Sites) { | ||||
| 		c.Sites = []string{"all"} | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	keep := []string{} | ||||
| 
 | ||||
| FIRST: | ||||
| 	for _, s := range c.Sites { | ||||
| 		for _, site := range sites { | ||||
| 			if s == site.Name { | ||||
| 				keep = append(keep, s) | ||||
| 				continue FIRST | ||||
| 			} | ||||
| 		} | ||||
| 		u.LogErrorf("Configured site not found on controller %s: %v", c.Role, s) | ||||
| 	} | ||||
| 
 | ||||
| 	if c.Sites = keep; len(keep) < 1 { | ||||
| 		c.Sites = []string{"all"} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) dumpSitesJSON(c *Controller, path, name string, sites unifi.Sites) ([]byte, error) { | ||||
| 	allJSON := []byte{} | ||||
| 
 | ||||
| 	for _, s := range sites { | ||||
| 		apiPath := fmt.Sprintf(path, s.Name) | ||||
| 		_, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping %s: '%s' JSON for site: %s (%s):\n", name, apiPath, s.Desc, s.Name) | ||||
| 
 | ||||
| 		body, err := c.Unifi.GetJSON(apiPath) | ||||
| 		if err != nil { | ||||
| 			return allJSON, err | ||||
| 		} | ||||
| 
 | ||||
| 		allJSON = append(allJSON, body...) | ||||
| 	} | ||||
| 
 | ||||
| 	return allJSON, nil | ||||
| } | ||||
| 
 | ||||
| func (u *InputUnifi) setDefaults(c *Controller) { | ||||
| 	if c.SaveSites == nil { | ||||
| 		t := true | ||||
| 		c.SaveSites = &t | ||||
| 	} | ||||
| 
 | ||||
| 	if c.URL == "" { | ||||
| 		c.URL = defaultURL | ||||
| 	} | ||||
| 
 | ||||
| 	if c.Role == "" { | ||||
| 		c.Role = c.URL | ||||
| 	} | ||||
| 
 | ||||
| 	if c.Pass == "" { | ||||
| 		c.Pass = defaultPass | ||||
| 	} | ||||
| 
 | ||||
| 	if c.User == "" { | ||||
| 		c.User = defaultUser | ||||
| 	} | ||||
| 
 | ||||
| 	if len(c.Sites) < 1 { | ||||
| 		c.Sites = []string{defaultSite} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // StringInSlice returns true if a string is in a slice.
 | ||||
| func StringInSlice(str string, slice []string) bool { | ||||
| 	for _, s := range slice { | ||||
| 		if strings.EqualFold(s, str) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return false | ||||
| } | ||||
|  | @ -1,140 +0,0 @@ | |||
| package inputunifi | ||||
| 
 | ||||
| /* This file contains the three poller.Input interface methods. */ | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // Initialize gets called one time when starting up.
 | ||||
| // Satisfies poller.Input interface.
 | ||||
| func (u *InputUnifi) Initialize(l poller.Logger) error { | ||||
| 	if u.Disable { | ||||
| 		l.Logf("UniFi input plugin disabled!") | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	if u.setDefaults(&u.Default); len(u.Controllers) < 1 && !u.Dynamic { | ||||
| 		new := u.Default // copy defaults.
 | ||||
| 		u.Controllers = []*Controller{&new} | ||||
| 	} | ||||
| 
 | ||||
| 	if len(u.Controllers) < 1 { | ||||
| 		l.Logf("No controllers configured. Polling dynamic controllers only!") | ||||
| 	} | ||||
| 
 | ||||
| 	u.dynamic = make(map[string]*Controller) | ||||
| 	u.Logger = l | ||||
| 
 | ||||
| 	for _, c := range u.Controllers { | ||||
| 		u.setDefaults(c) | ||||
| 
 | ||||
| 		switch err := u.getUnifi(c); err { | ||||
| 		case nil: | ||||
| 			if err := u.checkSites(c); err != nil { | ||||
| 				u.LogErrorf("checking sites on %s: %v", c.Role, err) | ||||
| 			} | ||||
| 
 | ||||
| 			u.Logf("Configured UniFi Controller at %s v%s as user %s. Sites: %v", | ||||
| 				c.URL, c.Unifi.ServerVersion, c.User, c.Sites) | ||||
| 		default: | ||||
| 			u.LogErrorf("Controller Auth or Connection failed, but continuing to retry! %s: %v", c.Role, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Metrics grabs all the measurements from a UniFi controller and returns them.
 | ||||
| func (u *InputUnifi) Metrics() (*poller.Metrics, bool, error) { | ||||
| 	return u.MetricsFrom(nil) | ||||
| } | ||||
| 
 | ||||
| // MetricsFrom grabs all the measurements from a UniFi controller and returns them.
 | ||||
| func (u *InputUnifi) MetricsFrom(filter *poller.Filter) (*poller.Metrics, bool, error) { | ||||
| 	if u.Disable { | ||||
| 		return nil, false, nil | ||||
| 	} | ||||
| 
 | ||||
| 	errs := []string{} | ||||
| 	metrics := &poller.Metrics{} | ||||
| 	ok := false | ||||
| 
 | ||||
| 	if filter != nil && filter.Path != "" { | ||||
| 		if !u.Dynamic { | ||||
| 			return metrics, false, fmt.Errorf("filter path requested but dynamic lookups disabled") | ||||
| 		} | ||||
| 
 | ||||
| 		// Attempt a dynamic metrics fetch from an unconfigured controller.
 | ||||
| 		m, err := u.dynamicController(filter.Path) | ||||
| 
 | ||||
| 		return m, err == nil && m != nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// Check if the request is for an existing, configured controller.
 | ||||
| 	for _, c := range u.Controllers { | ||||
| 		if filter != nil && !strings.EqualFold(c.Role, filter.Role) { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		m, err := u.collectController(c) | ||||
| 		if err != nil { | ||||
| 			errs = append(errs, err.Error()) | ||||
| 		} | ||||
| 
 | ||||
| 		if m == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		ok = true | ||||
| 		metrics = poller.AppendMetrics(metrics, m) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(errs) > 0 { | ||||
| 		return metrics, ok, fmt.Errorf(strings.Join(errs, ", ")) | ||||
| 	} | ||||
| 
 | ||||
| 	return metrics, ok, nil | ||||
| } | ||||
| 
 | ||||
| // RawMetrics returns API output from the first configured unifi controller.
 | ||||
| func (u *InputUnifi) RawMetrics(filter *poller.Filter) ([]byte, error) { | ||||
| 	if l := len(u.Controllers); filter.Unit >= l { | ||||
| 		return nil, fmt.Errorf("control number %d not found, %d controller(s) configured (0 index)", filter.Unit, l) | ||||
| 	} | ||||
| 
 | ||||
| 	c := u.Controllers[filter.Unit] | ||||
| 	if u.isNill(c) { | ||||
| 		u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) | ||||
| 
 | ||||
| 		if err := u.getUnifi(c); err != nil { | ||||
| 			return nil, fmt.Errorf("re-authenticating to %s: %v", c.Role, err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if err := u.checkSites(c); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	sites, err := u.getFilteredSites(c) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	switch filter.Kind { | ||||
| 	case "d", "device", "devices": | ||||
| 		return u.dumpSitesJSON(c, unifi.APIDevicePath, "Devices", sites) | ||||
| 	case "client", "clients", "c": | ||||
| 		return u.dumpSitesJSON(c, unifi.APIClientPath, "Clients", sites) | ||||
| 	case "other", "o": | ||||
| 		_, _ = fmt.Fprintf(os.Stderr, "[INFO] Dumping Path '%s':\n", filter.Path) | ||||
| 		return c.Unifi.GetJSON(filter.Path) | ||||
| 	default: | ||||
| 		return []byte{}, fmt.Errorf("must provide filter: devices, clients, other") | ||||
| 	} | ||||
| } | ||||
|  | @ -1,9 +0,0 @@ | |||
| // +build darwin
 | ||||
| 
 | ||||
| package poller | ||||
| 
 | ||||
| // DefaultConfFile is where to find config if --config is not prvided.
 | ||||
| const DefaultConfFile = "/usr/local/etc/unifi-poller/up.conf" | ||||
| 
 | ||||
| // DefaultObjPath is the path to look for shared object libraries (plugins).
 | ||||
| const DefaultObjPath = "/usr/local/lib/unifi-poller" | ||||
|  | @ -1,9 +0,0 @@ | |||
| // +build !windows,!darwin
 | ||||
| 
 | ||||
| package poller | ||||
| 
 | ||||
| // DefaultConfFile is where to find config if --config is not prvided.
 | ||||
| const DefaultConfFile = "/etc/unifi-poller/up.conf" | ||||
| 
 | ||||
| // DefaultObjPath is the path to look for shared object libraries (plugins).
 | ||||
| const DefaultObjPath = "/usr/lib/unifi-poller" | ||||
|  | @ -1,9 +0,0 @@ | |||
| // +build windows
 | ||||
| 
 | ||||
| package poller | ||||
| 
 | ||||
| // DefaultConfFile is where to find config if --config is not prvided.
 | ||||
| const DefaultConfFile = `C:\ProgramData\unifi-poller\up.conf` | ||||
| 
 | ||||
| // DefaultObjPath is useless in this context. Bummer.
 | ||||
| const DefaultObjPath = "PLUGINS_DO_NOT_WORK_ON_WINDOWS_SOWWWWWY" | ||||
|  | @ -1,150 +0,0 @@ | |||
| package poller | ||||
| 
 | ||||
| /* | ||||
| 	I consider this file the pinacle example of how to allow a Go application to be configured from a file. | ||||
| 	You can put your configuration into any file format: XML, YAML, JSON, TOML, and you can override any | ||||
| 	struct member using an environment variable. The Duration type is also supported. All of the Config{} | ||||
| 	and Duration{} types and methods are reusable in other projects. Just adjust the data in the struct to | ||||
| 	meet your app's needs. See the New() procedure and Start() method in start.go for example usage. | ||||
| */ | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"plugin" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/spf13/pflag" | ||||
| 	"golift.io/cnfg" | ||||
| 	"golift.io/cnfg/cnfgfile" | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// AppName is the name of the application.
 | ||||
| 	AppName = "unifi-poller" | ||||
| 	// ENVConfigPrefix is the prefix appended to an env variable tag name.
 | ||||
| 	ENVConfigPrefix = "UP" | ||||
| ) | ||||
| 
 | ||||
| // UnifiPoller contains the application startup data, and auth info for UniFi & Influx.
 | ||||
| type UnifiPoller struct { | ||||
| 	Flags *Flags | ||||
| 	*Config | ||||
| } | ||||
| 
 | ||||
| // Flags represents the CLI args available and their settings.
 | ||||
| type Flags struct { | ||||
| 	ConfigFile string | ||||
| 	DumpJSON   string | ||||
| 	ShowVer    bool | ||||
| 	*pflag.FlagSet | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| 	SitesDPI   []*unifi.DPITable | ||||
| 	ClientsDPI []*unifi.DPITable | ||||
| } | ||||
| 
 | ||||
| // Config represents the core library input data.
 | ||||
| type Config struct { | ||||
| 	*Poller `json:"poller" toml:"poller" xml:"poller" yaml:"poller"` | ||||
| } | ||||
| 
 | ||||
| // Poller is the global config values.
 | ||||
| type Poller struct { | ||||
| 	Plugins []string `json:"plugins" toml:"plugins" xml:"plugin" yaml:"plugins"` | ||||
| 	Debug   bool     `json:"debug" toml:"debug" xml:"debug,attr" yaml:"debug"` | ||||
| 	Quiet   bool     `json:"quiet,omitempty" toml:"quiet,omitempty" xml:"quiet,attr" yaml:"quiet"` | ||||
| } | ||||
| 
 | ||||
| // LoadPlugins reads-in dynamic shared libraries.
 | ||||
| // Not used very often, if at all.
 | ||||
| func (u *UnifiPoller) LoadPlugins() error { | ||||
| 	for _, p := range u.Plugins { | ||||
| 		name := strings.TrimSuffix(p, ".so") + ".so" | ||||
| 
 | ||||
| 		if name == ".so" { | ||||
| 			continue // Just ignore it. uhg.
 | ||||
| 		} | ||||
| 
 | ||||
| 		if _, err := os.Stat(name); os.IsNotExist(err) { | ||||
| 			name = path.Join(DefaultObjPath, name) | ||||
| 		} | ||||
| 
 | ||||
| 		u.Logf("Loading Dynamic Plugin: %s", name) | ||||
| 
 | ||||
| 		if _, err := plugin.Open(name); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // ParseConfigs parses the poller config and the config for each registered output plugin.
 | ||||
| func (u *UnifiPoller) ParseConfigs() error { | ||||
| 	// Parse core config.
 | ||||
| 	if err := u.parseInterface(u.Config); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Load dynamic plugins.
 | ||||
| 	if err := u.LoadPlugins(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if err := u.parseInputs(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return u.parseOutputs() | ||||
| } | ||||
| 
 | ||||
| // parseInterface parses the config file and environment variables into the provided interface.
 | ||||
| func (u *UnifiPoller) parseInterface(i interface{}) error { | ||||
| 	// Parse config file into provided interface.
 | ||||
| 	if err := cnfgfile.Unmarshal(i, u.Flags.ConfigFile); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Parse environment variables into provided interface.
 | ||||
| 	_, err := cnfg.UnmarshalENV(i, ENVConfigPrefix) | ||||
| 
 | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| // Parse input plugin configs.
 | ||||
| func (u *UnifiPoller) parseInputs() error { | ||||
| 	inputSync.Lock() | ||||
| 	defer inputSync.Unlock() | ||||
| 
 | ||||
| 	for _, i := range inputs { | ||||
| 		if err := u.parseInterface(i.Config); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Parse output plugin configs.
 | ||||
| func (u *UnifiPoller) parseOutputs() error { | ||||
| 	outputSync.Lock() | ||||
| 	defer outputSync.Unlock() | ||||
| 
 | ||||
| 	for _, o := range outputs { | ||||
| 		if err := u.parseInterface(o.Config); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,33 +0,0 @@ | |||
| package poller | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| // DumpJSONPayload prints raw json from the UniFi Controller. This is currently
 | ||||
| // tied into the -j CLI arg, and is probably not very useful outside that context.
 | ||||
| func (u *UnifiPoller) DumpJSONPayload() (err error) { | ||||
| 	u.Config.Quiet = true | ||||
| 	split := strings.SplitN(u.Flags.DumpJSON, " ", 2) | ||||
| 	filter := &Filter{Kind: split[0]} | ||||
| 
 | ||||
| 	if split2 := strings.Split(filter.Kind, ":"); len(split2) > 1 { | ||||
| 		filter.Kind = split2[0] | ||||
| 		filter.Unit, _ = strconv.Atoi(split2[1]) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(split) > 1 { | ||||
| 		filter.Path = split[1] | ||||
| 	} | ||||
| 
 | ||||
| 	m, err := inputs[0].RawMetrics(filter) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	fmt.Println(string(m)) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,165 +0,0 @@ | |||
| package poller | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	inputs    []*InputPlugin | ||||
| 	inputSync sync.Mutex | ||||
| ) | ||||
| 
 | ||||
| // Input plugins must implement this interface.
 | ||||
| type Input interface { | ||||
| 	Initialize(Logger) error                     // Called once on startup to initialize the plugin.
 | ||||
| 	Metrics() (*Metrics, bool, error)            // Called every time new metrics are requested.
 | ||||
| 	MetricsFrom(*Filter) (*Metrics, bool, error) // Called every time new metrics are requested.
 | ||||
| 	RawMetrics(*Filter) ([]byte, error) | ||||
| } | ||||
| 
 | ||||
| // InputPlugin describes an input plugin's consumable interface.
 | ||||
| type InputPlugin struct { | ||||
| 	Name   string | ||||
| 	Config interface{} // Each config is passed into an unmarshaller later.
 | ||||
| 	Input | ||||
| } | ||||
| 
 | ||||
| // Filter is used for metrics filters. Many fields for lots of expansion.
 | ||||
| type Filter struct { | ||||
| 	Type string | ||||
| 	Term string | ||||
| 	Name string | ||||
| 	Tags string | ||||
| 	Role string | ||||
| 	Kind string | ||||
| 	Path string | ||||
| 	Area int | ||||
| 	Item int | ||||
| 	Unit int | ||||
| 	Sign int64 | ||||
| 	Mass int64 | ||||
| 	Rate float64 | ||||
| 	Cost float64 | ||||
| 	Free bool | ||||
| 	True bool | ||||
| 	Done bool | ||||
| 	Stop bool | ||||
| } | ||||
| 
 | ||||
| // NewInput creates a metric input. This should be called by input plugins
 | ||||
| // init() functions.
 | ||||
| func NewInput(i *InputPlugin) { | ||||
| 	inputSync.Lock() | ||||
| 	defer inputSync.Unlock() | ||||
| 
 | ||||
| 	if i == nil || i.Input == nil { | ||||
| 		panic("nil output or method passed to poller.NewOutput") | ||||
| 	} | ||||
| 
 | ||||
| 	inputs = append(inputs, i) | ||||
| } | ||||
| 
 | ||||
| // InitializeInputs runs the passed-in initializer method for each input plugin.
 | ||||
| func (u *UnifiPoller) InitializeInputs() error { | ||||
| 	inputSync.Lock() | ||||
| 	defer inputSync.Unlock() | ||||
| 
 | ||||
| 	for _, input := range inputs { | ||||
| 		// This must return, or the app locks up here.
 | ||||
| 		if err := input.Initialize(u); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Metrics aggregates all the measurements from all configured inputs and returns them.
 | ||||
| func (u *UnifiPoller) Metrics() (*Metrics, bool, error) { | ||||
| 	errs := []string{} | ||||
| 	metrics := &Metrics{} | ||||
| 	ok := false | ||||
| 
 | ||||
| 	for _, input := range inputs { | ||||
| 		m, _, err := input.Metrics() | ||||
| 		if err != nil { | ||||
| 			errs = append(errs, err.Error()) | ||||
| 		} | ||||
| 
 | ||||
| 		if m == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		ok = true | ||||
| 		metrics = AppendMetrics(metrics, m) | ||||
| 	} | ||||
| 
 | ||||
| 	var err error | ||||
| 
 | ||||
| 	if len(errs) > 0 { | ||||
| 		err = fmt.Errorf(strings.Join(errs, ", ")) | ||||
| 	} | ||||
| 
 | ||||
| 	return metrics, ok, err | ||||
| } | ||||
| 
 | ||||
| // MetricsFrom aggregates all the measurements from filtered inputs and returns them.
 | ||||
| func (u *UnifiPoller) MetricsFrom(filter *Filter) (*Metrics, bool, error) { | ||||
| 	errs := []string{} | ||||
| 	metrics := &Metrics{} | ||||
| 	ok := false | ||||
| 
 | ||||
| 	for _, input := range inputs { | ||||
| 		if !strings.EqualFold(input.Name, filter.Name) { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		m, _, err := input.MetricsFrom(filter) | ||||
| 		if err != nil { | ||||
| 			errs = append(errs, err.Error()) | ||||
| 		} | ||||
| 
 | ||||
| 		if m == nil { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		ok = true | ||||
| 		metrics = AppendMetrics(metrics, m) | ||||
| 	} | ||||
| 
 | ||||
| 	var err error | ||||
| 
 | ||||
| 	if len(errs) > 0 { | ||||
| 		err = fmt.Errorf(strings.Join(errs, ", ")) | ||||
| 	} | ||||
| 
 | ||||
| 	return metrics, ok, err | ||||
| } | ||||
| 
 | ||||
| // AppendMetrics combined the metrics from two sources.
 | ||||
| func AppendMetrics(existing *Metrics, m *Metrics) *Metrics { | ||||
| 	existing.SitesDPI = append(existing.SitesDPI, m.SitesDPI...) | ||||
| 	existing.Sites = append(existing.Sites, m.Sites...) | ||||
| 	existing.ClientsDPI = append(existing.ClientsDPI, m.ClientsDPI...) | ||||
| 	existing.Clients = append(existing.Clients, m.Clients...) | ||||
| 	existing.IDSList = append(existing.IDSList, m.IDSList...) | ||||
| 
 | ||||
| 	if m.Devices == nil { | ||||
| 		return existing | ||||
| 	} | ||||
| 
 | ||||
| 	if existing.Devices == nil { | ||||
| 		existing.Devices = &unifi.Devices{} | ||||
| 	} | ||||
| 
 | ||||
| 	existing.UAPs = append(existing.UAPs, m.UAPs...) | ||||
| 	existing.USGs = append(existing.USGs, m.USGs...) | ||||
| 	existing.USWs = append(existing.USWs, m.USWs...) | ||||
| 	existing.UDMs = append(existing.UDMs, m.UDMs...) | ||||
| 
 | ||||
| 	return existing | ||||
| } | ||||
|  | @ -1,34 +0,0 @@ | |||
| package poller | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| ) | ||||
| 
 | ||||
| const callDepth = 2 | ||||
| 
 | ||||
| // Logger is passed into input packages so they may write logs.
 | ||||
| type Logger interface { | ||||
| 	Logf(m string, v ...interface{}) | ||||
| 	LogErrorf(m string, v ...interface{}) | ||||
| 	LogDebugf(m string, v ...interface{}) | ||||
| } | ||||
| 
 | ||||
| // Logf prints a log entry if quiet is false.
 | ||||
| func (u *UnifiPoller) Logf(m string, v ...interface{}) { | ||||
| 	if !u.Quiet { | ||||
| 		_ = 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.Debug && !u.Quiet { | ||||
| 		_ = log.Output(callDepth, fmt.Sprintf("[DEBUG] "+m, v...)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // LogErrorf prints an error log entry.
 | ||||
| func (u *UnifiPoller) LogErrorf(m string, v ...interface{}) { | ||||
| 	_ = log.Output(callDepth, fmt.Sprintf("[ERROR] "+m, v...)) | ||||
| } | ||||
|  | @ -1,72 +0,0 @@ | |||
| package poller | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	outputs    []*Output | ||||
| 	outputSync sync.Mutex | ||||
| ) | ||||
| 
 | ||||
| // Collect is passed into output packages so they may collect metrics to output.
 | ||||
| // Output packages must implement this interface.
 | ||||
| type Collect interface { | ||||
| 	Metrics() (*Metrics, bool, error) | ||||
| 	MetricsFrom(*Filter) (*Metrics, bool, error) | ||||
| 	Logger | ||||
| } | ||||
| 
 | ||||
| // Output defines the output data for a metric exporter like influx or prometheus.
 | ||||
| // Output packages should call NewOutput with this struct in init().
 | ||||
| type Output struct { | ||||
| 	Name   string | ||||
| 	Config interface{}         // Each config is passed into an unmarshaller later.
 | ||||
| 	Method func(Collect) error // Called on startup for each configured output.
 | ||||
| } | ||||
| 
 | ||||
| // NewOutput should be called by each output package's init function.
 | ||||
| func NewOutput(o *Output) { | ||||
| 	outputSync.Lock() | ||||
| 	defer outputSync.Unlock() | ||||
| 
 | ||||
| 	if o == nil || o.Method == nil { | ||||
| 		panic("nil output or method passed to poller.NewOutput") | ||||
| 	} | ||||
| 
 | ||||
| 	outputs = append(outputs, o) | ||||
| } | ||||
| 
 | ||||
| // InitializeOutputs runs all the configured output plugins.
 | ||||
| // If none exist, or they all exit an error is returned.
 | ||||
| func (u *UnifiPoller) InitializeOutputs() error { | ||||
| 	v := make(chan error) | ||||
| 	defer close(v) | ||||
| 
 | ||||
| 	var count int | ||||
| 
 | ||||
| 	for _, o := range outputs { | ||||
| 		count++ | ||||
| 
 | ||||
| 		go func(o *Output) { | ||||
| 			v <- o.Method(u) | ||||
| 		}(o) | ||||
| 	} | ||||
| 
 | ||||
| 	if count < 1 { | ||||
| 		return fmt.Errorf("no output plugins imported") | ||||
| 	} | ||||
| 
 | ||||
| 	for err := range v { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		if count--; count < 1 { | ||||
| 			return fmt.Errorf("all output plugins have stopped, or none enabled") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,83 +0,0 @@ | |||
| // Package poller provides the CLI interface to setup unifi-poller.
 | ||||
| package poller | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 
 | ||||
| 	"github.com/prometheus/common/version" | ||||
| 	"github.com/spf13/pflag" | ||||
| ) | ||||
| 
 | ||||
| // New returns a new poller struct.
 | ||||
| func New() *UnifiPoller { | ||||
| 	return &UnifiPoller{Config: &Config{Poller: &Poller{}}, Flags: &Flags{}} | ||||
| } | ||||
| 
 | ||||
| // 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.SetOutput(os.Stdout) | ||||
| 	log.SetFlags(log.LstdFlags) | ||||
| 	u.Flags.Parse(os.Args[1:]) | ||||
| 
 | ||||
| 	if u.Flags.ShowVer { | ||||
| 		fmt.Printf("%s v%s\n", AppName, version.Version) | ||||
| 		return nil // don't run anything else w/ version request.
 | ||||
| 	} | ||||
| 
 | ||||
| 	if u.Flags.DumpJSON == "" { // do not print this when dumping JSON.
 | ||||
| 		u.Logf("Loading Configuration File: %s", u.Flags.ConfigFile) | ||||
| 	} | ||||
| 
 | ||||
| 	// Parse config file and ENV variables.
 | ||||
| 	if err := u.ParseConfigs(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return u.Run() | ||||
| } | ||||
| 
 | ||||
| // Parse turns CLI arguments into data structures. Called by Start() on startup.
 | ||||
| func (f *Flags) 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 u.Flags.DumpJSON != "" { | ||||
| 		if err := u.InitializeInputs(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		return u.DumpJSONPayload() | ||||
| 	} | ||||
| 
 | ||||
| 	if u.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.Version, os.Getpid()) | ||||
| 
 | ||||
| 	if err := u.InitializeInputs(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return u.InitializeOutputs() | ||||
| } | ||||
|  | @ -1,4 +0,0 @@ | |||
| # prometheus | ||||
| 
 | ||||
| This package provides the interface to turn UniFi measurements into prometheus | ||||
| exported metrics. Requires the poller package for actual UniFi data collection. | ||||
|  | @ -1,136 +0,0 @@ | |||
| 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 | ||||
| 	DPITxPackets   *prometheus.Desc | ||||
| 	DPIRxPackets   *prometheus.Desc | ||||
| 	DPITxBytes     *prometheus.Desc | ||||
| 	DPIRxBytes     *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", "source", "wired"} | ||||
| 	labelW := append([]string{"radio_name", "radio", "radio_proto", "channel", "essid", "bssid", "radio_desc"}, labels...) | ||||
| 	labelDPI := []string{"name", "mac", "site_name", "source", "category", "application"} | ||||
| 
 | ||||
| 	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_ratio", "Client Connection Quality", labelW, nil), | ||||
| 		Satisfaction:   prometheus.NewDesc(ns+"satisfaction_ratio", "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), | ||||
| 		DPITxPackets:   prometheus.NewDesc(ns+"dpi_transmit_packets", "Client DPI Transmit Packets", labelDPI, nil), | ||||
| 		DPIRxPackets:   prometheus.NewDesc(ns+"dpi_receive_packets", "Client DPI Receive Packets", labelDPI, nil), | ||||
| 		DPITxBytes:     prometheus.NewDesc(ns+"dpi_transmit_bytes", "Client DPI Transmit Bytes", labelDPI, nil), | ||||
| 		DPIRxBytes:     prometheus.NewDesc(ns+"dpi_receive_bytes", "Client DPI Receive Bytes", labelDPI, nil), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *promUnifi) exportClientDPI(r report, s *unifi.DPITable) { | ||||
| 	for _, dpi := range s.ByApp { | ||||
| 		labelDPI := []string{s.Name, s.MAC, s.SiteName, s.SourceName, | ||||
| 			unifi.DPICats.Get(dpi.Cat), unifi.DPIApps.GetApp(dpi.Cat, dpi.App)} | ||||
| 
 | ||||
| 		// log.Println(labelDPI, dpi.Cat, dpi.App, dpi.TxBytes, dpi.RxBytes, dpi.TxPackets, dpi.RxPackets)
 | ||||
| 		r.send([]*metric{ | ||||
| 			{u.Client.DPITxPackets, gauge, dpi.TxPackets, labelDPI}, | ||||
| 			{u.Client.DPIRxPackets, gauge, dpi.RxPackets, labelDPI}, | ||||
| 			{u.Client.DPITxBytes, gauge, dpi.TxBytes, labelDPI}, | ||||
| 			{u.Client.DPIRxBytes, gauge, dpi.RxBytes, labelDPI}, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| 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, c.SourceName, ""} | ||||
| 	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, counter, c.WiredRxBytes, labels}, | ||||
| 			{u.Client.RxBytesR, gauge, c.WiredRxBytesR, labels}, | ||||
| 			{u.Client.RxPackets, counter, c.WiredRxPackets, labels}, | ||||
| 			{u.Client.TxBytes, counter, c.WiredTxBytes, labels}, | ||||
| 			{u.Client.TxBytesR, gauge, c.WiredTxBytesR, labels}, | ||||
| 			{u.Client.TxPackets, counter, c.WiredTxPackets, labels}, | ||||
| 		}) | ||||
| 	} else { | ||||
| 		labels[len(labels)-1] = "false" | ||||
| 		labelW[len(labelW)-1] = "false" | ||||
| 
 | ||||
| 		r.send([]*metric{ | ||||
| 			{u.Client.Anomalies, counter, c.Anomalies, labelW}, | ||||
| 			{u.Client.CCQ, gauge, float64(c.Ccq) / 1000.0, labelW}, | ||||
| 			{u.Client.Satisfaction, gauge, c.Satisfaction.Val / 100.0, labelW}, | ||||
| 			{u.Client.Noise, gauge, c.Noise, labelW}, | ||||
| 			{u.Client.RoamCount, counter, c.RoamCount, labelW}, | ||||
| 			{u.Client.RSSI, gauge, c.Rssi, labelW}, | ||||
| 			{u.Client.Signal, gauge, c.Signal, labelW}, | ||||
| 			{u.Client.TxPower, gauge, c.TxPower, labelW}, | ||||
| 			{u.Client.TxRate, gauge, c.TxRate * 1000, labelW}, | ||||
| 			{u.Client.WifiTxAttempts, counter, c.WifiTxAttempts, labelW}, | ||||
| 			{u.Client.RxRate, gauge, c.RxRate * 1000, labelW}, | ||||
| 			{u.Client.TxRetries, counter, c.TxRetries, labels}, | ||||
| 			{u.Client.TxBytes, counter, c.TxBytes, labels}, | ||||
| 			{u.Client.TxBytesR, gauge, c.TxBytesR, labels}, | ||||
| 			{u.Client.TxPackets, counter, c.TxPackets, labels}, | ||||
| 			{u.Client.RxBytes, counter, c.RxBytes, labels}, | ||||
| 			{u.Client.RxBytesR, gauge, c.RxBytesR, labels}, | ||||
| 			{u.Client.RxPackets, counter, c.RxPackets, labels}, | ||||
| 			{u.Client.BytesR, gauge, c.BytesR, labelW}, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	r.send([]*metric{{u.Client.Uptime, gauge, c.Uptime, labelW}}) | ||||
| } | ||||
|  | @ -1,346 +0,0 @@ | |||
| // Package promunifi provides the bridge between unifi-poller metrics and prometheus.
 | ||||
| package promunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"reflect" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	"github.com/prometheus/client_golang/prometheus" | ||||
| 	"github.com/prometheus/client_golang/prometheus/promhttp" | ||||
| 	"github.com/prometheus/common/version" | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// channel buffer, fits at least one batch.
 | ||||
| 	defaultBuffer     = 50 | ||||
| 	defaultHTTPListen = "0.0.0.0:9130" | ||||
| 	// simply fewer letters.
 | ||||
| 	counter = prometheus.CounterValue | ||||
| 	gauge   = prometheus.GaugeValue | ||||
| ) | ||||
| 
 | ||||
| type promUnifi struct { | ||||
| 	*Config `json:"prometheus" toml:"prometheus" xml:"prometheus" yaml:"prometheus"` | ||||
| 	Client  *uclient | ||||
| 	Device  *unifiDevice | ||||
| 	UAP     *uap | ||||
| 	USG     *usg | ||||
| 	USW     *usw | ||||
| 	Site    *site | ||||
| 	// This interface is passed to the Collect() method. The Collect method uses
 | ||||
| 	// this interface to retrieve the latest UniFi measurements and export them.
 | ||||
| 	Collector poller.Collect | ||||
| } | ||||
| 
 | ||||
| // Config is the input (config file) data used to initialize this output plugin.
 | ||||
| type Config struct { | ||||
| 	// If non-empty, each of the collected metrics is prefixed by the
 | ||||
| 	// provided string and an underscore ("_").
 | ||||
| 	Namespace  string `json:"namespace" toml:"namespace" xml:"namespace" yaml:"namespace"` | ||||
| 	HTTPListen string `json:"http_listen" toml:"http_listen" xml:"http_listen" yaml:"http_listen"` | ||||
| 	// If true, any error encountered during collection is reported as an
 | ||||
| 	// invalid metric (see NewInvalidMetric). Otherwise, errors are ignored
 | ||||
| 	// and the collected metrics will be incomplete. Possibly, no metrics
 | ||||
| 	// will be collected at all.
 | ||||
| 	ReportErrors bool `json:"report_errors" toml:"report_errors" xml:"report_errors" yaml:"report_errors"` | ||||
| 	Disable      bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"` | ||||
| 	// Buffer is a channel buffer.
 | ||||
| 	// Default is probably 50. Seems fast there; try 1 to see if CPU usage goes down?
 | ||||
| 	Buffer int `json:"buffer" toml:"buffer" xml:"buffer" yaml:"buffer"` | ||||
| } | ||||
| 
 | ||||
| type metric struct { | ||||
| 	Desc      *prometheus.Desc | ||||
| 	ValueType prometheus.ValueType | ||||
| 	Value     interface{} | ||||
| 	Labels    []string | ||||
| } | ||||
| 
 | ||||
| // Report accumulates counters that are printed to a log line.
 | ||||
| type Report struct { | ||||
| 	*Config | ||||
| 	Total   int             // Total count of metrics recorded.
 | ||||
| 	Errors  int             // Total count of errors recording metrics.
 | ||||
| 	Zeros   int             // Total count of metrics equal to zero.
 | ||||
| 	Metrics *poller.Metrics // Metrics collected and recorded.
 | ||||
| 	Elapsed time.Duration   // Duration elapsed collecting and exporting.
 | ||||
| 	Fetch   time.Duration   // Duration elapsed making controller requests.
 | ||||
| 	Start   time.Time       // Time collection began.
 | ||||
| 	ch      chan []*metric | ||||
| 	wg      sync.WaitGroup | ||||
| } | ||||
| 
 | ||||
| // target is used for targeted (sometimes dynamic) metrics scrapes.
 | ||||
| type target struct { | ||||
| 	*poller.Filter | ||||
| 	u *promUnifi | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	u := &promUnifi{Config: &Config{}} | ||||
| 
 | ||||
| 	poller.NewOutput(&poller.Output{ | ||||
| 		Name:   "prometheus", | ||||
| 		Config: u, | ||||
| 		Method: u.Run, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // Run creates the collectors and starts the web server up.
 | ||||
| // Should be run in a Go routine. Returns nil if not configured.
 | ||||
| func (u *promUnifi) Run(c poller.Collect) error { | ||||
| 	if u.Disable { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	u.Namespace = strings.Trim(strings.Replace(u.Namespace, "-", "_", -1), "_") | ||||
| 	if u.Namespace == "" { | ||||
| 		u.Namespace = strings.Replace(poller.AppName, "-", "", -1) | ||||
| 	} | ||||
| 
 | ||||
| 	if u.HTTPListen == "" { | ||||
| 		u.HTTPListen = defaultHTTPListen | ||||
| 	} | ||||
| 
 | ||||
| 	if u.Buffer == 0 { | ||||
| 		u.Buffer = defaultBuffer | ||||
| 	} | ||||
| 
 | ||||
| 	// Later can pass this in from poller by adding a method to the interface.
 | ||||
| 	u.Collector = c | ||||
| 	u.Client = descClient(u.Namespace + "_client_") | ||||
| 	u.Device = descDevice(u.Namespace + "_device_") // stats for all device types.
 | ||||
| 	u.UAP = descUAP(u.Namespace + "_device_") | ||||
| 	u.USG = descUSG(u.Namespace + "_device_") | ||||
| 	u.USW = descUSW(u.Namespace + "_device_") | ||||
| 	u.Site = descSite(u.Namespace + "_site_") | ||||
| 	mux := http.NewServeMux() | ||||
| 
 | ||||
| 	prometheus.MustRegister(version.NewCollector(u.Namespace)) | ||||
| 	prometheus.MustRegister(u) | ||||
| 	c.Logf("Prometheus exported at https://%s/ - namespace: %s", u.HTTPListen, u.Namespace) | ||||
| 	mux.Handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, | ||||
| 		promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}, | ||||
| 	)) | ||||
| 	mux.HandleFunc("/scrape", u.ScrapeHandler) | ||||
| 	mux.HandleFunc("/", u.DefaultHandler) | ||||
| 
 | ||||
| 	return http.ListenAndServe(u.HTTPListen, mux) | ||||
| } | ||||
| 
 | ||||
| // ScrapeHandler allows prometheus to scrape a single source, instead of all sources.
 | ||||
| func (u *promUnifi) ScrapeHandler(w http.ResponseWriter, r *http.Request) { | ||||
| 	t := &target{u: u, Filter: &poller.Filter{ | ||||
| 		Name: r.URL.Query().Get("input"), // "unifi"
 | ||||
| 		Path: r.URL.Query().Get("path"),  // url: "https://127.0.0.1:8443"
 | ||||
| 		Role: r.URL.Query().Get("role"),  // configured role in up.conf.
 | ||||
| 	}} | ||||
| 
 | ||||
| 	if t.Name == "" { | ||||
| 		u.Collector.LogErrorf("input parameter missing on scrape from %v", r.RemoteAddr) | ||||
| 		http.Error(w, `'input' parameter must be specified (try "unifi")`, 400) | ||||
| 
 | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if t.Role == "" && t.Path == "" { | ||||
| 		u.Collector.LogErrorf("role and path parameters missing on scrape from %v", r.RemoteAddr) | ||||
| 		http.Error(w, "'role' OR 'path' parameter must be specified: configured role OR unconfigured url", 400) | ||||
| 
 | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	registry := prometheus.NewRegistry() | ||||
| 
 | ||||
| 	registry.MustRegister(t) | ||||
| 	promhttp.HandlerFor( | ||||
| 		registry, promhttp.HandlerOpts{ErrorHandling: promhttp.ContinueOnError}, | ||||
| 	).ServeHTTP(w, r) | ||||
| } | ||||
| 
 | ||||
| func (u *promUnifi) DefaultHandler(w http.ResponseWriter, r *http.Request) { | ||||
| 	w.WriteHeader(200) | ||||
| 	_, _ = w.Write([]byte(poller.AppName + "\n")) | ||||
| } | ||||
| 
 | ||||
| // Describe satisfies the prometheus Collector. This returns all of the
 | ||||
| // metric descriptions that this packages produces.
 | ||||
| func (t *target) Describe(ch chan<- *prometheus.Desc) { | ||||
| 	t.u.Describe(ch) | ||||
| } | ||||
| 
 | ||||
| // Describe satisfies the prometheus Collector. This returns all of the
 | ||||
| // metric descriptions that this packages produces.
 | ||||
| func (u *promUnifi) Describe(ch chan<- *prometheus.Desc) { | ||||
| 	for _, f := range []interface{}{u.Client, u.Device, u.UAP, u.USG, u.USW, u.Site} { | ||||
| 		v := reflect.Indirect(reflect.ValueOf(f)) | ||||
| 
 | ||||
| 		// Loop each struct member and send it to the provided channel.
 | ||||
| 		for i := 0; i < v.NumField(); i++ { | ||||
| 			desc, ok := v.Field(i).Interface().(*prometheus.Desc) | ||||
| 			if ok && desc != nil { | ||||
| 				ch <- desc | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Collect satisfies the prometheus Collector. This runs for a single controller poll.
 | ||||
| func (t *target) Collect(ch chan<- prometheus.Metric) { | ||||
| 	t.u.collect(ch, t.Filter) | ||||
| } | ||||
| 
 | ||||
| // Collect satisfies the prometheus Collector. This runs the input method to get
 | ||||
| // the current metrics (from another package) then exports them for prometheus.
 | ||||
| func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { | ||||
| 	u.collect(ch, nil) | ||||
| } | ||||
| 
 | ||||
| func (u *promUnifi) collect(ch chan<- prometheus.Metric, filter *poller.Filter) { | ||||
| 	var err error | ||||
| 
 | ||||
| 	r := &Report{ | ||||
| 		Config: u.Config, | ||||
| 		ch:     make(chan []*metric, u.Config.Buffer), | ||||
| 		Start:  time.Now()} | ||||
| 	defer r.close() | ||||
| 
 | ||||
| 	ok := false | ||||
| 
 | ||||
| 	if filter == nil { | ||||
| 		r.Metrics, ok, err = u.Collector.Metrics() | ||||
| 	} else { | ||||
| 		r.Metrics, ok, err = u.Collector.MetricsFrom(filter) | ||||
| 	} | ||||
| 
 | ||||
| 	r.Fetch = time.Since(r.Start) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		r.error(ch, prometheus.NewInvalidDesc(err), fmt.Errorf("metric fetch failed")) | ||||
| 		u.Collector.LogErrorf("metric fetch failed: %v", err) | ||||
| 
 | ||||
| 		if !ok { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if r.Metrics.Devices == nil { | ||||
| 		r.Metrics.Devices = &unifi.Devices{} | ||||
| 	} | ||||
| 
 | ||||
| 	// Pass Report interface into our collecting and reporting methods.
 | ||||
| 	go u.exportMetrics(r, ch, r.ch) | ||||
| 	u.loopExports(r) | ||||
| } | ||||
| 
 | ||||
| // This is closely tied to the method above with a sync.WaitGroup.
 | ||||
| // This method runs in a go routine and exits when the channel closes.
 | ||||
| // This is where our channels connects to the prometheus channel.
 | ||||
| func (u *promUnifi) exportMetrics(r report, ch chan<- prometheus.Metric, ourChan chan []*metric) { | ||||
| 	descs := make(map[*prometheus.Desc]bool) // used as a counter
 | ||||
| 	defer r.report(u.Collector, descs) | ||||
| 
 | ||||
| 	for newMetrics := range ourChan { | ||||
| 		for _, m := range newMetrics { | ||||
| 			descs[m.Desc] = true | ||||
| 
 | ||||
| 			switch v := m.Value.(type) { | ||||
| 			case unifi.FlexInt: | ||||
| 				ch <- r.export(m, v.Val) | ||||
| 			case float64: | ||||
| 				ch <- r.export(m, v) | ||||
| 			case int64: | ||||
| 				ch <- r.export(m, float64(v)) | ||||
| 			case int: | ||||
| 				ch <- r.export(m, float64(v)) | ||||
| 			default: | ||||
| 				r.error(ch, m.Desc, fmt.Sprintf("not a number: %v", m.Value)) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		r.done() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *promUnifi) loopExports(r report) { | ||||
| 	m := r.metrics() | ||||
| 
 | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 	r.add() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.Sites { | ||||
| 			u.exportSite(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, s := range m.SitesDPI { | ||||
| 			u.exportSiteDPI(r, s) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, c := range m.Clients { | ||||
| 			u.exportClient(r, c) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, c := range m.ClientsDPI { | ||||
| 			u.exportClientDPI(r, c) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, d := range m.UAPs { | ||||
| 			u.exportUAP(r, d) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, d := range m.UDMs { | ||||
| 			u.exportUDM(r, d) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, d := range m.USGs { | ||||
| 			u.exportUSG(r, d) | ||||
| 		} | ||||
| 	}() | ||||
| 
 | ||||
| 	go func() { | ||||
| 		defer r.done() | ||||
| 
 | ||||
| 		for _, d := range m.USWs { | ||||
| 			u.exportUSW(r, d) | ||||
| 		} | ||||
| 	}() | ||||
| } | ||||
|  | @ -1,80 +0,0 @@ | |||
| package promunifi | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	"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() *poller.Metrics | ||||
| 	report(c poller.Collect, 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 | ||||
| const oneDecimalPoint = 10.0 | ||||
| 
 | ||||
| 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() *poller.Metrics { | ||||
| 	return r.Metrics | ||||
| } | ||||
| 
 | ||||
| func (r *Report) report(c poller.Collect, descs map[*prometheus.Desc]bool) { | ||||
| 	m := r.Metrics | ||||
| 	c.Logf("UniFi Measurements Exported. Site: %d, Client: %d, "+ | ||||
| 		"UAP: %d, USG/UDM: %d, USW: %d, 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), | ||||
| 		len(descs), r.Total, r.Errors, r.Zeros, | ||||
| 		r.Fetch.Round(time.Millisecond/oneDecimalPoint), | ||||
| 		r.Elapsed.Round(time.Millisecond/oneDecimalPoint)) | ||||
| } | ||||
| 
 | ||||
| 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.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) | ||||
| } | ||||
|  | @ -1,152 +0,0 @@ | |||
| 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 | ||||
| 	DPITxPackets          *prometheus.Desc | ||||
| 	DPIRxPackets          *prometheus.Desc | ||||
| 	DPITxBytes            *prometheus.Desc | ||||
| 	DPIRxBytes            *prometheus.Desc | ||||
| } | ||||
| 
 | ||||
| func descSite(ns string) *site { | ||||
| 	labels := []string{"subsystem", "status", "site_name", "source"} | ||||
| 	labelDPI := []string{"category", "application", "site_name", "source"} | ||||
| 	nd := prometheus.NewDesc | ||||
| 
 | ||||
| 	return &site{ | ||||
| 		NumUser:               nd(ns+"users", "Number of Users", labels, nil), | ||||
| 		NumGuest:              nd(ns+"guests", "Number of Guests", labels, nil), | ||||
| 		NumIot:                nd(ns+"iots", "Number of IoT Devices", labels, nil), | ||||
| 		TxBytesR:              nd(ns+"transmit_rate_bytes", "Bytes Transmit Rate", labels, nil), | ||||
| 		RxBytesR:              nd(ns+"receive_rate_bytes", "Bytes Receive Rate", labels, nil), | ||||
| 		NumAp:                 nd(ns+"aps", "Access Point Count", labels, nil), | ||||
| 		NumAdopted:            nd(ns+"adopted", "Adoption Count", labels, nil), | ||||
| 		NumDisabled:           nd(ns+"disabled", "Disabled Count", labels, nil), | ||||
| 		NumDisconnected:       nd(ns+"disconnected", "Disconnected Count", labels, nil), | ||||
| 		NumPending:            nd(ns+"pending", "Pending Count", labels, nil), | ||||
| 		NumGw:                 nd(ns+"gateways", "Gateway Count", labels, nil), | ||||
| 		NumSw:                 nd(ns+"switches", "Switch Count", labels, nil), | ||||
| 		NumSta:                nd(ns+"stations", "Station Count", labels, nil), | ||||
| 		Latency:               nd(ns+"latency_seconds", "Latency", labels, nil), | ||||
| 		Uptime:                nd(ns+"uptime_seconds", "Uptime", labels, nil), | ||||
| 		Drops:                 nd(ns+"intenet_drops_total", "Internet (WAN) Disconnections", labels, nil), | ||||
| 		XputUp:                nd(ns+"xput_up_rate", "Speedtest Upload", labels, nil), | ||||
| 		XputDown:              nd(ns+"xput_down_rate", "Speedtest Download", labels, nil), | ||||
| 		SpeedtestPing:         nd(ns+"speedtest_ping", "Speedtest Ping", labels, nil), | ||||
| 		RemoteUserNumActive:   nd(ns+"remote_user_active", "Remote Users Active", labels, nil), | ||||
| 		RemoteUserNumInactive: nd(ns+"remote_user_inactive", "Remote Users Inactive", labels, nil), | ||||
| 		RemoteUserRxBytes:     nd(ns+"remote_user_receive_bytes_total", "Remote Users Receive Bytes", labels, nil), | ||||
| 		RemoteUserTxBytes:     nd(ns+"remote_user_transmit_bytes_total", "Remote Users Transmit Bytes", labels, nil), | ||||
| 		RemoteUserRxPackets:   nd(ns+"remote_user_receive_packets_total", "Remote Users Receive Packets", labels, nil), | ||||
| 		RemoteUserTxPackets:   nd(ns+"remote_user_transmit_packets_total", "Remote Users Transmit Packets", labels, nil), | ||||
| 		DPITxPackets:          nd(ns+"dpi_transmit_packets", "Site DPI Transmit Packets", labelDPI, nil), | ||||
| 		DPIRxPackets:          nd(ns+"dpi_receive_packets", "Site DPI Receive Packets", labelDPI, nil), | ||||
| 		DPITxBytes:            nd(ns+"dpi_transmit_bytes", "Site DPI Transmit Bytes", labelDPI, nil), | ||||
| 		DPIRxBytes:            nd(ns+"dpi_receive_bytes", "Site DPI Receive Bytes", labelDPI, nil), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *promUnifi) exportSiteDPI(r report, s *unifi.DPITable) { | ||||
| 	for _, dpi := range s.ByApp { | ||||
| 		labelDPI := []string{unifi.DPICats.Get(dpi.Cat), unifi.DPIApps.GetApp(dpi.Cat, dpi.App), s.SiteName, s.SourceName} | ||||
| 
 | ||||
| 		//	log.Println(labelsDPI, dpi.Cat, dpi.App, dpi.TxBytes, dpi.RxBytes, dpi.TxPackets, dpi.RxPackets)
 | ||||
| 		r.send([]*metric{ | ||||
| 			{u.Site.DPITxPackets, gauge, dpi.TxPackets, labelDPI}, | ||||
| 			{u.Site.DPIRxPackets, gauge, dpi.RxPackets, labelDPI}, | ||||
| 			{u.Site.DPITxBytes, gauge, dpi.TxBytes, labelDPI}, | ||||
| 			{u.Site.DPIRxBytes, gauge, dpi.RxBytes, labelDPI}, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *promUnifi) exportSite(r report, s *unifi.Site) { | ||||
| 	for _, h := range s.Health { | ||||
| 		switch labels := []string{h.Subsystem, h.Status, s.SiteName, s.SourceName}; labels[0] { | ||||
| 		case "www": | ||||
| 			r.send([]*metric{ | ||||
| 				{u.Site.TxBytesR, gauge, h.TxBytesR, labels}, | ||||
| 				{u.Site.RxBytesR, gauge, h.RxBytesR, labels}, | ||||
| 				{u.Site.Uptime, gauge, h.Uptime, labels}, | ||||
| 				{u.Site.Latency, gauge, h.Latency.Val / 1000, labels}, | ||||
| 				{u.Site.XputUp, gauge, h.XputUp, labels}, | ||||
| 				{u.Site.XputDown, gauge, h.XputDown, labels}, | ||||
| 				{u.Site.SpeedtestPing, gauge, h.SpeedtestPing, labels}, | ||||
| 				{u.Site.Drops, counter, h.Drops, labels}, | ||||
| 			}) | ||||
| 		case "wlan": | ||||
| 			r.send([]*metric{ | ||||
| 				{u.Site.TxBytesR, gauge, h.TxBytesR, labels}, | ||||
| 				{u.Site.RxBytesR, gauge, h.RxBytesR, labels}, | ||||
| 				{u.Site.NumAdopted, gauge, h.NumAdopted, labels}, | ||||
| 				{u.Site.NumDisconnected, gauge, h.NumDisconnected, labels}, | ||||
| 				{u.Site.NumPending, gauge, h.NumPending, labels}, | ||||
| 				{u.Site.NumUser, gauge, h.NumUser, labels}, | ||||
| 				{u.Site.NumGuest, gauge, h.NumGuest, labels}, | ||||
| 				{u.Site.NumIot, gauge, h.NumIot, labels}, | ||||
| 				{u.Site.NumAp, gauge, h.NumAp, labels}, | ||||
| 				{u.Site.NumDisabled, gauge, h.NumDisabled, labels}, | ||||
| 			}) | ||||
| 		case "wan": | ||||
| 			r.send([]*metric{ | ||||
| 				{u.Site.TxBytesR, gauge, h.TxBytesR, labels}, | ||||
| 				{u.Site.RxBytesR, gauge, h.RxBytesR, labels}, | ||||
| 				{u.Site.NumAdopted, gauge, h.NumAdopted, labels}, | ||||
| 				{u.Site.NumDisconnected, gauge, h.NumDisconnected, labels}, | ||||
| 				{u.Site.NumPending, gauge, h.NumPending, labels}, | ||||
| 				{u.Site.NumGw, gauge, h.NumGw, labels}, | ||||
| 				{u.Site.NumSta, gauge, h.NumSta, labels}, | ||||
| 			}) | ||||
| 		case "lan": | ||||
| 			r.send([]*metric{ | ||||
| 				{u.Site.TxBytesR, gauge, h.TxBytesR, labels}, | ||||
| 				{u.Site.RxBytesR, gauge, h.RxBytesR, labels}, | ||||
| 				{u.Site.NumAdopted, gauge, h.NumAdopted, labels}, | ||||
| 				{u.Site.NumDisconnected, gauge, h.NumDisconnected, labels}, | ||||
| 				{u.Site.NumPending, gauge, h.NumPending, labels}, | ||||
| 				{u.Site.NumUser, gauge, h.NumUser, labels}, | ||||
| 				{u.Site.NumGuest, gauge, h.NumGuest, labels}, | ||||
| 				{u.Site.NumIot, gauge, h.NumIot, labels}, | ||||
| 				{u.Site.NumSw, gauge, h.NumSw, labels}, | ||||
| 			}) | ||||
| 		case "vpn": | ||||
| 			r.send([]*metric{ | ||||
| 				{u.Site.RemoteUserNumActive, gauge, h.RemoteUserNumActive, labels}, | ||||
| 				{u.Site.RemoteUserNumInactive, gauge, h.RemoteUserNumInactive, labels}, | ||||
| 				{u.Site.RemoteUserRxBytes, counter, h.RemoteUserRxBytes, labels}, | ||||
| 				{u.Site.RemoteUserTxBytes, counter, h.RemoteUserTxBytes, labels}, | ||||
| 				{u.Site.RemoteUserRxPackets, counter, h.RemoteUserRxPackets, labels}, | ||||
| 				{u.Site.RemoteUserTxPackets, counter, h.RemoteUserTxPackets, labels}, | ||||
| 			}) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,320 +0,0 @@ | |||
| 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 | ||||
| 	RadioExtchannel         *prometheus.Desc | ||||
| 	RadioGain               *prometheus.Desc | ||||
| 	RadioNumSta             *prometheus.Desc | ||||
| 	RadioTxPackets          *prometheus.Desc | ||||
| 	RadioTxRetries          *prometheus.Desc | ||||
| } | ||||
| 
 | ||||
| func descUAP(ns string) *uap { | ||||
| 	labelA := []string{"stat", "site_name", "name", "source"} // stat + labels[1:]
 | ||||
| 	labelV := []string{"vap_name", "bssid", "radio", "radio_name", "essid", "usage", "site_name", "name", "source"} | ||||
| 	labelR := []string{"radio_name", "radio", "site_name", "name", "source"} | ||||
| 	nd := prometheus.NewDesc | ||||
| 
 | ||||
| 	return &uap{ | ||||
| 		// 3x each - stat table: total, guest, user
 | ||||
| 		ApWifiTxDropped:     nd(ns+"stat_wifi_transmt_dropped_total", "Wifi Transmissions Dropped", labelA, nil), | ||||
| 		ApRxErrors:          nd(ns+"stat_receive_errors_total", "Receive Errors", labelA, nil), | ||||
| 		ApRxDropped:         nd(ns+"stat_receive_dropped_total", "Receive Dropped", labelA, nil), | ||||
| 		ApRxFrags:           nd(ns+"stat_receive_frags_total", "Received Frags", labelA, nil), | ||||
| 		ApRxCrypts:          nd(ns+"stat_receive_crypts_total", "Receive Crypts", labelA, nil), | ||||
| 		ApTxPackets:         nd(ns+"stat_transmit_packets_total", "Transmit Packets", labelA, nil), | ||||
| 		ApTxBytes:           nd(ns+"stat_transmit_bytes_total", "Transmit Bytes", labelA, nil), | ||||
| 		ApTxErrors:          nd(ns+"stat_transmit_errors_total", "Transmit Errors", labelA, nil), | ||||
| 		ApTxDropped:         nd(ns+"stat_transmit_dropped_total", "Transmit Dropped", labelA, nil), | ||||
| 		ApTxRetries:         nd(ns+"stat_retries_tx_total", "Transmit Retries", labelA, nil), | ||||
| 		ApRxPackets:         nd(ns+"stat_receive_packets_total", "Receive Packets", labelA, nil), | ||||
| 		ApRxBytes:           nd(ns+"stat_receive_bytes_total", "Receive Bytes", labelA, nil), | ||||
| 		WifiTxAttempts:      nd(ns+"stat_wifi_transmit_attempts_total", "Wifi Transmission Attempts", labelA, nil), | ||||
| 		MacFilterRejections: nd(ns+"stat_mac_filter_rejects_total", "MAC Filter Rejections", labelA, nil), | ||||
| 		// N each - 1 per Virtual AP (VAP)
 | ||||
| 		VAPCcq:                   nd(ns+"vap_ccq_ratio", "VAP Client Connection Quality", labelV, nil), | ||||
| 		VAPMacFilterRejections:   nd(ns+"vap_mac_filter_rejects_total", "VAP MAC Filter Rejections", labelV, nil), | ||||
| 		VAPNumSatisfactionSta:    nd(ns+"vap_satisfaction_stations", "VAP Number Satisifaction Stations", labelV, nil), | ||||
| 		VAPAvgClientSignal:       nd(ns+"vap_average_client_signal", "VAP Average Client Signal", labelV, nil), | ||||
| 		VAPSatisfaction:          nd(ns+"vap_satisfaction_ratio", "VAP Satisfaction", labelV, nil), | ||||
| 		VAPSatisfactionNow:       nd(ns+"vap_satisfaction_now_ratio", "VAP Satisfaction Now", labelV, nil), | ||||
| 		VAPDNSAvgLatency:         nd(ns+"vap_dns_latency_average_seconds", "VAP DNS Latency Average", labelV, nil), | ||||
| 		VAPRxBytes:               nd(ns+"vap_receive_bytes_total", "VAP Bytes Received", labelV, nil), | ||||
| 		VAPRxCrypts:              nd(ns+"vap_receive_crypts_total", "VAP Crypts Received", labelV, nil), | ||||
| 		VAPRxDropped:             nd(ns+"vap_receive_dropped_total", "VAP Dropped Received", labelV, nil), | ||||
| 		VAPRxErrors:              nd(ns+"vap_receive_errors_total", "VAP Errors Received", labelV, nil), | ||||
| 		VAPRxFrags:               nd(ns+"vap_receive_frags_total", "VAP Frags Received", labelV, nil), | ||||
| 		VAPRxNwids:               nd(ns+"vap_receive_nwids_total", "VAP Nwids Received", labelV, nil), | ||||
| 		VAPRxPackets:             nd(ns+"vap_receive_packets_total", "VAP Packets Received", labelV, nil), | ||||
| 		VAPTxBytes:               nd(ns+"vap_transmit_bytes_total", "VAP Bytes Transmitted", labelV, nil), | ||||
| 		VAPTxDropped:             nd(ns+"vap_transmit_dropped_total", "VAP Dropped Transmitted", labelV, nil), | ||||
| 		VAPTxErrors:              nd(ns+"vap_transmit_errors_total", "VAP Errors Transmitted", labelV, nil), | ||||
| 		VAPTxPackets:             nd(ns+"vap_transmit_packets_total", "VAP Packets Transmitted", labelV, nil), | ||||
| 		VAPTxPower:               nd(ns+"vap_transmit_power", "VAP Transmit Power", labelV, nil), | ||||
| 		VAPTxRetries:             nd(ns+"vap_transmit_retries_total", "VAP Retries Transmitted", labelV, nil), | ||||
| 		VAPTxCombinedRetries:     nd(ns+"vap_transmit_retries_combined_total", "VAP Retries Combined Tx", labelV, nil), | ||||
| 		VAPTxDataMpduBytes:       nd(ns+"vap_data_mpdu_transmit_bytes_total", "VAP Data MPDU Bytes Tx", labelV, nil), | ||||
| 		VAPTxRtsRetries:          nd(ns+"vap_transmit_rts_retries_total", "VAP RTS Retries Transmitted", labelV, nil), | ||||
| 		VAPTxSuccess:             nd(ns+"vap_transmit_success_total", "VAP Success Transmits", labelV, nil), | ||||
| 		VAPTxTotal:               nd(ns+"vap_transmit_total", "VAP Transmit Total", labelV, nil), | ||||
| 		VAPTxGoodbytes:           nd(ns+"vap_transmit_goodbyes", "VAP Goodbyes Transmitted", labelV, nil), | ||||
| 		VAPTxLatAvg:              nd(ns+"vap_transmit_latency_average_seconds", "VAP Latency Average Tx", labelV, nil), | ||||
| 		VAPTxLatMax:              nd(ns+"vap_transmit_latency_maximum_seconds", "VAP Latency Maximum Tx", labelV, nil), | ||||
| 		VAPTxLatMin:              nd(ns+"vap_transmit_latency_minimum_seconds", "VAP Latency Minimum Tx", labelV, nil), | ||||
| 		VAPRxGoodbytes:           nd(ns+"vap_receive_goodbyes", "VAP Goodbyes Received", labelV, nil), | ||||
| 		VAPRxLatAvg:              nd(ns+"vap_receive_latency_average_seconds", "VAP Latency Average Rx", labelV, nil), | ||||
| 		VAPRxLatMax:              nd(ns+"vap_receive_latency_maximum_seconds", "VAP Latency Maximum Rx", labelV, nil), | ||||
| 		VAPRxLatMin:              nd(ns+"vap_receive_latency_minimum_seconds", "VAP Latency Minimum Rx", labelV, nil), | ||||
| 		VAPWifiTxLatencyMovAvg:   nd(ns+"vap_transmit_latency_moving_avg_seconds", "VAP Latency Moving Avg Tx", labelV, nil), | ||||
| 		VAPWifiTxLatencyMovMax:   nd(ns+"vap_transmit_latency_moving_max_seconds", "VAP Latency Moving Min Tx", labelV, nil), | ||||
| 		VAPWifiTxLatencyMovMin:   nd(ns+"vap_transmit_latency_moving_min_seconds", "VAP Latency Moving Max Tx", labelV, nil), | ||||
| 		VAPWifiTxLatencyMovTotal: nd(ns+"vap_transmit_latency_moving_total", "VAP Latency Moving Total Tramsit", labelV, nil), | ||||
| 		VAPWifiTxLatencyMovCount: nd(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: nd(ns+"radio_current_antenna_gain", "Radio Current Antenna Gain", labelR, nil), | ||||
| 		RadioHt:                 nd(ns+"radio_ht", "Radio HT", labelR, nil), | ||||
| 		RadioMaxTxpower:         nd(ns+"radio_max_transmit_power", "Radio Maximum Transmit Power", labelR, nil), | ||||
| 		RadioMinTxpower:         nd(ns+"radio_min_transmit_power", "Radio Minimum Transmit Power", labelR, nil), | ||||
| 		RadioNss:                nd(ns+"radio_nss", "Radio Nss", labelR, nil), | ||||
| 		RadioRadioCaps:          nd(ns+"radio_caps", "Radio Capabilities", labelR, nil), | ||||
| 		RadioTxPower:            nd(ns+"radio_transmit_power", "Radio Transmit Power", labelR, nil), | ||||
| 		RadioAstBeXmit:          nd(ns+"radio_ast_be_xmit", "Radio AstBe Transmit", labelR, nil), | ||||
| 		RadioChannel:            nd(ns+"radio_channel", "Radio Channel", labelR, nil), | ||||
| 		RadioCuSelfRx:           nd(ns+"radio_channel_utilization_receive_ratio", "Channel Utilization Rx", labelR, nil), | ||||
| 		RadioCuSelfTx:           nd(ns+"radio_channel_utilization_transmit_ratio", "Channel Utilization Tx", labelR, nil), | ||||
| 		RadioExtchannel:         nd(ns+"radio_ext_channel", "Radio Ext Channel", labelR, nil), | ||||
| 		RadioGain:               nd(ns+"radio_gain", "Radio Gain", labelR, nil), | ||||
| 		RadioNumSta:             nd(ns+"radio_stations", "Radio Total Station Count", append(labelR, "station_type"), nil), | ||||
| 		RadioTxPackets:          nd(ns+"radio_transmit_packets", "Radio Transmitted Packets", labelR, nil), | ||||
| 		RadioTxRetries:          nd(ns+"radio_transmit_retries", "Radio Transmit Retries", labelR, nil), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *promUnifi) exportUAP(r report, d *unifi.UAP) { | ||||
| 	if !d.Adopted.Val || d.Locating.Val { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	labels := []string{d.Type, d.SiteName, d.Name, d.SourceName} | ||||
| 	infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} | ||||
| 	u.exportUAPstats(r, labels, d.Stat.Ap, d.BytesD, d.TxBytesD, d.RxBytesD, d.BytesR) | ||||
| 	u.exportVAPtable(r, labels, d.VapTable) | ||||
| 	u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) | ||||
| 	u.exportSYSstats(r, labels, d.SysStats, d.SystemStats) | ||||
| 	u.exportSTAcount(r, labels, d.UserNumSta, d.GuestNumSta) | ||||
| 	u.exportRADtable(r, labels, d.RadioTable, d.RadioTableStats) | ||||
| 	r.send([]*metric{ | ||||
| 		{u.Device.Info, gauge, 1.0, append(labels, infoLabels...)}, | ||||
| 		{u.Device.Uptime, gauge, d.Uptime, labels}, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // udm doesn't have these stats exposed yet, so pass 2 or 6 metrics.
 | ||||
| func (u *promUnifi) exportUAPstats(r report, labels []string, ap *unifi.Ap, bytes ...unifi.FlexInt) { | ||||
| 	if ap == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	labelU := []string{"user", labels[1], labels[2], labels[3]} | ||||
| 	labelG := []string{"guest", labels[1], labels[2], labels[3]} | ||||
| 	r.send([]*metric{ | ||||
| 		// ap only stuff.
 | ||||
| 		{u.Device.BytesD, counter, bytes[0], labels},   // not sure if these 3 Ds are counters or gauges.
 | ||||
| 		{u.Device.TxBytesD, counter, bytes[1], labels}, // not sure if these 3 Ds are counters or gauges.
 | ||||
| 		{u.Device.RxBytesD, counter, bytes[2], labels}, // not sure if these 3 Ds are counters or gauges.
 | ||||
| 		{u.Device.BytesR, gauge, bytes[3], labels},     // only UAP has this one, and those ^ weird.
 | ||||
| 		// user
 | ||||
| 		{u.UAP.ApWifiTxDropped, counter, ap.UserWifiTxDropped, labelU}, | ||||
| 		{u.UAP.ApRxErrors, counter, ap.UserRxErrors, labelU}, | ||||
| 		{u.UAP.ApRxDropped, counter, ap.UserRxDropped, labelU}, | ||||
| 		{u.UAP.ApRxFrags, counter, ap.UserRxFrags, labelU}, | ||||
| 		{u.UAP.ApRxCrypts, counter, ap.UserRxCrypts, labelU}, | ||||
| 		{u.UAP.ApTxPackets, counter, ap.UserTxPackets, labelU}, | ||||
| 		{u.UAP.ApTxBytes, counter, ap.UserTxBytes, labelU}, | ||||
| 		{u.UAP.ApTxErrors, counter, ap.UserTxErrors, labelU}, | ||||
| 		{u.UAP.ApTxDropped, counter, ap.UserTxDropped, labelU}, | ||||
| 		{u.UAP.ApTxRetries, counter, ap.UserTxRetries, labelU}, | ||||
| 		{u.UAP.ApRxPackets, counter, ap.UserRxPackets, labelU}, | ||||
| 		{u.UAP.ApRxBytes, counter, ap.UserRxBytes, labelU}, | ||||
| 		{u.UAP.WifiTxAttempts, counter, ap.UserWifiTxAttempts, labelU}, | ||||
| 		{u.UAP.MacFilterRejections, counter, ap.UserMacFilterRejections, labelU}, | ||||
| 		// guest
 | ||||
| 		{u.UAP.ApWifiTxDropped, counter, ap.GuestWifiTxDropped, labelG}, | ||||
| 		{u.UAP.ApRxErrors, counter, ap.GuestRxErrors, labelG}, | ||||
| 		{u.UAP.ApRxDropped, counter, ap.GuestRxDropped, labelG}, | ||||
| 		{u.UAP.ApRxFrags, counter, ap.GuestRxFrags, labelG}, | ||||
| 		{u.UAP.ApRxCrypts, counter, ap.GuestRxCrypts, labelG}, | ||||
| 		{u.UAP.ApTxPackets, counter, ap.GuestTxPackets, labelG}, | ||||
| 		{u.UAP.ApTxBytes, counter, ap.GuestTxBytes, labelG}, | ||||
| 		{u.UAP.ApTxErrors, counter, ap.GuestTxErrors, labelG}, | ||||
| 		{u.UAP.ApTxDropped, counter, ap.GuestTxDropped, labelG}, | ||||
| 		{u.UAP.ApTxRetries, counter, ap.GuestTxRetries, labelG}, | ||||
| 		{u.UAP.ApRxPackets, counter, ap.GuestRxPackets, labelG}, | ||||
| 		{u.UAP.ApRxBytes, counter, ap.GuestRxBytes, labelG}, | ||||
| 		{u.UAP.WifiTxAttempts, counter, ap.GuestWifiTxAttempts, labelG}, | ||||
| 		{u.UAP.MacFilterRejections, counter, ap.GuestMacFilterRejections, labelG}, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // UAP VAP Table
 | ||||
| func (u *promUnifi) exportVAPtable(r report, labels []string, vt unifi.VapTable) { | ||||
| 	// vap table stats
 | ||||
| 	for _, v := range vt { | ||||
| 		if !v.Up.Val { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		labelV := []string{v.Name, v.Bssid, v.Radio, v.RadioName, v.Essid, v.Usage, labels[1], labels[2], labels[3]} | ||||
| 		r.send([]*metric{ | ||||
| 			{u.UAP.VAPCcq, gauge, float64(v.Ccq) / 1000.0, labelV}, | ||||
| 			{u.UAP.VAPMacFilterRejections, counter, v.MacFilterRejections, labelV}, | ||||
| 			{u.UAP.VAPNumSatisfactionSta, gauge, v.NumSatisfactionSta, labelV}, | ||||
| 			{u.UAP.VAPAvgClientSignal, gauge, v.AvgClientSignal.Val, labelV}, | ||||
| 			{u.UAP.VAPSatisfaction, gauge, v.Satisfaction.Val / 100.0, labelV}, | ||||
| 			{u.UAP.VAPSatisfactionNow, gauge, v.SatisfactionNow.Val / 100.0, labelV}, | ||||
| 			{u.UAP.VAPDNSAvgLatency, gauge, v.DNSAvgLatency.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPRxBytes, counter, v.RxBytes, labelV}, | ||||
| 			{u.UAP.VAPRxCrypts, counter, v.RxCrypts, labelV}, | ||||
| 			{u.UAP.VAPRxDropped, counter, v.RxDropped, labelV}, | ||||
| 			{u.UAP.VAPRxErrors, counter, v.RxErrors, labelV}, | ||||
| 			{u.UAP.VAPRxFrags, counter, v.RxFrags, labelV}, | ||||
| 			{u.UAP.VAPRxNwids, counter, v.RxNwids, labelV}, | ||||
| 			{u.UAP.VAPRxPackets, counter, v.RxPackets, labelV}, | ||||
| 			{u.UAP.VAPTxBytes, counter, v.TxBytes, labelV}, | ||||
| 			{u.UAP.VAPTxDropped, counter, v.TxDropped, labelV}, | ||||
| 			{u.UAP.VAPTxErrors, counter, v.TxErrors, labelV}, | ||||
| 			{u.UAP.VAPTxPackets, counter, v.TxPackets, labelV}, | ||||
| 			{u.UAP.VAPTxPower, gauge, v.TxPower, labelV}, | ||||
| 			{u.UAP.VAPTxRetries, counter, v.TxRetries, labelV}, | ||||
| 			{u.UAP.VAPTxCombinedRetries, counter, v.TxCombinedRetries, labelV}, | ||||
| 			{u.UAP.VAPTxDataMpduBytes, counter, v.TxDataMpduBytes, labelV}, | ||||
| 			{u.UAP.VAPTxRtsRetries, counter, v.TxRtsRetries, labelV}, | ||||
| 			{u.UAP.VAPTxTotal, counter, v.TxTotal, labelV}, | ||||
| 			{u.UAP.VAPTxGoodbytes, counter, v.TxTCPStats.Goodbytes, labelV}, | ||||
| 			{u.UAP.VAPTxLatAvg, gauge, v.TxTCPStats.LatAvg.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPTxLatMax, gauge, v.TxTCPStats.LatMax.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPTxLatMin, gauge, v.TxTCPStats.LatMin.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPRxGoodbytes, counter, v.RxTCPStats.Goodbytes, labelV}, | ||||
| 			{u.UAP.VAPRxLatAvg, gauge, v.RxTCPStats.LatAvg.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPRxLatMax, gauge, v.RxTCPStats.LatMax.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPRxLatMin, gauge, v.RxTCPStats.LatMin.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPWifiTxLatencyMovAvg, gauge, v.WifiTxLatencyMov.Avg.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPWifiTxLatencyMovMax, gauge, v.WifiTxLatencyMov.Max.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPWifiTxLatencyMovMin, gauge, v.WifiTxLatencyMov.Min.Val / 1000, labelV}, | ||||
| 			{u.UAP.VAPWifiTxLatencyMovTotal, counter, v.WifiTxLatencyMov.Total, labelV},      // not sure if gauge or counter.
 | ||||
| 			{u.UAP.VAPWifiTxLatencyMovCount, counter, v.WifiTxLatencyMov.TotalCount, labelV}, // not sure if gauge or counter.
 | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // UAP Radio Table
 | ||||
| func (u *promUnifi) exportRADtable(r report, labels []string, rt unifi.RadioTable, rts unifi.RadioTableStats) { | ||||
| 	// radio table
 | ||||
| 	for _, p := range rt { | ||||
| 		labelR := []string{p.Name, p.Radio, labels[1], labels[2], labels[3]} | ||||
| 		labelRUser := append(labelR, "user") | ||||
| 		labelRGuest := append(labelR, "guest") | ||||
| 
 | ||||
| 		r.send([]*metric{ | ||||
| 			{u.UAP.RadioCurrentAntennaGain, gauge, p.CurrentAntennaGain, labelR}, | ||||
| 			{u.UAP.RadioHt, gauge, p.Ht, labelR}, | ||||
| 			{u.UAP.RadioMaxTxpower, gauge, p.MaxTxpower, labelR}, | ||||
| 			{u.UAP.RadioMinTxpower, gauge, p.MinTxpower, labelR}, | ||||
| 			{u.UAP.RadioNss, gauge, p.Nss, labelR}, | ||||
| 			{u.UAP.RadioRadioCaps, gauge, 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, gauge, t.TxPower, labelR}, | ||||
| 				{u.UAP.RadioAstBeXmit, gauge, t.AstBeXmit, labelR}, | ||||
| 				{u.UAP.RadioChannel, gauge, t.Channel, labelR}, | ||||
| 				{u.UAP.RadioCuSelfRx, gauge, t.CuSelfRx.Val / 100.0, labelR}, | ||||
| 				{u.UAP.RadioCuSelfTx, gauge, t.CuSelfTx.Val / 100.0, labelR}, | ||||
| 				{u.UAP.RadioExtchannel, gauge, t.Extchannel, labelR}, | ||||
| 				{u.UAP.RadioGain, gauge, t.Gain, labelR}, | ||||
| 				{u.UAP.RadioNumSta, gauge, t.GuestNumSta, labelRGuest}, | ||||
| 				{u.UAP.RadioNumSta, gauge, t.UserNumSta, labelRUser}, | ||||
| 				{u.UAP.RadioTxPackets, gauge, t.TxPackets, labelR}, | ||||
| 				{u.UAP.RadioTxRetries, gauge, t.TxRetries, labelR}, | ||||
| 			}) | ||||
| 
 | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | @ -1,131 +0,0 @@ | |||
| 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 { | ||||
| 	Info          *prometheus.Desc | ||||
| 	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
 | ||||
| 	TxBytesD      *prometheus.Desc // ap only
 | ||||
| 	RxBytesD      *prometheus.Desc // ap only
 | ||||
| 	Counter       *prometheus.Desc | ||||
| 	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{"type", "site_name", "name", "source"} | ||||
| 	infoLabels := []string{"version", "model", "serial", "mac", "ip", "id", "bytes", "uptime"} | ||||
| 
 | ||||
| 	return &unifiDevice{ | ||||
| 		Info:          prometheus.NewDesc(ns+"info", "Device Information", append(labels, infoLabels...), nil), | ||||
| 		Uptime:        prometheus.NewDesc(ns+"uptime_seconds", "Device 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), | ||||
| 		TxBytesD:      prometheus.NewDesc(ns+"d_tranmsit_bytes", "Transmit Bytes D???", labels, nil), | ||||
| 		RxBytesD:      prometheus.NewDesc(ns+"d_receive_bytes", "Receive Bytes D???", labels, nil), | ||||
| 		Counter:       prometheus.NewDesc(ns+"stations", "Number of Stations", append(labels, "station_type"), 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_ratio", "System CPU % Utilized", labels, nil), | ||||
| 		Mem:           prometheus.NewDesc(ns+"memory_utilization_ratio", "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) { | ||||
| 	if !d.Adopted.Val || d.Locating.Val { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	labels := []string{d.Type, d.SiteName, d.Name, d.SourceName} | ||||
| 	infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} | ||||
| 	// Shared data (all devices do this).
 | ||||
| 	u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) | ||||
| 	u.exportSYSstats(r, labels, d.SysStats, d.SystemStats) | ||||
| 	u.exportSTAcount(r, labels, d.UserNumSta, d.GuestNumSta, d.NumDesktop, d.NumMobile, d.NumHandheld) | ||||
| 	// Switch Data
 | ||||
| 	u.exportUSWstats(r, labels, d.Stat.Sw) | ||||
| 	u.exportPRTtable(r, labels, d.PortTable) | ||||
| 	// Gateway Data
 | ||||
| 	u.exportWANPorts(r, labels, d.Wan1, d.Wan2) | ||||
| 	u.exportUSGstats(r, labels, d.Stat.Gw, d.SpeedtestStatus, d.Uplink) | ||||
| 	// Dream Machine System Data.
 | ||||
| 	r.send([]*metric{ | ||||
| 		{u.Device.Info, gauge, 1.0, append(labels, infoLabels...)}, | ||||
| 		{u.Device.Uptime, gauge, d.Uptime, labels}, | ||||
| 	}) | ||||
| 
 | ||||
| 	// Wireless Data - UDM (non-pro) only
 | ||||
| 	if d.Stat.Ap != nil && d.VapTable != nil { | ||||
| 		u.exportUAPstats(r, labels, d.Stat.Ap, d.BytesD, d.TxBytesD, d.RxBytesD, d.BytesR) | ||||
| 		u.exportVAPtable(r, labels, *d.VapTable) | ||||
| 		u.exportRADtable(r, labels, *d.RadioTable, *d.RadioTableStats) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // shared by all
 | ||||
| func (u *promUnifi) exportBYTstats(r report, labels []string, tx, rx unifi.FlexInt) { | ||||
| 	r.send([]*metric{ | ||||
| 		{u.Device.TotalTxBytes, counter, tx, labels}, | ||||
| 		{u.Device.TotalRxBytes, counter, rx, labels}, | ||||
| 		{u.Device.TotalBytes, counter, tx.Val + rx.Val, labels}, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // shared by all, pass 2 or 5 stats.
 | ||||
| func (u *promUnifi) exportSTAcount(r report, labels []string, stas ...unifi.FlexInt) { | ||||
| 	r.send([]*metric{ | ||||
| 		{u.Device.Counter, gauge, stas[0], append(labels, "user")}, | ||||
| 		{u.Device.Counter, gauge, stas[1], append(labels, "guest")}, | ||||
| 	}) | ||||
| 
 | ||||
| 	if len(stas) > 2 { | ||||
| 		r.send([]*metric{ | ||||
| 			{u.Device.Counter, gauge, stas[2], append(labels, "desktop")}, | ||||
| 			{u.Device.Counter, gauge, stas[3], append(labels, "mobile")}, | ||||
| 			{u.Device.Counter, gauge, stas[4], append(labels, "handheld")}, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // shared by all
 | ||||
| func (u *promUnifi) exportSYSstats(r report, labels []string, s unifi.SysStats, ss unifi.SystemStats) { | ||||
| 	r.send([]*metric{ | ||||
| 		{u.Device.Loadavg1, gauge, s.Loadavg1, labels}, | ||||
| 		{u.Device.Loadavg5, gauge, s.Loadavg5, labels}, | ||||
| 		{u.Device.Loadavg15, gauge, s.Loadavg15, labels}, | ||||
| 		{u.Device.MemUsed, gauge, s.MemUsed, labels}, | ||||
| 		{u.Device.MemTotal, gauge, s.MemTotal, labels}, | ||||
| 		{u.Device.MemBuffer, gauge, s.MemBuffer, labels}, | ||||
| 		{u.Device.CPU, gauge, ss.CPU.Val / 100.0, labels}, | ||||
| 		{u.Device.Mem, gauge, ss.Mem.Val / 100.0, labels}, | ||||
| 	}) | ||||
| } | ||||
|  | @ -1,144 +0,0 @@ | |||
| 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 | ||||
| 	UplinkLatency  *prometheus.Desc | ||||
| 	UplinkSpeed    *prometheus.Desc | ||||
| 	Runtime        *prometheus.Desc | ||||
| 	XputDownload   *prometheus.Desc | ||||
| 	XputUpload     *prometheus.Desc | ||||
| } | ||||
| 
 | ||||
| func descUSG(ns string) *usg { | ||||
| 	labels := []string{"port", "site_name", "name", "source"} | ||||
| 
 | ||||
| 	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), | ||||
| 		UplinkLatency:  prometheus.NewDesc(ns+"uplink_latency_seconds", "Uplink Latency", labels, nil), | ||||
| 		UplinkSpeed:    prometheus.NewDesc(ns+"uplink_speed_mbps", "Uplink Speed", 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) { | ||||
| 	if !d.Adopted.Val || d.Locating.Val { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	labels := []string{d.Type, d.SiteName, d.Name, d.SourceName} | ||||
| 	infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} | ||||
| 
 | ||||
| 	// Gateway System Data.
 | ||||
| 	u.exportWANPorts(r, labels, d.Wan1, d.Wan2) | ||||
| 	u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) | ||||
| 	u.exportSYSstats(r, labels, d.SysStats, d.SystemStats) | ||||
| 	u.exportUSGstats(r, labels, d.Stat.Gw, d.SpeedtestStatus, d.Uplink) | ||||
| 	u.exportSTAcount(r, labels, d.UserNumSta, d.GuestNumSta, d.NumDesktop, d.UserNumSta, d.GuestNumSta) | ||||
| 	r.send([]*metric{ | ||||
| 		{u.Device.Info, gauge, 1.0, append(labels, infoLabels...)}, | ||||
| 		{u.Device.Uptime, gauge, d.Uptime, labels}, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // Gateway States
 | ||||
| func (u *promUnifi) exportUSGstats(r report, labels []string, gw *unifi.Gw, st unifi.SpeedtestStatus, ul unifi.Uplink) { | ||||
| 	if gw == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	labelLan := []string{"lan", labels[1], labels[2], labels[3]} | ||||
| 	labelWan := []string{"all", labels[1], labels[2], labels[3]} | ||||
| 
 | ||||
| 	r.send([]*metric{ | ||||
| 		{u.USG.LanRxPackets, counter, gw.LanRxPackets, labelLan}, | ||||
| 		{u.USG.LanRxBytes, counter, gw.LanRxBytes, labelLan}, | ||||
| 		{u.USG.LanTxPackets, counter, gw.LanTxPackets, labelLan}, | ||||
| 		{u.USG.LanTxBytes, counter, gw.LanTxBytes, labelLan}, | ||||
| 		{u.USG.LanRxDropped, counter, gw.LanRxDropped, labelLan}, | ||||
| 		{u.USG.UplinkLatency, gauge, ul.Latency.Val / 1000, labelWan}, | ||||
| 		{u.USG.UplinkSpeed, gauge, ul.Speed, labelWan}, | ||||
| 		// Speed Test Stats
 | ||||
| 		{u.USG.Latency, gauge, st.Latency.Val / 1000, labelWan}, | ||||
| 		{u.USG.Runtime, gauge, st.Runtime, labelWan}, | ||||
| 		{u.USG.XputDownload, gauge, st.XputDownload, labelWan}, | ||||
| 		{u.USG.XputUpload, gauge, st.XputUpload, labelWan}, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // WAN Stats
 | ||||
| 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[1], labels[2], labels[3]} | ||||
| 
 | ||||
| 		r.send([]*metric{ | ||||
| 			{u.USG.WanRxPackets, counter, wan.RxPackets, labelWan}, | ||||
| 			{u.USG.WanRxBytes, counter, wan.RxBytes, labelWan}, | ||||
| 			{u.USG.WanRxDropped, counter, wan.RxDropped, labelWan}, | ||||
| 			{u.USG.WanRxErrors, counter, wan.RxErrors, labelWan}, | ||||
| 			{u.USG.WanTxPackets, counter, wan.TxPackets, labelWan}, | ||||
| 			{u.USG.WanTxBytes, counter, wan.TxBytes, labelWan}, | ||||
| 			{u.USG.WanRxBroadcast, counter, wan.RxBroadcast, labelWan}, | ||||
| 			{u.USG.WanRxMulticast, counter, wan.RxMulticast, labelWan}, | ||||
| 			{u.USG.WanSpeed, counter, wan.Speed.Val * 1000000, labelWan}, | ||||
| 			{u.USG.WanTxBroadcast, counter, wan.TxBroadcast, labelWan}, | ||||
| 			{u.USG.WanTxBytesR, counter, wan.TxBytesR, labelWan}, | ||||
| 			{u.USG.WanTxDropped, counter, wan.TxDropped, labelWan}, | ||||
| 			{u.USG.WanTxErrors, counter, wan.TxErrors, labelWan}, | ||||
| 			{u.USG.WanTxMulticast, counter, wan.TxMulticast, labelWan}, | ||||
| 			{u.USG.WanBytesR, gauge, wan.BytesR, labelWan}, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,194 +0,0 @@ | |||
| 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{"site_name", "name", "source"} | ||||
| 	labelP := []string{"port_id", "port_num", "port_name", "port_mac", "port_ip", "site_name", "name", "source"} | ||||
| 	nd := prometheus.NewDesc | ||||
| 
 | ||||
| 	return &usw{ | ||||
| 		// This data may be derivable by sum()ing the port data.
 | ||||
| 		SwRxPackets:   nd(ns+"switch_receive_packets_total", "Switch Packets Received Total", labelS, nil), | ||||
| 		SwRxBytes:     nd(ns+"switch_receive_bytes_total", "Switch Bytes Received Total", labelS, nil), | ||||
| 		SwRxErrors:    nd(ns+"switch_receive_errors_total", "Switch Errors Received Total", labelS, nil), | ||||
| 		SwRxDropped:   nd(ns+"switch_receive_dropped_total", "Switch Dropped Received Total", labelS, nil), | ||||
| 		SwRxCrypts:    nd(ns+"switch_receive_crypts_total", "Switch Crypts Received Total", labelS, nil), | ||||
| 		SwRxFrags:     nd(ns+"switch_receive_frags_total", "Switch Frags Received Total", labelS, nil), | ||||
| 		SwTxPackets:   nd(ns+"switch_transmit_packets_total", "Switch Packets Transmit Total", labelS, nil), | ||||
| 		SwTxBytes:     nd(ns+"switch_transmit_bytes_total", "Switch Bytes Transmit Total", labelS, nil), | ||||
| 		SwTxErrors:    nd(ns+"switch_transmit_errors_total", "Switch Errors Transmit Total", labelS, nil), | ||||
| 		SwTxDropped:   nd(ns+"switch_transmit_dropped_total", "Switch Dropped Transmit Total", labelS, nil), | ||||
| 		SwTxRetries:   nd(ns+"switch_transmit_retries_total", "Switch Retries Transmit Total", labelS, nil), | ||||
| 		SwRxMulticast: nd(ns+"switch_receive_multicast_total", "Switch Multicast Receive Total", labelS, nil), | ||||
| 		SwRxBroadcast: nd(ns+"switch_receive_broadcast_total", "Switch Broadcast Receive Total", labelS, nil), | ||||
| 		SwTxMulticast: nd(ns+"switch_transmit_multicast_total", "Switch Multicast Transmit Total", labelS, nil), | ||||
| 		SwTxBroadcast: nd(ns+"switch_transmit_broadcast_total", "Switch Broadcast Transmit Total", labelS, nil), | ||||
| 		SwBytes:       nd(ns+"switch_bytes_total", "Switch Bytes Transferred Total", labelS, nil), | ||||
| 		// per-port data
 | ||||
| 		PoeCurrent:   nd(pns+"poe_amperes", "POE Current", labelP, nil), | ||||
| 		PoePower:     nd(pns+"poe_watts", "POE Power", labelP, nil), | ||||
| 		PoeVoltage:   nd(pns+"poe_volts", "POE Voltage", labelP, nil), | ||||
| 		RxBroadcast:  nd(pns+"receive_broadcast_total", "Receive Broadcast", labelP, nil), | ||||
| 		RxBytes:      nd(pns+"receive_bytes_total", "Total Receive Bytes", labelP, nil), | ||||
| 		RxBytesR:     nd(pns+"receive_rate_bytes", "Receive Bytes Rate", labelP, nil), | ||||
| 		RxDropped:    nd(pns+"receive_dropped_total", "Total Receive Dropped", labelP, nil), | ||||
| 		RxErrors:     nd(pns+"receive_errors_total", "Total Receive Errors", labelP, nil), | ||||
| 		RxMulticast:  nd(pns+"receive_multicast_total", "Total Receive Multicast", labelP, nil), | ||||
| 		RxPackets:    nd(pns+"receive_packets_total", "Total Receive Packets", labelP, nil), | ||||
| 		Satisfaction: nd(pns+"satisfaction_ratio", "Satisfaction", labelP, nil), | ||||
| 		Speed:        nd(pns+"port_speed_bps", "Speed", labelP, nil), | ||||
| 		TxBroadcast:  nd(pns+"transmit_broadcast_total", "Total Transmit Broadcast", labelP, nil), | ||||
| 		TxBytes:      nd(pns+"transmit_bytes_total", "Total Transmit Bytes", labelP, nil), | ||||
| 		TxBytesR:     nd(pns+"transmit_rate_bytes", "Transmit Bytes Rate", labelP, nil), | ||||
| 		TxDropped:    nd(pns+"transmit_dropped_total", "Total Transmit Dropped", labelP, nil), | ||||
| 		TxErrors:     nd(pns+"transmit_errors_total", "Total Transmit Errors", labelP, nil), | ||||
| 		TxMulticast:  nd(pns+"transmit_multicast_total", "Total Tranmist Multicast", labelP, nil), | ||||
| 		TxPackets:    nd(pns+"transmit_packets_total", "Total Transmit Packets", labelP, nil), | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (u *promUnifi) exportUSW(r report, d *unifi.USW) { | ||||
| 	if !d.Adopted.Val || d.Locating.Val { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	labels := []string{d.Type, d.SiteName, d.Name, d.SourceName} | ||||
| 	infoLabels := []string{d.Version, d.Model, d.Serial, d.Mac, d.IP, d.ID, d.Bytes.Txt, d.Uptime.Txt} | ||||
| 
 | ||||
| 	u.exportUSWstats(r, labels, d.Stat.Sw) | ||||
| 	u.exportPRTtable(r, labels, d.PortTable) | ||||
| 	u.exportBYTstats(r, labels, d.TxBytes, d.RxBytes) | ||||
| 	u.exportSYSstats(r, labels, d.SysStats, d.SystemStats) | ||||
| 	u.exportSTAcount(r, labels, d.UserNumSta, d.GuestNumSta) | ||||
| 	r.send([]*metric{ | ||||
| 		{u.Device.Info, gauge, 1.0, append(labels, infoLabels...)}, | ||||
| 		{u.Device.Uptime, gauge, d.Uptime, labels}, | ||||
| 	}) | ||||
| 
 | ||||
| 	// Switch System Data.
 | ||||
| 	if d.HasTemperature.Val { | ||||
| 		r.send([]*metric{{u.Device.Temperature, gauge, d.GeneralTemperature, labels}}) | ||||
| 	} | ||||
| 
 | ||||
| 	if d.HasFan.Val { | ||||
| 		r.send([]*metric{{u.Device.FanLevel, gauge, d.FanLevel, labels}}) | ||||
| 	} | ||||
| 
 | ||||
| 	if d.TotalMaxPower.Txt != "" { | ||||
| 		r.send([]*metric{{u.Device.TotalMaxPower, gauge, d.TotalMaxPower, labels}}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Switch Stats
 | ||||
| func (u *promUnifi) exportUSWstats(r report, labels []string, sw *unifi.Sw) { | ||||
| 	if sw == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	labelS := labels[1:] | ||||
| 
 | ||||
| 	r.send([]*metric{ | ||||
| 		{u.USW.SwRxPackets, counter, sw.RxPackets, labelS}, | ||||
| 		{u.USW.SwRxBytes, counter, sw.RxBytes, labelS}, | ||||
| 		{u.USW.SwRxErrors, counter, sw.RxErrors, labelS}, | ||||
| 		{u.USW.SwRxDropped, counter, sw.RxDropped, labelS}, | ||||
| 		{u.USW.SwRxCrypts, counter, sw.RxCrypts, labelS}, | ||||
| 		{u.USW.SwRxFrags, counter, sw.RxFrags, labelS}, | ||||
| 		{u.USW.SwTxPackets, counter, sw.TxPackets, labelS}, | ||||
| 		{u.USW.SwTxBytes, counter, sw.TxBytes, labelS}, | ||||
| 		{u.USW.SwTxErrors, counter, sw.TxErrors, labelS}, | ||||
| 		{u.USW.SwTxDropped, counter, sw.TxDropped, labelS}, | ||||
| 		{u.USW.SwTxRetries, counter, sw.TxRetries, labelS}, | ||||
| 		{u.USW.SwRxMulticast, counter, sw.RxMulticast, labelS}, | ||||
| 		{u.USW.SwRxBroadcast, counter, sw.RxBroadcast, labelS}, | ||||
| 		{u.USW.SwTxMulticast, counter, sw.TxMulticast, labelS}, | ||||
| 		{u.USW.SwTxBroadcast, counter, sw.TxBroadcast, labelS}, | ||||
| 		{u.USW.SwBytes, counter, sw.Bytes, labelS}, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // Switch Port Table
 | ||||
| func (u *promUnifi) exportPRTtable(r report, labels []string, pt []unifi.Port) { | ||||
| 	// Per-port data on a switch
 | ||||
| 	for _, p := range pt { | ||||
| 		if !p.Up.Val || !p.Enable.Val { | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// Copy labels, and add four new ones.
 | ||||
| 		labelP := []string{labels[2] + " Port " + p.PortIdx.Txt, p.PortIdx.Txt, | ||||
| 			p.Name, p.Mac, p.IP, labels[1], labels[2], labels[3]} | ||||
| 
 | ||||
| 		if p.PoeEnable.Val && p.PortPoe.Val { | ||||
| 			r.send([]*metric{ | ||||
| 				{u.USW.PoeCurrent, gauge, p.PoeCurrent, labelP}, | ||||
| 				{u.USW.PoePower, gauge, p.PoePower, labelP}, | ||||
| 				{u.USW.PoeVoltage, gauge, p.PoeVoltage, labelP}, | ||||
| 			}) | ||||
| 		} | ||||
| 
 | ||||
| 		r.send([]*metric{ | ||||
| 			{u.USW.RxBroadcast, counter, p.RxBroadcast, labelP}, | ||||
| 			{u.USW.RxBytes, counter, p.RxBytes, labelP}, | ||||
| 			{u.USW.RxBytesR, gauge, p.RxBytesR, labelP}, | ||||
| 			{u.USW.RxDropped, counter, p.RxDropped, labelP}, | ||||
| 			{u.USW.RxErrors, counter, p.RxErrors, labelP}, | ||||
| 			{u.USW.RxMulticast, counter, p.RxMulticast, labelP}, | ||||
| 			{u.USW.RxPackets, counter, p.RxPackets, labelP}, | ||||
| 			{u.USW.Satisfaction, gauge, p.Satisfaction.Val / 100.0, labelP}, | ||||
| 			{u.USW.Speed, gauge, p.Speed.Val * 1000000, labelP}, | ||||
| 			{u.USW.TxBroadcast, counter, p.TxBroadcast, labelP}, | ||||
| 			{u.USW.TxBytes, counter, p.TxBytes, labelP}, | ||||
| 			{u.USW.TxBytesR, gauge, p.TxBytesR, labelP}, | ||||
| 			{u.USW.TxDropped, counter, p.TxDropped, labelP}, | ||||
| 			{u.USW.TxErrors, counter, p.TxErrors, labelP}, | ||||
| 			{u.USW.TxMulticast, counter, p.TxMulticast, labelP}, | ||||
| 			{u.USW.TxPackets, counter, p.TxPackets, labelP}, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | @ -1,26 +0,0 @@ | |||
| # MYSQL Output Plugin Example | ||||
| 
 | ||||
| The code here, and the dynamic plugin provided shows an example of how you can | ||||
| write your own output for unifi-poller. This plugin records some very basic | ||||
| data about clients on a unifi network into a mysql database. | ||||
| 
 | ||||
| You could write outputs that do... anything. An example: They could compare current | ||||
| connected clients to a previous list (in a db, or stored in memory), and send a | ||||
| notification if it changes. The possibilities are endless. | ||||
| 
 | ||||
| You must compile your plugin using the unifi-poller source for the version you're | ||||
| using. In other words, to build a plugin for version 2.0.1, do this: | ||||
| ``` | ||||
| mkdir -p $GOPATH/src/github.com/davidnewhall | ||||
| cd $GOPATH/src/github.com/davidnewhall | ||||
| 
 | ||||
| git clone git@github.com:davidnewhall/unifi-poller.git | ||||
| cd unifi-poller | ||||
| 
 | ||||
| git checkout v2.0.1 | ||||
| make vendor | ||||
| 
 | ||||
| cp -r <your plugin> plugins/ | ||||
| GOOS=linux make plugins | ||||
| ``` | ||||
| The plugin you copy in *must* have a `main.go` file for `make plugins` to build it. | ||||
|  | @ -1,45 +0,0 @@ | |||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	"golift.io/cnfg" | ||||
| ) | ||||
| 
 | ||||
| // mysqlConfig represents the data that is unmarshalled from the up.conf config file for this plugins.
 | ||||
| type mysqlConfig struct { | ||||
| 	Interval cnfg.Duration `json:"interval" toml:"interval" xml:"interval" yaml:"interval"` | ||||
| 	Host     string        `json:"host" toml:"host" xml:"host" yaml:"host"` | ||||
| 	User     string        `json:"user" toml:"user" xml:"user" yaml:"user"` | ||||
| 	Pass     string        `json:"pass" toml:"pass" xml:"pass" yaml:"pass"` | ||||
| 	DB       string        `json:"db" toml:"db" xml:"db" yaml:"db"` | ||||
| 	Table    string        `json:"table" toml:"table" xml:"table" yaml:"table"` | ||||
| 	// Maps do not work with ENV VARIABLES yet, but may in the future.
 | ||||
| 	Fields []string `json:"fields" toml:"fields" xml:"field" yaml:"fields"` | ||||
| } | ||||
| 
 | ||||
| // Pointers are ignored during ENV variable unmarshal, avoid pointers to your config.
 | ||||
| // Only capital (exported) members are unmarshaled when passed into poller.NewOutput().
 | ||||
| type plugin struct { | ||||
| 	Config mysqlConfig `json:"mysql" toml:"mysql" xml:"mysql" yaml:"mysql"` | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	u := &plugin{Config: mysqlConfig{}} | ||||
| 
 | ||||
| 	poller.NewOutput(&poller.Output{ | ||||
| 		Name:   "mysql", | ||||
| 		Config: u, // pass in the struct *above* your config (so it can see the struct tags).
 | ||||
| 		Method: u.Run, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func main() { | ||||
| 	fmt.Println("this is a unifi-poller plugin; not an application") | ||||
| } | ||||
| 
 | ||||
| func (a *plugin) Run(c poller.Collect) error { | ||||
| 	c.Logf("mysql plugin is not finished") | ||||
| 	return nil | ||||
| } | ||||
		Loading…
	
		Reference in New Issue