make output plugins call in to initialize
This commit is contained in:
		
							parent
							
								
									60d645c1a7
								
							
						
					
					
						commit
						ac39d1727f
					
				|  | @ -32,11 +32,6 @@ SOURCE_URL="https://${IMPORT_PATH}" | |||
| # Used for documentation links. | ||||
| URL="${SOURCE_URL}" | ||||
| 
 | ||||
| # This parameter is passed in as -X to go build. Used to override the Version variable in a package. | ||||
| # This makes a path like github.com/user/hello-world/helloworld.Version=1.3.3 | ||||
| # Name the Version-containing library the same as the github repo, without dashes. | ||||
| VERSION_PATH="${IMPORT_PATH}/pkg/poller.Version" | ||||
| 
 | ||||
| # Dynamic. Recommend not changing. | ||||
| VVERSION=$(git describe --abbrev=0 --tags $(git rev-list --tags --max-count=1)) | ||||
| VERSION="$(echo $VVERSION | tr -d v | grep -E '^\S+$' || echo development)" | ||||
|  | @ -51,4 +46,4 @@ COMMIT="$(git rev-parse --short HEAD || echo 0)" | |||
| # This is a custom download path for homebrew formula. | ||||
| SOURCE_PATH=https://golift.io/${BINARY}/archive/v${VERSION}.tar.gz | ||||
| 
 | ||||
| export IMPORT_PATH SOURCE_URL URL VERSION_PATH VVERSION VERSION ITERATION DATE BRANCH COMMIT SOURCE_PATH | ||||
| export IMPORT_PATH SOURCE_URL URL VVERSION VERSION ITERATION DATE BRANCH COMMIT SOURCE_PATH | ||||
|  |  | |||
							
								
								
									
										2
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										2
									
								
								Makefile
								
								
								
								
							|  | @ -46,7 +46,7 @@ VERSION_LDFLAGS:= \ | |||
|   -X $(IMPORT_PATH)/vendor/github.com/prometheus/common/version.Branch=$(BRANCH) \
 | ||||
|   -X $(IMPORT_PATH)/vendor/github.com/prometheus/common/version.BuildDate=$(DATE) \
 | ||||
|   -X $(IMPORT_PATH)/vendor/github.com/prometheus/common/version.Revision=$(COMMIT) \
 | ||||
|   -X $(VERSION_PATH)=$(VERSION)-$(ITERATION) | ||||
|   -X $(IMPORT_PATH)/vendor/github.com/prometheus/common/version.Version=$(VERSION)-$(ITERATION) | ||||
| 
 | ||||
| # Makefile targets follow.
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -65,10 +65,10 @@ is provided so the application can be easily adapted to any environment. | |||
| 
 | ||||
| `Config File Parameters` | ||||
| 
 | ||||
|     interval               default: 30s | ||||
|         How often to poll the controller for updated client and device data. | ||||
|         The UniFi Controller only updates traffic stats about every 30-60 seconds. | ||||
|         Only works if "mode" (below) is "influx" - other modes do not use interval. | ||||
| Additional parameters are added by output packages. Parameters can also be set | ||||
| using environment variables. See the GitHub wiki for more information!  | ||||
| 
 | ||||
|     >>> POLLER FIELDS FOLLOW - you may have multiple controllers: | ||||
| 
 | ||||
|     debug                  default: false | ||||
|         This turns on time stamps and line numbers in logs, outputs a few extra | ||||
|  | @ -79,56 +79,7 @@ is provided so the application can be easily adapted to any environment. | |||
|         errors will be logged. Using this with debug=true adds line numbers to | ||||
|         any error logs. | ||||
| 
 | ||||
|     mode                   default: "influx" | ||||
|         * Value: influx | ||||
|         This default mode runs this application as a daemon. It will poll | ||||
|         the controller at the configured interval and report measurements to | ||||
|         InfluxDB. Providing an invalid value will run in this default mode. | ||||
| 
 | ||||
|         * Value: influxlambda | ||||
|         Setting this value will invoke a run-once mode where the application | ||||
|         immediately polls the controller and reports the metrics to InfluxDB. | ||||
|         Then it exits. This mode is useful in an AWS Lambda or a crontab where | ||||
|         the execution timings are controlled. This mode may also be adapted | ||||
|         to run in other collector scripts and apps like telegraf or diamond. | ||||
|         This mode can also be combined with a "test database" in InfluxDB to | ||||
|         give yourself a "test config file" you may run ad-hoc to test changes. | ||||
| 
 | ||||
|         * Value: prometheus | ||||
|         In this mode the application opens an http interface and exports the | ||||
|         measurements at /metrics for collection by prometheus. Enabling this | ||||
|         mode disables InfluxDB usage entirely. | ||||
| 
 | ||||
|         * Value: both | ||||
|         Setting the mode to "both" will cause the InfluxDB poller routine to run | ||||
|         along with the Prometheus exporter. You can run both at the same time. | ||||
| 
 | ||||
|     http_listen            default: 0.0.0.0:9130 | ||||
|         This option controls the IP and port the http listener uses when the | ||||
|         mode is set to prometheus. This setting has no effect when other modes | ||||
|         are in use. Metrics become available at the /metrics URI. | ||||
| 
 | ||||
|     influx_url             default: http://127.0.0.1:8086 | ||||
|         This is the URL where the Influx web server is available. | ||||
| 
 | ||||
|     influx_user            default: unifi | ||||
|         Username used to authenticate with InfluxDB. | ||||
| 
 | ||||
|     influx_pass            default: unifi | ||||
|         Password used to authenticate with InfluxDB. | ||||
| 
 | ||||
|     influx_db              default: unifi | ||||
|         Custom database created in InfluxDB to use with this application. | ||||
|         On first setup, log into InfluxDB and create access: | ||||
|         $ influx -host localhost -port 8086 | ||||
|         CREATE DATABASE unifi | ||||
|         CREATE USER unifi WITH PASSWORD 'unifi' WITH ALL PRIVILEGES | ||||
|         GRANT ALL ON unifi TO unifi | ||||
| 
 | ||||
|     influx_insecure_ssl    default: false | ||||
|         Setting this to true will allow use of InfluxDB with an invalid SSL certificate. | ||||
| 
 | ||||
|       >>> CONTROLLER FIELDS FOLLOW - you may have multiple controllers: | ||||
|     >>> CONTROLLER FIELDS FOLLOW - you may have multiple controllers: | ||||
| 
 | ||||
