Merge pull request #42 from davidnewhall/dn2_site_metrics

Add site metrics to influxdb.
This commit is contained in:
David Newhall II 2019-06-14 22:01:51 -07:00 committed by GitHub
commit 80e8cd6115
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 108 additions and 51 deletions

6
Gopkg.lock generated
View File

@ -2,12 +2,12 @@
[[projects]] [[projects]]
digest = "1:fda9365965d38b80007d47efbf516adbc65d266515f263e9c43336ab47ef0f69" digest = "1:f42822e830b569d8527ad6e57585e6ccc094296bc9d648ccd256f95249453ce1"
name = "github.com/golift/unifi" name = "github.com/golift/unifi"
packages = ["."] packages = ["."]
pruneopts = "UT" pruneopts = "UT"
revision = "1d74eaae61aad0558126b6ade64753ebcc5982ef" revision = "fc5a69d27d7527038ae55ee5112bc755db380879"
version = "v2.0.4" version = "v2.1.0"
[[projects]] [[projects]]
branch = "master" branch = "master"

View File

@ -3,6 +3,28 @@
Collect your Unifi Controller Data and send it to an InfluxDB instance. Collect your Unifi Controller Data and send it to an InfluxDB instance.
Grafana dashboards included. Updated 2019. Grafana dashboards included. Updated 2019.
## Description
[Ubiquiti](https://www.ui.com) makes networking devices like switches, gateways
(routers) and wireless access points. They have a line of equipment named
[UniFi](https://www.ui.com/products/#unifi) that uses a
[controller](https://www.ui.com/download/unifi/) to keep stats and simplify network
device configuration. This controller can be installed on Windows, macOS and Linux.
Ubiquiti also provides a dedicated hardware device called a
[CloudKey](https://www.ui.com/unifi/unifi-cloud-key/) that runs the controller software.
Unifi-Poller is a smaller application that runs on macOS, Linux or Docker. It
polls your controller every 30 seconds for metric data and stores that data in
an Influx Database. A small setup with 2 APs, 1 switch, 1 gateway and 40 clients
produces nearly 3000 fields (metrics).
This application requires your controller to be running all the time. If you run
a Unifi Controller, there's no excuse not to install
[Influx](https://github.com/davidnewhall/unifi-poller/wiki/InfluxDB),
[Grafana](https://github.com/davidnewhall/unifi-poller/wiki/Grafana) and this app.
You'll have a plethora of data at your fingertips and the ability to craft custom
graphs to slice the data any way you choose. Good luck!
## Installation ## Installation
[See the Wiki!](https://github.com/davidnewhall/unifi-poller/wiki/Installation) [See the Wiki!](https://github.com/davidnewhall/unifi-poller/wiki/Installation)

View File

@ -1,6 +1,7 @@
package unifipoller package unifipoller
import ( import (
"strings"
"time" "time"
"github.com/golift/unifi" "github.com/golift/unifi"
@ -39,6 +40,14 @@ type UnifiPoller struct {
*Config *Config
} }
// Metrics contains all the data from the controller.
type Metrics struct {
unifi.Sites
unifi.Clients
*unifi.Devices
influx.BatchPoints
}
// Config represents the data needed to poll a controller and report to influxdb. // Config represents the data needed to poll a controller and report to influxdb.
type Config struct { type Config struct {
Interval Dur `json:"interval,_omitempty" toml:"interval,_omitempty" xml:"interval" yaml:"interval"` Interval Dur `json:"interval,_omitempty" toml:"interval,_omitempty" xml:"interval" yaml:"interval"`
@ -60,7 +69,7 @@ type Dur struct{ value time.Duration }
// UnmarshalTOML parses a duration type from a config file. // UnmarshalTOML parses a duration type from a config file.
func (v *Dur) UnmarshalTOML(data []byte) error { func (v *Dur) UnmarshalTOML(data []byte) error {
unquoted := string(data[1 : len(data)-1]) unquoted := strings.Trim(string(data), `"`)
dur, err := time.ParseDuration(unquoted) dur, err := time.ParseDuration(unquoted)
if err == nil { if err == nil {
v.value = dur v.value = dur

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"strings"
"github.com/golift/unifi" "github.com/golift/unifi"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -24,13 +25,15 @@ func (u *UnifiPoller) DumpJSONPayload() (err error) {
fmt.Fprintf(os.Stderr, "[ERROR] "+m, v...) fmt.Fprintf(os.Stderr, "[ERROR] "+m, v...)
} // Log all errors to stderr. } // Log all errors to stderr.
switch sites, err := u.filterSites(u.Sites); { switch sites, err := u.GetFilteredSites(); {
case err != nil: case err != nil:
return err return err
case StringInSlice(u.DumpJSON, []string{"d", "device", "devices"}): case StringInSlice(u.DumpJSON, []string{"d", "device", "devices"}):
return u.DumpDeviceJSON(sites) return u.DumpDeviceJSON(sites)
case StringInSlice(u.DumpJSON, []string{"client", "clients", "c"}): case StringInSlice(u.DumpJSON, []string{"client", "clients", "c"}):
return u.DumpClientsJSON(sites) return u.DumpClientsJSON(sites)
case strings.HasPrefix(u.DumpJSON, "other "):
return u.DumpOtherJSON(sites)
default: default:
return errors.New("must provide filter: devices, clients") return errors.New("must provide filter: devices, clients")
} }
@ -58,6 +61,20 @@ func (u *UnifiPoller) DumpDeviceJSON(sites []unifi.Site) error {
return nil return nil
} }
// DumpOtherJSON prints the raw json for a user-provided path in a Unifi Controller.
func (u *UnifiPoller) DumpOtherJSON(sites []unifi.Site) error {
for _, s := range sites {
path := strings.SplitN(u.DumpJSON, " ", 2)[1]
if strings.Contains(path, "%s") {
path = fmt.Sprintf(path, s.Name)
}
if err := u.dumpJSON(path, "Other", s); err != nil {
return err
}
}
return nil
}
func (u *UnifiPoller) dumpJSON(path, what string, site unifi.Site) error { func (u *UnifiPoller) dumpJSON(path, what string, site unifi.Site) error {
req, err := u.UniReq(path, "") req, err := u.UniReq(path, "")
if err != nil { if err != nil {

View File

@ -40,95 +40,100 @@ FIRST:
func (u *UnifiPoller) PollController() { func (u *UnifiPoller) PollController() {
log.Println("[INFO] Everything checks out! Poller started, interval:", u.Interval.value) log.Println("[INFO] Everything checks out! Poller started, interval:", u.Interval.value)
ticker := time.NewTicker(u.Interval.value) ticker := time.NewTicker(u.Interval.value)
var err error
for range ticker.C { for range ticker.C {
m := &Metrics{}
// Get the sites we care about. // Get the sites we care about.
sites, err := u.filterSites(u.Sites) if m.Sites, err = u.GetFilteredSites(); err != nil {
if err != nil {
logErrors([]error{err}, "uni.GetSites()") logErrors([]error{err}, "uni.GetSites()")
} }
// Get all the points. // Get all the points.
clients, err := u.GetClients(sites) if m.Clients, err = u.GetClients(m.Sites); err != nil {
if err != nil {
logErrors([]error{err}, "uni.GetClients()") logErrors([]error{err}, "uni.GetClients()")
} }
devices, err := u.GetDevices(sites) if m.Devices, err = u.GetDevices(m.Sites); err != nil {
if err != nil {
logErrors([]error{err}, "uni.GetDevices()") logErrors([]error{err}, "uni.GetDevices()")
} }
// Make a new Points Batcher. // Make a new Points Batcher.
bp, err := influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.InfluxDB}) m.BatchPoints, err = influx.NewBatchPoints(influx.BatchPointsConfig{Database: u.InfluxDB})
if err != nil { if err != nil {
logErrors([]error{err}, "influx.NewBatchPoints") logErrors([]error{err}, "influx.NewBatchPoints")
continue continue
} }
// Batch (and send) all the points. // Batch (and send) all the points.
if errs := batchPoints(devices, clients, bp); errs != nil && hasErr(errs) { if errs := m.SendPoints(); errs != nil && hasErr(errs) {
logErrors(errs, "asset.Points()") logErrors(errs, "asset.Points()")
} }
if err := u.Write(bp); err != nil { if err := u.Write(m.BatchPoints); err != nil {
logErrors([]error{err}, "infdb.Write(bp)") logErrors([]error{err}, "infdb.Write(bp)")
} }
// Talk about the data. // Talk about the data.
var fieldcount, pointcount int var fieldcount, pointcount int
for _, p := range bp.Points() { for _, p := range m.Points() {
pointcount++ pointcount++
i, _ := p.Fields() i, _ := p.Fields()
fieldcount += len(i) fieldcount += len(i)
} }
u.Logf("Unifi Measurements Recorded. Sites: %d Clients: %d, "+ u.Logf("Unifi Measurements Recorded. Sites: %d, Clients: %d, "+
"Wireless APs: %d, Gateways: %d, Switches: %d, Points: %d, Fields: %d", "Wireless APs: %d, Gateways: %d, Switches: %d, Points: %d, Fields: %d",
len(sites), len(clients.UCLs), len(m.Sites), len(m.Clients), len(m.UAPs), len(m.USGs), len(m.USWs), pointcount, fieldcount)
len(devices.UAPs), len(devices.USGs), len(devices.USWs), pointcount, fieldcount)
} }
} }
// batchPoints combines all device and client data into influxdb data points. // SendPoints combines all device and client data into influxdb data points.
func batchPoints(devices *unifi.Devices, clients *unifi.Clients, bp influx.BatchPoints) (errs []error) { // Call this after you've collected all the data you care about.
process := func(asset Asset) error { // This sends all the batched points to InfluxDB.
if asset == nil { func (m *Metrics) SendPoints() (errs []error) {
return nil for _, asset := range m.Sites {
} errs = append(errs, m.processPoints(asset))
influxPoints, err := asset.Points()
if err != nil {
return err
}
bp.AddPoints(influxPoints)
return nil
} }
if devices != nil { for _, asset := range m.Clients {
for _, asset := range devices.UAPs { errs = append(errs, m.processPoints(asset))
errs = append(errs, process(asset))
}
for _, asset := range devices.USGs {
errs = append(errs, process(asset))
}
for _, asset := range devices.USWs {
errs = append(errs, process(asset))
}
} }
if clients != nil { if m.Devices == nil {
for _, asset := range clients.UCLs { return
errs = append(errs, process(asset)) }
} for _, asset := range m.UAPs {
errs = append(errs, m.processPoints(asset))
}
for _, asset := range m.USGs {
errs = append(errs, m.processPoints(asset))
}
for _, asset := range m.USWs {
errs = append(errs, m.processPoints(asset))
} }
return return
} }
// filterSites returns a list of sites to fetch data for. // processPoints is helper function for SendPoints.
// Omits requested but unconfigured sites. func (m *Metrics) processPoints(asset Asset) error {
func (u *UnifiPoller) filterSites(filter []string) ([]unifi.Site, error) { if asset == nil {
return nil
}
points, err := asset.Points()
if err != nil || points == nil {
return err
}
m.BatchPoints.AddPoints(points)
return nil
}
// GetFilteredSites returns a list of sites to fetch data for.
// Omits requested but unconfigured sites. Grabs the full list from the
// controller and filters the sites provided in the config file.
func (u *UnifiPoller) GetFilteredSites() (unifi.Sites, error) {
sites, err := u.GetSites() sites, err := u.GetSites()
if err != nil { if err != nil {
return nil, err return nil, err
} else if len(filter) < 1 || StringInSlice("all", filter) { } else if len(u.Sites) < 1 || StringInSlice("all", u.Sites) {
return sites, nil return sites, nil
} }
var i int var i int
for _, s := range sites { for _, s := range sites {
// Only include valid sites in the request filter. // Only include valid sites in the request filter.
if StringInSlice(s.Name, filter) { if StringInSlice(s.Name, u.Sites) {
sites[i] = s sites[i] = s
i++ i++
} }

View File

@ -101,7 +101,11 @@ func (u *UnifiPoller) GetUnifi() (err error) {
if u.Debug && !u.Quiet { if u.Debug && !u.Quiet {
u.Unifi.DebugLog = log.Printf // Log debug messages. u.Unifi.DebugLog = log.Printf // Log debug messages.
} }
u.Logf("Authenticated to Unifi Controller at %s as user %s", u.UnifiBase, u.UnifiUser) v, err := u.GetServer()
if err != nil {
v.ServerVersion = "unknown"
}
u.Logf("Authenticated to Unifi Controller at %s version %s as user %s", u.UnifiBase, v.ServerVersion, u.UnifiUser)
if err = u.CheckSites(); err != nil { if err = u.CheckSites(); err != nil {
return err return err
} }