|     sites                  default: ["all"] | ||||
|         This list of strings should represent the names of sites on the UniFi | ||||
|  |  | |||
|  | @ -3,11 +3,7 @@ | |||
| ######################################################## | ||||
| 
 | ||||
| 
 | ||||
| # The UniFi Controller only updates traffic stats about every 30 seconds. | ||||
| # Setting this to something lower may lead to "zeros" in your data. | ||||
| # If you're getting zeros now, set this to "1m" | ||||
| interval = "30s" | ||||
| 
 | ||||
| [poller] | ||||
| # Turns on line numbers, microsecond logging, and a per-device log. | ||||
| # The default is false, but I personally leave this on at home (four devices). | ||||
| # This may be noisy if you have a lot of devices. It adds one line per device. | ||||
|  | @ -17,38 +13,34 @@ debug = false | |||
| # Recommend enabling debug with this setting for better error logging. | ||||
| quiet = false | ||||
| 
 | ||||
| # Which mode to run this application in. The default mode is "influx". Providing | ||||
| # an invalid mode will also result in "influx". In this default mode the application | ||||
| # runs as a daemon and polls the controller at the configured interval. | ||||
| # | ||||
| # Other options: "influxlambda",  "prometheus", "both" | ||||
| # | ||||
| # Mode "influxlambda" makes the application exit after collecting and reporting metrics | ||||
| # to InfluxDB one time. This mode requires an external process like an AWS Lambda | ||||
| # or a simple crontab to keep the timings accurate on UniFi Poller run intervals. | ||||
| # | ||||
| # Mode "prometheus" opens an HTTP server on port 9130 and exports the metrics at | ||||
| # /metrics for polling collection by a prometheus server. This disables influxdb. | ||||
| # | ||||
| # Mode "both" runs the Prometheus HTTP server and InfluxDB poller interval at | ||||
| # the same time. | ||||
| mode = "influx" | ||||
| 
 | ||||
| #### OUTPUTS | ||||
| 
 | ||||
| [prometheus] | ||||
| disable = false | ||||
| # This controls on which ip and port /metrics is exported when mode is "prometheus". | ||||
| # This has no effect in other modes. Must contain a colon and port. | ||||
| http_listen = "0.0.0.0:9130" | ||||
| report_errors = false | ||||
| 
 | ||||
| [influxdb] | ||||
| disable = false | ||||
| # InfluxDB does not require auth by default, so the user/password are probably unimportant. | ||||
| influx_url = "http://127.0.0.1:8086" | ||||
| influx_user = "unifi" | ||||
| influx_pass = "unifi" | ||||
| url = "http://127.0.0.1:8086" | ||||
| user = "unifi" | ||||
| pass = "unifi" | ||||
| # Be sure to create this database. | ||||
| influx_db = "unifi" | ||||
| # If your InfluxDB uses an invalid SSL cert, set this to true. | ||||
| influx_insecure_ssl = false | ||||
| db = "unifi" | ||||
| # If your InfluxDB uses a valid SSL cert, set this to true. | ||||
| verify_ssl = false | ||||
| # The UniFi Controller only updates traffic stats about every 30 seconds. | ||||
| # Setting this to something lower may lead to "zeros" in your data. | ||||
| # If you're getting zeros now, set this to "1m" | ||||
| interval = "30s" | ||||
| 
 | ||||
| #### INPUTS | ||||
| 
 | ||||
| # You may repeat the following section to poll additional controllers. | ||||
| 
 | ||||
| [[controller]] | ||||
| # Friendly name used in dashboards. | ||||
| name = "" | ||||
|  |  | |||
|  | @ -1,22 +1,33 @@ | |||
| { | ||||
|  "interval": "30s", | ||||
|  "debug": false, | ||||
|  "quiet": false, | ||||
|  "mode": "influx", | ||||
|  "http_listen": "0.0.0.0:9130", | ||||
|  "influx_url": "http://127.0.0.1:8086", | ||||
|  "influx_user": "unifi", | ||||
|  "influx_pass": "unifi", | ||||
|  "influx_db": "unifi", | ||||
|  "influx_insecure_ssl": false, | ||||
|  "controller": [{ | ||||
|    "name": "", | ||||
|    "user": "influx", | ||||
|    "pass": "", | ||||
|    "url": "https://127.0.0.1:8443", | ||||
|    "sites": ["all"], | ||||
|    "save_ids": false, | ||||
|    "save_sites": true, | ||||
|    "verify_ssl": false | ||||
|   "poller": { | ||||
|     "debug": false, | ||||
|     "quiet": false | ||||
|   }, | ||||
| 
 | ||||
|   "prometheus": { | ||||
|     "disable": false, | ||||
|     "http_listen": "0.0.0.0:9130", | ||||
|     "report_errors": false | ||||
|   }, | ||||
| 
 | ||||
|   "influxdb": { | ||||
|      "disable": false, | ||||
|      "url": "http://127.0.0.1:8086", | ||||
|      "user": "unifi", | ||||
|      "pass": "unifi", | ||||
|      "db": "unifi", | ||||
|      "verify_ssl": false, | ||||
|      "interval": "30s" | ||||
|   }, | ||||
| 
 | ||||
|   "controller": [{ | ||||
|      "name": "", | ||||
|      "user": "influx", | ||||
|      "pass": "", | ||||
|      "url": "https://127.0.0.1:8443", | ||||
|      "sites": ["all"], | ||||
|      "save_ids": false, | ||||
|      "save_sites": true, | ||||
|      "verify_ssl": false | ||||
|   }] | ||||
| } | ||||
|  |  | |||
|  | @ -5,21 +5,23 @@ | |||
| # provided values are defaults. See up.conf.example!  # | ||||
| ####################################################### | ||||
| --> | ||||
| <unifi-poller> | ||||
| <poller debug="false" quiet="false"> | ||||
| 
 | ||||
|   <interval>60s</interval> | ||||
|   <prometheus disable="false"> | ||||
|     <http_listen>0.0.0.0:9130</http_listen> | ||||
|     <report_errors>false</report_errors> | ||||
|   </prometheus> | ||||
| 
 | ||||
|   <debug>false</debug> | ||||
|   <quiet>false</quiet> | ||||
|   <influxdb disable="false"> | ||||
|     <interval>30s</interval> | ||||
|     <url>http://127.0.0.1:8086</url> | ||||
|     <user>unifi</user> | ||||
|     <pass>unifi</pass> | ||||
|     <db>unifi</db> | ||||
|     <verify_ssl>false</verify_ssl> | ||||
|   </influxdb> | ||||
| 
 | ||||
|   <mode>influx</mode> | ||||
|   <http_listen>0.0.0.0:9130</http_listen> | ||||
| 
 | ||||
|   <influx_db>unifi</influx_db> | ||||
|   <influx_pass>unifi</influx_pass> | ||||
|   <influx_url>http://127.0.0.1:8086</influx_url> | ||||
|   <influx_user>unifi</influx_user> | ||||
|   <influx_insecure_ssl>false</influx_insecure_ssl> | ||||
|   <!-- Repeat this stanza to poll additional controllers. --> | ||||
|   <controller name=""> | ||||
|     <sites>all</sites> | ||||
|     <user>influx</user> | ||||
|  | @ -29,4 +31,5 @@ | |||
|     <save_ids>false</save_ids> | ||||
|     <save_sites>true</save_sites> | ||||
|   </controller> | ||||
| </unifi-poller> | ||||
| 
 | ||||
| </poller> | ||||
|  |  | |||
|  | @ -3,19 +3,24 @@ | |||
| # provided values are defaults. See up.conf.example!   # | ||||
| ######################################################## | ||||
| --- | ||||
| interval: "30s" | ||||
| 
 | ||||
| debug: false | ||||
| quiet: false | ||||
| poller: | ||||
|   debug: false | ||||
|   quiet: false | ||||
| 
 | ||||
| mode: "influx" | ||||
| http_listen: "0.0.0.0:9130" | ||||
| prometheus: | ||||
|   disable: false | ||||
|   http_listen: "0.0.0.0:9130" | ||||
|   report_errors: false | ||||
| 
 | ||||
| influx_url: "http://127.0.0.1:8086" | ||||
| influx_user: "unifi" | ||||
| influx_pass: "unifi" | ||||
| influx_db: "unifi" | ||||
| influx_insecure_ssl: false | ||||
| influxdb: | ||||
|   disable: false | ||||
|   interval: "30s" | ||||
|   url:  "http://127.0.0.1:8086" | ||||
|   user: "unifi" | ||||
|   pass: "unifi" | ||||
|   db:   "unifi" | ||||
|   verify_ssl: false | ||||
| 
 | ||||
| controller: | ||||
|   - name: "" | ||||
|  |  | |||
							
								
								
									
										3
									
								
								main.go
								
								
								
								
							
							
						
						
									
										3
									
								
								main.go
								
								
								
								
							|  | @ -4,6 +4,9 @@ import ( | |||
| 	"log" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	// Enable output plugins!
 | ||||
| 	_ "github.com/davidnewhall/unifi-poller/pkg/influxunifi" | ||||
| 	_ "github.com/davidnewhall/unifi-poller/pkg/promunifi" | ||||
| ) | ||||
| 
 | ||||
| // Keep it simple.
 | ||||
|  |  | |||
|  | @ -1,29 +1,47 @@ | |||
| // Package influx provides the methods to turn UniFi measurements into influx
 | ||||
| // 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/metrics" | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	influx "github.com/influxdata/influxdb1-client/v2" | ||||
| 	conf "golift.io/config" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	defaultInterval   = 30 * time.Second | ||||
| 	defaultInfluxDB   = "unifi" | ||||
| 	defaultInfluxUser = "unifi" | ||||
| 	defaultInfluxURL  = "http://127.0.0.1:8086" | ||||
| ) | ||||
| 
 | ||||
| // Config defines the data needed to store metrics in InfluxDB
 | ||||
| type Config struct { | ||||
| 	Database string | ||||
| 	URL      string | ||||
| 	User     string | ||||
| 	Pass     string | ||||
| 	BadSSL   bool | ||||
| 	Interval  conf.Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"` | ||||
| 	Disable   bool          `json:"disable" toml:"disable" xml:"disable" 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 { | ||||
| 	cf     *Config | ||||
| 	influx influx.Client | ||||
| 	Collector poller.Collect | ||||
| 	influx    influx.Client | ||||
| 	LastCheck time.Time | ||||
| 	*InfluxDB | ||||
| } | ||||
| 
 | ||||
| type metric struct { | ||||
|  | @ -32,26 +50,101 @@ type metric struct { | |||
| 	Fields map[string]interface{} | ||||
| } | ||||
| 
 | ||||
| // New returns an InfluxDB interface.
 | ||||
| func New(c *Config) (*InfluxUnifi, error) { | ||||
| 	i, err := influx.NewHTTPClient(influx.HTTPConfig{ | ||||
| 		Addr:      c.URL, | ||||
| 		Username:  c.User, | ||||
| 		Password:  c.Pass, | ||||
| 		TLSConfig: &tls.Config{InsecureSkipVerify: c.BadSSL}, | ||||
| func init() { | ||||
| 	u := &InfluxUnifi{InfluxDB: &InfluxDB{}, LastCheck: time.Now()} | ||||
| 	poller.NewOutput(&poller.Output{ | ||||
| 		Name:   "influxdb", | ||||
| 		Config: u.InfluxDB, | ||||
| 		Method: u.Run, | ||||
| 	}) | ||||
| 	return &InfluxUnifi{cf: c, influx: i}, err | ||||
| } | ||||
| 
 | ||||
| // 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) | ||||
| 	log.Printf("[INFO] Everything checks out! Poller started, InfluxDB interval: %v", interval) | ||||
| 
 | ||||
| 	ticker := time.NewTicker(interval) | ||||
| 	for u.LastCheck = range ticker.C { | ||||
| 		metrics, err := u.Collector.Metrics() | ||||
| 		if err != nil { | ||||
| 			u.Collector.LogErrorf("%v", err) | ||||
| 			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 = conf.Duration{Duration: defaultInterval} | ||||
| 	} else if u.Config.Interval.Duration < defaultInterval/2 { | ||||
| 		u.Config.Interval = conf.Duration{Duration: defaultInterval / 2} | ||||
| 	} | ||||
| 
 | ||||
| 	u.Config.Interval = conf.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 *metrics.Metrics) (*Report, error) { | ||||
| func (u *InfluxUnifi) ReportMetrics(m *poller.Metrics) (*Report, error) { | ||||
| 	r := &Report{Metrics: m, ch: make(chan *metric), Start: time.Now()} | ||||
| 	defer close(r.ch) | ||||
| 	// Make a new Influx Points Batcher.
 | ||||
| 	var err error | ||||
| 	r.bp, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.cf.Database}) | ||||
| 	r.bp, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.Config.DB}) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("influx.NewBatchPoints: %v", err) | ||||
| 	} | ||||
|  | @ -140,3 +233,13 @@ func (u *InfluxUnifi) loopPoints(r report) { | |||
| 		} | ||||
| 	}() | ||||
| } | ||||
| 
 | ||||
| // 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)) | ||||
| } | ||||
|  |  | |||
|  | @ -4,13 +4,13 @@ import ( | |||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/metrics" | ||||
| 	"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 *metrics.Metrics | ||||
| 	Metrics *poller.Metrics | ||||
| 	Errors  []error | ||||
| 	Total   int | ||||
| 	Fields  int | ||||
|  | @ -28,10 +28,10 @@ type report interface { | |||
| 	send(m *metric) | ||||
| 	error(err error) | ||||
| 	batch(m *metric, pt *influx.Point) | ||||
| 	metrics() *metrics.Metrics | ||||
| 	metrics() *poller.Metrics | ||||
| } | ||||
| 
 | ||||
| func (r *Report) metrics() *metrics.Metrics { | ||||
| func (r *Report) metrics() *poller.Metrics { | ||||
| 	return r.Metrics | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,16 +0,0 @@ | |||
| package metrics | ||||
| 
 | ||||
| import ( | ||||
| 	"time" | ||||
| 
 | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // Metrics is a type shared by the exporting and reporting packages.
 | ||||
| type Metrics struct { | ||||
| 	TS time.Time | ||||
| 	unifi.Sites | ||||
| 	unifi.IDSList | ||||
| 	unifi.Clients | ||||
| 	*unifi.Devices | ||||
| } | ||||
|  | @ -12,26 +12,17 @@ import ( | |||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/influxunifi" | ||||
| 	"github.com/spf13/pflag" | ||||
| 	"golift.io/config" | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // Version is injected by the Makefile
 | ||||
| var Version = "development" | ||||
| 
 | ||||
| // App defaults in case they're missing from the config.
 | ||||
| const ( | ||||
| 	// App defaults in case they're missing from the config.
 | ||||
| 	appName           = "unifi-poller" | ||||
| 	defaultInterval   = 30 * time.Second | ||||
| 	defaultInfluxDB   = "unifi" | ||||
| 	defaultInfluxUser = "unifi" | ||||
| 	defaultInfluxPass = "unifi" | ||||
| 	defaultInfluxURL  = "http://127.0.0.1:8086" | ||||
| 	defaultUnifiUser  = "influx" | ||||
| 	defaultUnifiURL   = "https://127.0.0.1:8443" | ||||
| 	defaultHTTPListen = "0.0.0.0:9130" | ||||
| 	// AppName is the name of the application.
 | ||||
| 	AppName          = "unifi-poller" | ||||
| 	defaultUnifiUser = "influx" | ||||
| 	defaultUnifiURL  = "https://127.0.0.1:8443" | ||||
| ) | ||||
| 
 | ||||
| // ENVConfigPrefix is the prefix appended to an env variable tag
 | ||||
|  | @ -40,10 +31,8 @@ const ENVConfigPrefix = "UP" | |||
| 
 | ||||
| // UnifiPoller contains the application startup data, and auth info for UniFi & Influx.
 | ||||
| type UnifiPoller struct { | ||||
| 	Influx     *influxunifi.InfluxUnifi | ||||
| 	Flag       *Flag | ||||
| 	Config     *Config | ||||
| 	LastCheck  time.Time | ||||
| 	sync.Mutex // locks the Unifi struct member when re-authing to unifi.
 | ||||
| } | ||||
| 
 | ||||
|  | @ -55,6 +44,15 @@ type Flag struct { | |||
| 	*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 | ||||
| } | ||||
| 
 | ||||
| // Controller represents the configuration for a UniFi Controller.
 | ||||
| // Each polled controller may have its own configuration.
 | ||||
| type Controller struct { | ||||
|  | @ -73,16 +71,43 @@ type Controller struct { | |||
| // This is all of the data stored in the config file.
 | ||||
| // Any with explicit defaults have omitempty on json and toml tags.
 | ||||
| type Config struct { | ||||
| 	Interval    config.Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval" yaml:"interval"` | ||||
| 	Debug       bool            `json:"debug" toml:"debug" xml:"debug" yaml:"debug"` | ||||
| 	Quiet       bool            `json:"quiet,omitempty" toml:"quiet,omitempty" xml:"quiet" yaml:"quiet"` | ||||
| 	InfxBadSSL  bool            `json:"influx_insecure_ssl" toml:"influx_insecure_ssl" xml:"influx_insecure_ssl" yaml:"influx_insecure_ssl"` | ||||
| 	Mode        string          `json:"mode" toml:"mode" xml:"mode" yaml:"mode"` | ||||
| 	HTTPListen  string          `json:"http_listen" toml:"http_listen" xml:"http_listen" yaml:"http_listen"` | ||||
| 	Namespace   string          `json:"namespace" toml:"namespace" xml:"namespace" yaml:"namespace"` | ||||
| 	InfluxURL   string          `json:"influx_url,omitempty" toml:"influx_url,omitempty" xml:"influx_url" yaml:"influx_url"` | ||||
| 	InfluxUser  string          `json:"influx_user,omitempty" toml:"influx_user,omitempty" xml:"influx_user" yaml:"influx_user"` | ||||
| 	InfluxPass  string          `json:"influx_pass,omitempty" toml:"influx_pass,omitempty" xml:"influx_pass" yaml:"influx_pass"` | ||||
| 	InfluxDB    string          `json:"influx_db,omitempty" toml:"influx_db,omitempty" xml:"influx_db" yaml:"influx_db"` | ||||
| 	Controllers []Controller    `json:"controller,omitempty" toml:"controller,omitempty" xml:"controller" yaml:"controller"` | ||||
| 	Poller      `json:"poller" toml:"poller" xml:"poller" yaml:"poller"` | ||||
| 	Controllers []Controller `json:"controller,omitempty" toml:"controller,omitempty" xml:"controller" yaml:"controller"` | ||||
| } | ||||
| 
 | ||||
| // Poller is the global config values.
 | ||||
| type Poller struct { | ||||
| 	Debug bool `json:"debug" toml:"debug" xml:"debug,attr" yaml:"debug"` | ||||
| 	Quiet bool `json:"quiet,omitempty" toml:"quiet,omitempty" xml:"quiet,attr" yaml:"quiet"` | ||||
| } | ||||
| 
 | ||||
| // ParseConfigs parses the poller config and the config for each registered output plugin.
 | ||||
| func (u *UnifiPoller) ParseConfigs() error { | ||||
| 	// Parse config file.
 | ||||
| 	if err := config.ParseFile(u.Config, u.Flag.ConfigFile); err != nil { | ||||
| 		u.Flag.Usage() | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Update Config with ENV variable overrides.
 | ||||
| 	if _, err := config.ParseENV(u.Config, ENVConfigPrefix); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	outputSync.Lock() | ||||
| 	defer outputSync.Unlock() | ||||
| 
 | ||||
| 	for _, o := range outputs { | ||||
| 		// Parse config file for each output plugin.
 | ||||
| 		if err := config.ParseFile(o.Config, u.Flag.ConfigFile); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		// Update Config for each output with ENV variable overrides.
 | ||||
| 		if _, err := config.ParseENV(o.Config, ENVConfigPrefix); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  |  | |||
|  | @ -1,79 +0,0 @@ | |||
| package poller | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/influxunifi" | ||||
| ) | ||||
| 
 | ||||
| // GetInfluxDB returns an InfluxDB interface.
 | ||||
| func (u *UnifiPoller) GetInfluxDB() (err error) { | ||||
| 	if u.Influx != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	u.Influx, err = influxunifi.New(&influxunifi.Config{ | ||||
| 		Database: u.Config.InfluxDB, | ||||
| 		User:     u.Config.InfluxUser, | ||||
| 		Pass:     u.Config.InfluxPass, | ||||
| 		BadSSL:   u.Config.InfxBadSSL, | ||||
| 		URL:      u.Config.InfluxURL, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("influxdb: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	u.Logf("Logging Measurements to InfluxDB at %s as user %s", u.Config.InfluxURL, u.Config.InfluxUser) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // PollController runs forever, polling UniFi and pushing to InfluxDB
 | ||||
| // This is started by Run() or RunBoth() after everything checks out.
 | ||||
| func (u *UnifiPoller) PollController() { | ||||
| 	interval := u.Config.Interval.Round(time.Second) | ||||
| 	log.Printf("[INFO] Everything checks out! Poller started, InfluxDB interval: %v", interval) | ||||
| 
 | ||||
| 	ticker := time.NewTicker(interval) | ||||
| 	for u.LastCheck = range ticker.C { | ||||
| 		if err := u.CollectAndProcess(); err != nil { | ||||
| 			u.LogErrorf("%v", err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // CollectAndProcess collects measurements and then reports them to InfluxDB
 | ||||
| // Can be called once or in a ticker loop. This function and all the ones below
 | ||||
| // handle their own logging. An error is returned so the calling function may
 | ||||
| // determine if there was a read or write error and act on it. This is currently
 | ||||
| // called in two places in this library. One returns an error, one does not.
 | ||||
| func (u *UnifiPoller) CollectAndProcess() error { | ||||
| 	if err := u.GetInfluxDB(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	metrics, err := u.CollectMetrics() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	report, err := u.Influx.ReportMetrics(metrics) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	u.LogInfluxReport(report) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // LogInfluxReport writes a log message after exporting to influxdb.
 | ||||
| func (u *UnifiPoller) LogInfluxReport(r *influxunifi.Report) { | ||||
| 	idsMsg := fmt.Sprintf("IDS Events: %d, ", len(r.Metrics.IDSList)) | ||||
| 	u.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)) | ||||
| } | ||||
|  | @ -0,0 +1,72 @@ | |||
| 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, error) | ||||
| 	Logf(m string, v ...interface{}) | ||||
| 	LogErrorf(m string, v ...interface{}) | ||||
| 	LogDebugf(m string, v ...interface{}) | ||||
| } | ||||
| 
 | ||||
| // 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 configured") | ||||
| 	} | ||||
| 
 | ||||
| 	for err := range v { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		if count--; count < 1 { | ||||
| 			return fmt.Errorf("all output plugins have stopped") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
|  | @ -1,53 +0,0 @@ | |||
| package poller | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/metrics" | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/promunifi" | ||||
| 	"github.com/prometheus/client_golang/prometheus" | ||||
| 	"github.com/prometheus/client_golang/prometheus/promhttp" | ||||
| 	"github.com/prometheus/common/version" | ||||
| ) | ||||
| 
 | ||||
| const oneDecimalPoint = 10 | ||||
| 
 | ||||
| // RunPrometheus starts the web server and registers the collector.
 | ||||
| func (u *UnifiPoller) RunPrometheus() error { | ||||
| 	u.Logf("Exporting Measurements for Prometheus at https://%s/metrics", u.Config.HTTPListen) | ||||
| 	http.Handle("/metrics", promhttp.Handler()) | ||||
| 	ns := strings.Replace(u.Config.Namespace, "-", "", -1) | ||||
| 	prometheus.MustRegister(promunifi.NewUnifiCollector(promunifi.UnifiCollectorCnfg{ | ||||
| 		Namespace:    ns, | ||||
| 		CollectFn:    u.ExportMetrics, | ||||
| 		LoggingFn:    u.LogExportReport, | ||||
| 		ReportErrors: true, // XXX: Does this need to be configurable?
 | ||||
| 	})) | ||||
| 
 | ||||
| 	version.Version = Version | ||||
| 	prometheus.MustRegister(version.NewCollector(ns)) | ||||
| 
 | ||||
| 	return http.ListenAndServe(u.Config.HTTPListen, nil) | ||||
| } | ||||
| 
 | ||||
| // ExportMetrics updates the internal metrics provided via
 | ||||
| // HTTP at /metrics for prometheus collection.
 | ||||
| // This is run by Prometheus as CollectFn.
 | ||||
| func (u *UnifiPoller) ExportMetrics() (*metrics.Metrics, error) { | ||||
| 	return u.CollectMetrics() | ||||
| } | ||||
| 
 | ||||
| // LogExportReport is called after prometheus exports metrics.
 | ||||
| // This is run by Prometheus as LoggingFn
 | ||||
| func (u *UnifiPoller) LogExportReport(report *promunifi.Report) { | ||||
| 	m := report.Metrics | ||||
| 	u.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), | ||||
| 		report.Descs, report.Total, report.Errors, report.Zeros, | ||||
| 		report.Fetch.Round(time.Millisecond/oneDecimalPoint), | ||||
| 		report.Elapsed.Round(time.Millisecond/oneDecimalPoint)) | ||||
| } | ||||
|  | @ -5,26 +5,16 @@ import ( | |||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/github/hub/version" | ||||
| 	"github.com/spf13/pflag" | ||||
| 	"golift.io/config" | ||||
| ) | ||||
| 
 | ||||
| // New returns a new poller struct preloaded with default values.
 | ||||
| // No need to call this if you call Start.c
 | ||||
| func New() *UnifiPoller { | ||||
| 	return &UnifiPoller{ | ||||
| 		Config: &Config{ | ||||
| 			InfluxURL:  defaultInfluxURL, | ||||
| 			InfluxUser: defaultInfluxUser, | ||||
| 			InfluxPass: defaultInfluxPass, | ||||
| 			InfluxDB:   defaultInfluxDB, | ||||
| 			Interval:   config.Duration{Duration: defaultInterval}, | ||||
| 			HTTPListen: defaultHTTPListen, | ||||
| 			Namespace:  appName, | ||||
| 		}, | ||||
| 		Config: &Config{}, | ||||
| 		Flag: &Flag{ | ||||
| 			ConfigFile: DefaultConfFile, | ||||
| 		}, | ||||
|  | @ -40,7 +30,7 @@ func (u *UnifiPoller) Start() error { | |||
| 	u.Flag.Parse(os.Args[1:]) | ||||
| 
 | ||||
| 	if u.Flag.ShowVer { | ||||
| 		fmt.Printf("%s v%s\n", appName, Version) | ||||
| 		fmt.Printf("%s v%s\n", AppName, version.Version) | ||||
| 		return nil // don't run anything else w/ version request.
 | ||||
| 	} | ||||
| 
 | ||||
|  | @ -48,14 +38,8 @@ func (u *UnifiPoller) Start() error { | |||
| 		u.Logf("Loading Configuration File: %s", u.Flag.ConfigFile) | ||||
| 	} | ||||
| 
 | ||||
| 	// Parse config file.
 | ||||
| 	if err := config.ParseFile(u.Config, u.Flag.ConfigFile); err != nil { | ||||
| 		u.Flag.Usage() | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Update Config with ENV variable overrides.
 | ||||
| 	if _, err := config.ParseENV(u.Config, ENVConfigPrefix); err != nil { | ||||
| 	// Parse config file and ENV variables.
 | ||||
| 	if err := u.ParseConfigs(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
|  | @ -78,16 +62,14 @@ func (u *UnifiPoller) Start() error { | |||
| 		u.LogDebugf("Debug Logging Enabled") | ||||
| 	} | ||||
| 
 | ||||
| 	log.Printf("[INFO] UniFi Poller v%v Starting Up! PID: %d", Version, os.Getpid()) | ||||
| 
 | ||||
| 	return u.Run() | ||||
| } | ||||
| 
 | ||||
| // Parse turns CLI arguments into data structures. Called by Start() on startup.
 | ||||
| func (f *Flag) Parse(args []string) { | ||||
| 	f.FlagSet = pflag.NewFlagSet(appName, pflag.ExitOnError) | ||||
| 	f.FlagSet = pflag.NewFlagSet(AppName, pflag.ExitOnError) | ||||
| 	f.Usage = func() { | ||||
| 		fmt.Printf("Usage: %s [--config=/path/to/up.conf] [--version]", appName) | ||||
| 		fmt.Printf("Usage: %s [--config=/path/to/up.conf] [--version]", AppName) | ||||
| 		f.PrintDefaults() | ||||
| 	} | ||||
| 
 | ||||
|  | @ -103,6 +85,8 @@ func (f *Flag) Parse(args []string) { | |||
| // 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 { | ||||
| 	log.Printf("[INFO] UniFi Poller v%v Starting Up! PID: %d", version.Version, os.Getpid()) | ||||
| 
 | ||||
| 	for i, c := range u.Config.Controllers { | ||||
| 		if c.Name == "" { | ||||
| 			u.Config.Controllers[i].Name = c.URL | ||||
|  | @ -117,17 +101,5 @@ func (u *UnifiPoller) Run() error { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	switch strings.ToLower(u.Config.Mode) { | ||||
| 	default: | ||||
| 		u.PollController() | ||||
| 		return nil | ||||
| 	case "influxlambda", "lambdainflux", "lambda_influx", "influx_lambda": | ||||
| 		u.LastCheck = time.Now() | ||||
| 		return u.CollectAndProcess() | ||||
| 	case "both": | ||||
| 		go u.PollController() | ||||
| 		fallthrough | ||||
| 	case "prometheus", "exporter": | ||||
| 		return u.RunPrometheus() | ||||
| 	} | ||||
| 	return u.InitializeOutputs() | ||||
| } | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ import ( | |||
| 	"strings" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/metrics" | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
|  | @ -42,10 +41,6 @@ func (u *UnifiPoller) GetUnifi(c Controller) error { | |||
| // CheckSites makes sure the list of provided sites exists on the controller.
 | ||||
| // This does not run in Lambda (run-once) mode.
 | ||||
| func (u *UnifiPoller) CheckSites(c Controller) error { | ||||
| 	if strings.Contains(strings.ToLower(u.Config.Mode), "lambda") { | ||||
| 		return nil // Skip this in lambda mode.
 | ||||
| 	} | ||||
| 
 | ||||
| 	u.LogDebugf("Checking Controller Sites List") | ||||
| 
 | ||||
| 	sites, err := c.Unifi.GetSites() | ||||
|  | @ -58,6 +53,7 @@ func (u *UnifiPoller) CheckSites(c Controller) error { | |||
| 	for _, site := range sites { | ||||
| 		msg = append(msg, site.Name+" ("+site.Desc+")") | ||||
| 	} | ||||
| 
 | ||||
| 	u.Logf("Found %d site(s) on controller: %v", len(msg), strings.Join(msg, ", ")) | ||||
| 
 | ||||
| 	if StringInSlice("all", c.Sites) { | ||||
|  | @ -78,10 +74,10 @@ FIRST: | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // CollectMetrics grabs all the measurements from a UniFi controller and returns them.
 | ||||
| func (u *UnifiPoller) CollectMetrics() (*metrics.Metrics, error) { | ||||
| // Metrics grabs all the measurements from a UniFi controller and returns them.
 | ||||
| func (u *UnifiPoller) Metrics() (*Metrics, error) { | ||||
| 	errs := []string{} | ||||
| 	metrics := &metrics.Metrics{} | ||||
| 	metrics := &Metrics{} | ||||
| 
 | ||||
| 	for _, c := range u.Config.Controllers { | ||||
| 		m, err := u.checkAndPollController(c) | ||||
|  | @ -120,7 +116,7 @@ func (u *UnifiPoller) CollectMetrics() (*metrics.Metrics, error) { | |||
| 	return metrics, err | ||||
| } | ||||
| 
 | ||||
| func (u *UnifiPoller) checkAndPollController(c Controller) (*metrics.Metrics, error) { | ||||
| func (u *UnifiPoller) checkAndPollController(c Controller) (*Metrics, error) { | ||||
| 	if c.Unifi == nil { | ||||
| 		u.Logf("Re-authenticating to UniFi Controller: %s", c.URL) | ||||
| 
 | ||||
|  | @ -146,10 +142,10 @@ func (u *UnifiPoller) checkAndPollController(c Controller) (*metrics.Metrics, er | |||
| 	return u.collectController(c) | ||||
| } | ||||
| 
 | ||||
| func (u *UnifiPoller) collectController(c Controller) (*metrics.Metrics, error) { | ||||
| func (u *UnifiPoller) collectController(c Controller) (*Metrics, error) { | ||||
| 	var err error | ||||
| 
 | ||||
| 	m := &metrics.Metrics{TS: u.LastCheck} // At this point, it's the Current Check.
 | ||||
| 	m := &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 { | ||||
|  | @ -178,7 +174,7 @@ func (u *UnifiPoller) collectController(c Controller) (*metrics.Metrics, error) | |||
| // augmentMetrics is our middleware layer between collecting metrics and writing them.
 | ||||
| // This is where we can manipuate the returned data or make arbitrary decisions.
 | ||||
| // This function currently adds parent device names to client metrics.
 | ||||
| func (u *UnifiPoller) augmentMetrics(c Controller, metrics *metrics.Metrics) *metrics.Metrics { | ||||
| func (u *UnifiPoller) augmentMetrics(c Controller, metrics *Metrics) *Metrics { | ||||
| 	if metrics == nil || metrics.Devices == nil || metrics.Clients == nil { | ||||
| 		return metrics | ||||
| 	} | ||||
|  |  | |||
|  | @ -1,50 +1,59 @@ | |||
| // Package promunifi provides the bridge between unifi metrics and prometheus.
 | ||||
| // 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/metrics" | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	"github.com/prometheus/client_golang/prometheus" | ||||
| 	"github.com/prometheus/common/version" | ||||
| 	"golift.io/unifi" | ||||
| ) | ||||
| 
 | ||||
| // channel buffer, fits at least one batch.
 | ||||
| const buffer = 50 | ||||
| 
 | ||||
| // simply fewer letters.
 | ||||
| const counter = prometheus.CounterValue | ||||
| const gauge = prometheus.GaugeValue | ||||
| 
 | ||||
| // UnifiCollectorCnfg defines the data needed to collect and report UniFi Metrics.
 | ||||
| type UnifiCollectorCnfg struct { | ||||
| 	// If non-empty, each of the collected metrics is prefixed by the
 | ||||
| 	// provided string and an underscore ("_").
 | ||||
| 	Namespace string | ||||
| 	// If true, any error encountered during collection is reported as an
 | ||||
| 	// invalid metric (see NewInvalidMetric). Otherwise, errors are ignored
 | ||||
| 	// and the collected metrics will be incomplete. Possibly, no metrics
 | ||||
| 	// will be collected at all.
 | ||||
| 	ReportErrors bool | ||||
| 	// This function is passed to the Collect() method. The Collect method runs
 | ||||
| 	// this function to retrieve the latest UniFi measurements and export them.
 | ||||
| 	CollectFn func() (*metrics.Metrics, error) | ||||
| 	// Provide a logger function if you want to run a routine *after* prometheus checks in.
 | ||||
| 	LoggingFn func(*Report) | ||||
| } | ||||
| const ( | ||||
| 	// channel buffer, fits at least one batch.
 | ||||
| 	buffer            = 50 | ||||
| 	defaultHTTPListen = "0.0.0.0:9130" | ||||
| 	// simply fewer letters.
 | ||||
| 	counter = prometheus.CounterValue | ||||
| 	gauge   = prometheus.GaugeValue | ||||
| ) | ||||
| 
 | ||||
| type promUnifi struct { | ||||
| 	Config UnifiCollectorCnfg | ||||
| 	*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 | ||||
| } | ||||
| 
 | ||||
| // Prometheus allows the data to be nested in the config file.
 | ||||
| type Prometheus struct { | ||||
| 	Config Config `json:"prometheus" toml:"prometheus" xml:"prometheus" yaml:"prometheus"` | ||||
| } | ||||
| 
 | ||||
| // Config is the input (config file) data used to initialize this output plugin.
 | ||||
| type Config struct { | ||||
| 	Disable bool `json:"disable" toml:"disable" xml:"disable" yaml:"disable"` | ||||
| 	// 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"` | ||||
| 	// 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"` | ||||
| 	HTTPListen   string `json:"http_listen" toml:"http_listen" xml:"http_listen" yaml:"http_listen"` | ||||
| } | ||||
| 
 | ||||
| type metric struct { | ||||
|  | @ -54,41 +63,64 @@ type metric struct { | |||
| 	Labels    []string | ||||
| } | ||||
| 
 | ||||
| // Report is passed into LoggingFn to log the export metrics to stdout (outside this package).
 | ||||
| // Report accumulates counters that are printed to a log line.
 | ||||
| type Report struct { | ||||
| 	Total   int              // Total count of metrics recorded.
 | ||||
| 	Errors  int              // Total count of errors recording metrics.
 | ||||
| 	Zeros   int              // Total count of metrics equal to zero.
 | ||||
| 	Descs   int              // Total count of unique metrics descriptions.
 | ||||
| 	Metrics *metrics.Metrics // Metrics collected and recorded.
 | ||||
| 	Elapsed time.Duration    // Duration elapsed collecting and exporting.
 | ||||
| 	Fetch   time.Duration    // Duration elapsed making controller requests.
 | ||||
| 	Start   time.Time        // Time collection began.
 | ||||
| 	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 | ||||
| 	cf      UnifiCollectorCnfg | ||||
| 	Config | ||||
| } | ||||
| 
 | ||||
| // NewUnifiCollector returns a prometheus collector that will export any available
 | ||||
| // UniFi metrics. You must provide a collection function in the opts.
 | ||||
| func NewUnifiCollector(opts UnifiCollectorCnfg) prometheus.Collector { | ||||
| 	if opts.CollectFn == nil { | ||||
| 		panic("nil collector function") | ||||
| func init() { | ||||
| 	u := &promUnifi{Prometheus: &Prometheus{}} | ||||
| 	poller.NewOutput(&poller.Output{ | ||||
| 		Name:   "prometheus", | ||||
| 		Config: u.Prometheus, | ||||
| 		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.Config.Disable { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	if opts.Namespace = strings.Trim(opts.Namespace, "_") + "_"; opts.Namespace == "_" { | ||||
| 		opts.Namespace = "" | ||||
| 	if u.Config.Namespace == "" { | ||||
| 		u.Config.Namespace = strings.Replace(poller.AppName, "-", "", -1) | ||||
| 	} | ||||
| 
 | ||||
| 	return &promUnifi{ | ||||
| 		Config: opts, | ||||
| 		Client: descClient(opts.Namespace + "client_"), | ||||
| 		Device: descDevice(opts.Namespace + "device_"), // stats for all device types.
 | ||||
| 		UAP:    descUAP(opts.Namespace + "device_"), | ||||
| 		USG:    descUSG(opts.Namespace + "device_"), | ||||
| 		USW:    descUSW(opts.Namespace + "device_"), | ||||
| 		Site:   descSite(opts.Namespace + "site_"), | ||||
| 	if u.Config.HTTPListen == "" { | ||||
| 		u.Config.HTTPListen = defaultHTTPListen | ||||
| 	} | ||||
| 
 | ||||
| 	name := strings.Replace(u.Config.Namespace, "-", "_", -1) | ||||
| 
 | ||||
| 	ns := name | ||||
| 	if ns = strings.Trim(ns, "_") + "_"; ns == "_" { | ||||
| 		ns = "" | ||||
| 	} | ||||
| 
 | ||||
| 	prometheus.MustRegister(version.NewCollector(name)) | ||||
| 	prometheus.MustRegister(&promUnifi{ | ||||
| 		Collector: c, | ||||
| 		Client:    descClient(ns + "client_"), | ||||
| 		Device:    descDevice(ns + "device_"), // stats for all device types.
 | ||||
| 		UAP:       descUAP(ns + "device_"), | ||||
| 		USG:       descUSG(ns + "device_"), | ||||
| 		USW:       descUSW(ns + "device_"), | ||||
| 		Site:      descSite(ns + "site_"), | ||||
| 	}) | ||||
| 	c.Logf("Exporting Measurements for Prometheus at https://%s/metrics, namespace: %s", u.Config.HTTPListen, u.Config.Namespace) | ||||
| 
 | ||||
| 	return http.ListenAndServe(u.Config.HTTPListen, nil) | ||||
| } | ||||
| 
 | ||||
| // Describe satisfies the prometheus Collector. This returns all of the
 | ||||
|  | @ -112,10 +144,10 @@ func (u *promUnifi) Describe(ch chan<- *prometheus.Desc) { | |||
| func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { | ||||
| 	var err error | ||||
| 
 | ||||
| 	r := &Report{cf: u.Config, ch: make(chan []*metric, buffer), Start: time.Now()} | ||||
| 	r := &Report{Config: u.Config, ch: make(chan []*metric, buffer), Start: time.Now()} | ||||
| 	defer r.close() | ||||
| 
 | ||||
| 	if r.Metrics, err = r.cf.CollectFn(); err != nil { | ||||
| 	if r.Metrics, err = u.Collector.Metrics(); err != nil { | ||||
| 		r.error(ch, prometheus.NewInvalidDesc(fmt.Errorf("metric fetch failed")), err) | ||||
| 		return | ||||
| 	} | ||||
|  | @ -135,7 +167,7 @@ func (u *promUnifi) Collect(ch chan<- prometheus.Metric) { | |||
| // 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(descs) | ||||
| 	defer r.report(u.Collector, descs) | ||||
| 
 | ||||
| 	for newMetrics := range ourChan { | ||||
| 		for _, m := range newMetrics { | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import ( | |||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/metrics" | ||||
| 	"github.com/davidnewhall/unifi-poller/pkg/poller" | ||||
| 	"github.com/prometheus/client_golang/prometheus" | ||||
| ) | ||||
| 
 | ||||
|  | @ -16,14 +16,15 @@ type report interface { | |||
| 	add() | ||||
| 	done() | ||||
| 	send([]*metric) | ||||
| 	metrics() *metrics.Metrics | ||||
| 	report(descs map[*prometheus.Desc]bool) | ||||
| 	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) | ||||
|  | @ -38,17 +39,19 @@ func (r *Report) send(m []*metric) { | |||
| 	r.ch <- m | ||||
| } | ||||
| 
 | ||||
| func (r *Report) metrics() *metrics.Metrics { | ||||
| func (r *Report) metrics() *poller.Metrics { | ||||
| 	return r.Metrics | ||||
| } | ||||
| 
 | ||||
| func (r *Report) report(descs map[*prometheus.Desc]bool) { | ||||
| 	if r.cf.LoggingFn == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	r.Descs = len(descs) | ||||
| 	r.cf.LoggingFn(r) | ||||
| func (r *Report) 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 { | ||||
|  | @ -64,7 +67,7 @@ func (r *Report) export(m *metric, v float64) prometheus.Metric { | |||
| func (r *Report) error(ch chan<- prometheus.Metric, d *prometheus.Desc, v interface{}) { | ||||
| 	r.Errors++ | ||||
| 
 | ||||
| 	if r.cf.ReportErrors { | ||||
| 	if r.Config.ReportErrors { | ||||
| 		ch <- prometheus.NewInvalidMetric(d, fmt.Errorf("error: %v", v)) | ||||
| 	} | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue