diff --git a/integrations/datadogunifi/alarms.go b/integrations/datadogunifi/alarms.go index a18029d3..dbbacd9b 100644 --- a/integrations/datadogunifi/alarms.go +++ b/integrations/datadogunifi/alarms.go @@ -1 +1,88 @@ package datadogunifi + +import ( + "fmt" + "strconv" + "time" + + "github.com/unpoller/unifi" +) + +const ( + alarmT = item("Alarm") + anomalyT = item("Anomaly") +) + +// batchAlarms generates alarm events and logs for Datadog. +func (u *DatadogUnifi) batchAlarms(r report, event *unifi.Alarm) { // nolint:dupl + if time.Since(event.Datetime) > u.Interval.Duration+time.Second { + return // The event is older than our interval, ignore it. + } + + tagMap := map[string]string{ + "dest_port": strconv.Itoa(event.DestPort), + "src_port": strconv.Itoa(event.SrcPort), + "dest_ip": event.DestIP, + "dst_mac": event.DstMAC, + "host": event.Host, + "msg": event.Msg, + "src_ip": event.SrcIP, + "src_mac": event.SrcMAC, + "dstip_asn": fmt.Sprintf("%d", event.DestIPGeo.Asn), + "dstip_latitude": fmt.Sprintf("%0.6f", event.DestIPGeo.Latitude), + "dstip_longitude": fmt.Sprintf("%0.6f", event.DestIPGeo.Longitude), + "dstip_city": event.DestIPGeo.City, + "dstip_continent_code": event.DestIPGeo.ContinentCode, + "dstip_country_code": event.DestIPGeo.CountryCode, + "dstip_country_name": event.DestIPGeo.CountryName, + "dstip_organization": event.DestIPGeo.Organization, + "srcip_asn": fmt.Sprintf("%d", event.SourceIPGeo.Asn), + "srcip_latitude": fmt.Sprintf("%0.6f", event.SourceIPGeo.Latitude), + "srcip_longitude": fmt.Sprintf("%0.6f", event.SourceIPGeo.Longitude), + "srcip_city": event.SourceIPGeo.City, + "srcip_continent_code": event.SourceIPGeo.ContinentCode, + "srcip_country_code": event.SourceIPGeo.CountryCode, + "srcip_country_name": event.SourceIPGeo.CountryName, + "srcip_organization": event.SourceIPGeo.Organization, + "site_name": event.SiteName, + "source": event.SourceName, + "in_iface": event.InIface, + "event_type": event.EventType, + "subsystem": event.Subsystem, + "archived": event.Archived.Txt, + "usgip": event.USGIP, + "proto": event.Proto, + "key": event.Key, + "catname": event.Catname, + "app_proto": event.AppProto, + "action": event.InnerAlertAction, + } + r.addCount(alarmT) + + tagMap = cleanTags(tagMap) + tags := tagMapToTags(tagMap) + title := fmt.Sprintf("[%s][%s] Alarm at %s from %s", event.EventType, event.Catname, event.SiteName, event.SourceName) + r.reportEvent(title, event.Datetime, event.Msg, tags) + r.reportWarnLog(fmt.Sprintf("[%d] %s: %s", event.Datetime.Unix(), title, event.Msg), tagMapToZapFields(tagMap)) +} + +// batchAnomaly generates Anomalies from UniFi for Datadog. +func (u *DatadogUnifi) batchAnomaly(r report, event *unifi.Anomaly) { + if time.Since(event.Datetime) > u.Interval.Duration+time.Second { + return // The event is older than our interval, ignore it. + } + + r.addCount(anomalyT) + + tagMap := cleanTags(map[string]string{ + "application": "unifi_anomaly", + "source": event.SourceName, + "site_name": event.SiteName, + "device_mac": event.DeviceMAC, + }) + tags := tagMapToTags(tagMap) + + title := fmt.Sprintf("Anomaly detected at %s from %s", event.SiteName, event.SourceName) + r.reportEvent(title, event.Datetime, event.Anomaly, tags) + r.reportWarnLog(fmt.Sprintf("[%d] %s: %s", event.Datetime.Unix(), title, event.Anomaly), tagMapToZapFields(tagMap)) +} diff --git a/integrations/datadogunifi/clients.go b/integrations/datadogunifi/clients.go index 91070964..a8422fee 100644 --- a/integrations/datadogunifi/clients.go +++ b/integrations/datadogunifi/clients.go @@ -1,108 +1,122 @@ package datadogunifi import ( - "github.com/unifi-poller/unifi" + "github.com/unpoller/unifi" ) -// reportClient generates Unifi Client datapoints for InfluxDB. -// These points can be passed directly to influx. -func (u *DatadogUnifi) reportClient(r report, s *unifi.Client) { // nolint: funlen - tags := []string{ - tag("mac", s.Mac), - tag("site_name", s.SiteName), - tag("source", s.SourceName), - tag("ap_name", s.ApName), - tag("gw_name", s.GwName), - tag("sw_name", s.SwName), - tag("oui", s.Oui), - tag("radio_name", s.RadioName), - tag("radio", s.Radio), - tag("radio_proto", s.RadioProto), - tag("name", s.Name), - tag("fixed_ip", s.FixedIP), - tag("sw_port", s.SwPort.Txt), - tag("os_class", s.OsClass.Txt), - tag("os_name", s.OsName.Txt), - tag("dev_cat", s.DevCat.Txt), - tag("dev_id", s.DevID.Txt), - tag("dev_vendor", s.DevVendor.Txt), - tag("dev_family", s.DevFamily.Txt), - tag("is_wired", s.IsWired.Txt), - tag("is_guest", s.IsGuest.Txt), - tag("use_fixedip", s.UseFixedIP.Txt), - tag("channel", s.Channel.Txt), - tag("vlan", s.Vlan.Txt), - tag("hostname", s.Name), - tag("radio_desc", s.RadioDescription), - tag("ip", s.IP), - tag("essid", s.Essid), - tag("bssid", s.Bssid), +// batchClient generates Unifi Client datapoints for Datadog. +// These points can be passed directly to Datadog. +func (u *DatadogUnifi) batchClient(r report, s *unifi.Client) { // nolint: funlen + 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, + "hostname": s.Name, + "essid": s.Essid, + "bssid": s.Bssid, + "ip": s.IP, + } + powerSaveEnabled := 0.0 + if s.PowersaveEnabled.Val { + powerSaveEnabled = 1.0 + } + data := map[string]float64{ + "anomalies": float64(s.Anomalies), + "channel": s.Channel.Val, + "satisfaction": s.Satisfaction.Val, + "bytes_r": float64(s.BytesR), + "ccq": float64(s.Ccq), + "noise": float64(s.Noise), + "powersave_enabled": powerSaveEnabled, + "roam_count": float64(s.RoamCount), + "rssi": float64(s.Rssi), + "rx_bytes": float64(s.RxBytes), + "rx_bytes_r": float64(s.RxBytesR), + "rx_packets": float64(s.RxPackets), + "rx_rate": float64(s.RxRate), + "signal": float64(s.Signal), + "tx_bytes": float64(s.TxBytes), + "tx_bytes_r": float64(s.TxBytesR), + "tx_packets": float64(s.TxPackets), + "tx_retries": float64(s.TxRetries), + "tx_power": float64(s.TxPower), + "tx_rate": float64(s.TxRate), + "uptime": float64(s.Uptime), + "wifi_tx_attempts": float64(s.WifiTxAttempts), + "wired-rx_bytes": float64(s.WiredRxBytes), + "wired-rx_bytes-r": float64(s.WiredRxBytesR), + "wired-rx_packets": float64(s.WiredRxPackets), + "wired-tx_bytes": float64(s.WiredTxBytes), + "wired-tx_bytes-r": float64(s.WiredTxBytesR), + "wired-tx_packets": float64(s.WiredTxPackets), } - data := map[string]float64{ - "anomalies": float64(s.Anomalies), - "channel": s.Channel.Val, - "satisfaction": s.Satisfaction.Val, - "bytes_r": float64(s.BytesR), - "ccq": float64(s.Ccq), - "noise": float64(s.Noise), - "roam_count": float64(s.RoamCount), - "rssi": float64(s.Rssi), - "rx_bytes": float64(s.RxBytes), - "rx_bytes_r": float64(s.RxBytesR), - "rx_packets": float64(s.RxPackets), - "rx_rate": float64(s.RxRate), - "signal": float64(s.Signal), - "tx_bytes": float64(s.TxBytes), - "tx_bytes_r": float64(s.TxBytesR), - "tx_packets": float64(s.TxPackets), - "tx_retries": float64(s.TxRetries), - "tx_power": float64(s.TxPower), - "tx_rate": float64(s.TxRate), - "uptime": float64(s.Uptime), - "wifi_tx_attempts": float64(s.WifiTxAttempts), - "wired-rx_bytes": float64(s.WiredRxBytes), - "wired-rx_bytes-r": float64(s.WiredRxBytesR), - "wired-rx_packets": float64(s.WiredRxPackets), - "wired-tx_bytes": float64(s.WiredTxBytes), - "wired-tx_bytes-r": float64(s.WiredTxBytesR), - "wired-tx_packets": float64(s.WiredTxPackets), - } metricName := metricNamespace("clients") - reportGaugeForMap(r, metricName, data, tags) + + reportGaugeForFloat64Map(r, metricName, data, tags) } // totalsDPImap: controller, site, name (app/cat name), dpi. type totalsDPImap map[string]map[string]map[string]unifi.DPIData -func (u *DatadogUnifi) reportClientDPI(r report, s *unifi.DPITable, appTotal, catTotal totalsDPImap) { +func (u *DatadogUnifi) batchClientDPI(r report, v interface{}, appTotal, catTotal totalsDPImap) { + s, ok := v.(*unifi.DPITable) + if !ok { + u.LogErrorf("invalid type given to batchClientDPI: %T", v) + return + } + for _, dpi := range s.ByApp { category := unifi.DPICats.Get(dpi.Cat) application := unifi.DPIApps.GetApp(dpi.Cat, dpi.App) fillDPIMapTotals(appTotal, application, s.SourceName, s.SiteName, dpi) fillDPIMapTotals(catTotal, category, s.SourceName, s.SiteName, dpi) - tags := []string{ - tag("category", category), - tag("application", application), - tag("name", s.Name), - tag("mac", s.MAC), - tag("site_name", s.SiteName), - tag("source", s.SourceName), + tags := map[string]string{ + "category": category, + "application": application, + "name": s.Name, + "mac": s.MAC, + "site_name": s.SiteName, + "source": s.SourceName, } + data := map[string]float64{ "tx_packets": float64(dpi.TxPackets), "rx_packets": float64(dpi.RxPackets), "tx_bytes": float64(dpi.TxBytes), "rx_bytes": float64(dpi.RxBytes), } - metricName := metricNamespace("clientdpi") - reportGaugeForMap(r, metricName, data, tags) + + metricName := metricNamespace("client_dpi") + + reportGaugeForFloat64Map(r, metricName, data, tags) } } // fillDPIMapTotals fills in totals for categories and applications. maybe clients too. -// This allows less processing in InfluxDB to produce total transfer data per cat or app. +// This allows less processing in Datadog to produce total transfer data per cat or app. func fillDPIMapTotals(m totalsDPImap, name, controller, site string, dpi unifi.DPIData) { if m[controller] == nil { m[controller] = make(map[string]map[string]unifi.DPIData) @@ -148,19 +162,26 @@ func reportClientDPItotals(r report, appTotal, catTotal totalsDPImap) { for controller, s := range k.val { for site, c := range s { for name, m := range c { - tags := []string{ - tag("site_name", site), - tag("source", controller), - tag("name", name), + tags := map[string]string{ + "category": "TOTAL", + "application": "TOTAL", + "name": "TOTAL", + "mac": "TOTAL", + "site_name": site, + "source": controller, } + tags[k.kind] = name + data := map[string]float64{ "tx_packets": float64(m.TxPackets), "rx_packets": float64(m.RxPackets), "tx_bytes": float64(m.TxBytes), "rx_bytes": float64(m.RxBytes), } - metricName := metricNamespace("clientdpi.totals") - reportGaugeForMap(r, metricName, data, tags) + + metricName := metricNamespace("client_dpi") + + reportGaugeForFloat64Map(r, metricName, data, tags) } } } diff --git a/integrations/datadogunifi/datadog.go b/integrations/datadogunifi/datadog.go index 377dca26..af94e22b 100644 --- a/integrations/datadogunifi/datadog.go +++ b/integrations/datadogunifi/datadog.go @@ -3,13 +3,12 @@ package datadogunifi import ( - "fmt" - "log" "time" "github.com/DataDog/datadog-go/statsd" - "github.com/unifi-poller/poller" - "github.com/unifi-poller/unifi" + "github.com/unpoller/poller" + "github.com/unpoller/unifi" + "go.uber.org/zap" "golift.io/cnfg" ) @@ -25,6 +24,9 @@ type Config struct { // Interval controls the collection and reporting interval Interval cnfg.Duration `json:"interval,omitempty" toml:"interval,omitempty" xml:"interval,omitempty" yaml:"interval,omitempty"` + // Save data for dead ports? ie. ports that are down or disabled. + DeadPorts bool `json:"dead_ports,omitempty" toml:"dead_ports,omitempty" xml:"dead_ports,omitempty" yaml:"dead_ports,omitempty"` + // Disable when true disables this output plugin Disable bool `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"` // Address determines how to talk to the Datadog agent @@ -107,11 +109,13 @@ type DatadogUnifi struct { Collector poller.Collect datadog statsd.ClientInterface LastCheck time.Time + Logger *zap.SugaredLogger *Datadog } func init() { // nolint: gochecknoinits - u := &DatadogUnifi{Datadog: &Datadog{}, LastCheck: time.Now()} + l, _ := zap.NewProduction() + u := &DatadogUnifi{Datadog: &Datadog{}, LastCheck: time.Now(), Logger: l.Sugar()} poller.NewOutput(&poller.Output{ Name: "datadog", @@ -188,8 +192,13 @@ func (u *DatadogUnifi) setConfigDefaults() { // Run runs a ticker to poll the unifi server and update Datadog. func (u *DatadogUnifi) Run(c poller.Collect) error { - if u.Config == nil || u.Disable { - c.Logf("DataDog config is missing (or disabled): Datadog output is disabled!") + defer u.Logger.Sync() + if u.Disable { + u.Logger.Debug("Datadog config is disabled, output is disabled.") + return nil + } + if u.Config == nil && !u.Disable { + u.Logger.Error("DataDog config is missing and is not disabled: Datadog output is disabled!") return nil } @@ -212,26 +221,29 @@ func (u *DatadogUnifi) Run(c poller.Collect) error { func (u *DatadogUnifi) PollController() { interval := u.Interval.Round(time.Second) ticker := time.NewTicker(interval) - log.Printf("[INFO] Everything checks out! Poller started, Datadog interval: %v", interval) + u.Logger.Info("Everything checks out! Poller started", zap.Duration("interval", interval)) for u.LastCheck = range ticker.C { - metrics, ok, collectErr := u.Collector.Metrics() - if collectErr != nil { - u.Collector.LogErrorf("metric fetch for Datadog failed: %v", collectErr) - - if !ok { - continue - } - } - - report, err := u.ReportMetrics(metrics) + metrics, err := u.Collector.Metrics(&poller.Filter{Name: "unifi"}) if err != nil { - // Is the agent down? - u.Collector.LogErrorf("%v", err) + u.Logger.Error("metric fetch for Datadog failed", zap.Error(err)) continue } - report.error(collectErr) + events, err := u.Collector.Events(&poller.Filter{Name: "unifi", Dur: interval}) + if err != nil { + u.Logger.Error("event fetch for Datadog failed", zap.Error(err)) + continue + } + + report, err := u.ReportMetrics(metrics, events) + if err != nil { + // Is the agent down? + u.Logger.Error("unable to report metrics and events", zap.Error(err)) + report.reportCount("unifi.collect.errors", 1, []string{}) + continue + } + report.reportCount("unifi.collect.success", 1, []string{}) u.LogDatadogReport(report) } } @@ -239,12 +251,19 @@ func (u *DatadogUnifi) PollController() { // ReportMetrics batches all device and client data into datadog data points. // Call this after you've collected all the data you care about. // Returns an error if datadog statsd calls fail, otherwise returns a report. -func (u *DatadogUnifi) ReportMetrics(m *poller.Metrics) (*Report, error) { - r := &Report{Metrics: m, Start: time.Now()} +func (u *DatadogUnifi) ReportMetrics(m *poller.Metrics, e *poller.Events) (*Report, error) { + r := &Report{ + Metrics: m, + Events: e, + Start: time.Now(), + Counts: &Counts{Val: make(map[item]int)}, + Logger: u.Logger, + } // batch all the points. u.loopPoints(r) r.End = time.Now() r.Elapsed = r.End.Sub(r.Start) + r.reportTiming("unifi.collector_timing", r.Elapsed, []string{}) return r, nil } @@ -252,65 +271,82 @@ func (u *DatadogUnifi) ReportMetrics(m *poller.Metrics) (*Report, error) { func (u *DatadogUnifi) loopPoints(r report) { m := r.metrics() - for _, s := range m.SitesDPI { - u.reportSiteDPI(r, s) + for _, s := range m.RogueAPs { + u.switchExport(r, s) } for _, s := range m.Sites { - u.reportSite(r, s) + u.switchExport(r, s) + } + + for _, s := range m.SitesDPI { + u.reportSiteDPI(r, s.(*unifi.DPITable)) + } + + for _, s := range m.Clients { + u.switchExport(r, s) + } + + for _, s := range m.Devices { + u.switchExport(r, s) + } + + for _, s := range r.events().Logs { + u.switchExport(r, s) } appTotal := make(totalsDPImap) catTotal := make(totalsDPImap) for _, s := range m.ClientsDPI { - u.reportClientDPI(r, s, appTotal, catTotal) + u.batchClientDPI(r, s, appTotal, catTotal) } reportClientDPItotals(r, appTotal, catTotal) - - for _, s := range m.Clients { - u.reportClient(r, s) - } - - for _, s := range m.IDSList { - u.reportIDS(r, s) - } - - u.loopDevicePoints(r) } -func (u *DatadogUnifi) loopDevicePoints(r report) { - m := r.metrics() - if m.Devices == nil { - m.Devices = &unifi.Devices{} - return - } - - for _, s := range m.UAPs { - u.reportUAP(r, s) - } - - for _, s := range m.USGs { - u.reportUSG(r, s) - } - - for _, s := range m.USWs { - u.reportUSW(r, s) - } - - for _, s := range m.UDMs { - u.reportUDM(r, s) +func (u *DatadogUnifi) switchExport(r report, v interface{}) { //nolint:cyclop + switch v := v.(type) { + case *unifi.RogueAP: + u.batchRogueAP(r, v) + case *unifi.UAP: + u.batchUAP(r, v) + case *unifi.USW: + u.batchUSW(r, v) + case *unifi.USG: + u.batchUSG(r, v) + case *unifi.UXG: + u.batchUXG(r, v) + case *unifi.UDM: + u.batchUDM(r, v) + case *unifi.Site: + u.reportSite(r, v) + case *unifi.Client: + u.batchClient(r, v) + case *unifi.Event: + u.batchEvent(r, v) + case *unifi.IDS: + u.batchIDS(r, v) + case *unifi.Alarm: + u.batchAlarms(r, v) + case *unifi.Anomaly: + u.batchAnomaly(r, v) + default: + u.Logger.Error("invalid export", zap.Reflect("type", v)) } } -// LogInfluxReport writes a log message after exporting to influxdb. +// LogDatadogReport writes a log message after exporting to Datadog. func (u *DatadogUnifi) LogDatadogReport(r *Report) { m := r.Metrics - idsMsg := fmt.Sprintf("IDS Events: %d, ", len(m.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(m.Sites), len(m.Clients), len(m.UAPs), - len(m.UDMs)+len(m.USGs), len(m.USWs), idsMsg, r.Total, - r.Fields, len(r.Errors), r.Elapsed.Round(time.Millisecond)) + u.Logger.Info("UniFi Metrics Recorded", + zap.Int("num_sites", len(m.Sites)), + zap.Int("num_sites_dpi", len(m.SitesDPI)), + zap.Int("num_clients", len(m.Clients)), + zap.Int("num_clients_dpi", len(m.ClientsDPI)), + zap.Int("num_rogue_ap", len(m.RogueAPs)), + zap.Int("num_devices", len(m.Devices)), + zap.Errors("errors", r.Errors), + zap.Duration("elapsed", r.Elapsed), + ) } diff --git a/integrations/datadogunifi/events.go b/integrations/datadogunifi/events.go index a18029d3..a95f3e25 100644 --- a/integrations/datadogunifi/events.go +++ b/integrations/datadogunifi/events.go @@ -1 +1,143 @@ package datadogunifi + +import ( + "fmt" + "strconv" + "time" + + "github.com/unpoller/unifi" +) + +// These constants are used as names for printed/logged counters. +const ( + eventT = item("Event") + idsT = item("IDS") +) + +// batchIDS generates intrusion detection datapoints for Datadog. +func (u *DatadogUnifi) batchIDS(r report, i *unifi.IDS) { // nolint:dupl + if time.Since(i.Datetime) > u.Interval.Duration+time.Second { + return // The event is older than our interval, ignore it. + } + + tagMap := map[string]string{ + "dest_port": strconv.Itoa(i.DestPort), + "src_port": strconv.Itoa(i.SrcPort), + "dest_ip": i.DestIP, + "dst_mac": i.DstMAC, + "host": i.Host, + "msg": i.Msg, + "src_ip": i.SrcIP, + "src_mac": i.SrcMAC, + "dstip_asn": fmt.Sprintf("%d", i.DestIPGeo.Asn), + "dstip_latitude": fmt.Sprintf("%0.6f", i.DestIPGeo.Latitude), + "dstip_longitude": fmt.Sprintf("%0.6f", i.DestIPGeo.Longitude), + "dstip_city": i.DestIPGeo.City, + "dstip_continent_code": i.DestIPGeo.ContinentCode, + "dstip_country_code": i.DestIPGeo.CountryCode, + "dstip_country_name": i.DestIPGeo.CountryName, + "dstip_organization": i.DestIPGeo.Organization, + "srcip_asn": fmt.Sprintf("%d", i.SourceIPGeo.Asn), + "srcip_latitude": fmt.Sprintf("%0.6f", i.SourceIPGeo.Latitude), + "srcip_longitude": fmt.Sprintf("%0.6f", i.SourceIPGeo.Longitude), + "srcip_city": i.SourceIPGeo.City, + "srcip_continent_code": i.SourceIPGeo.ContinentCode, + "srcip_country_code": i.SourceIPGeo.CountryCode, + "srcip_country_name": i.SourceIPGeo.CountryName, + "srcip_organization": i.SourceIPGeo.Organization, + "site_name": i.SiteName, + "source": i.SourceName, + "in_iface": i.InIface, + "event_type": i.EventType, + "subsystem": i.Subsystem, + "archived": i.Archived.Txt, + "usgip": i.USGIP, + "proto": i.Proto, + "key": i.Key, + "catname": i.Catname, + "app_proto": i.AppProto, + "action": i.InnerAlertAction, + } + + r.addCount(idsT) + + tagMap = cleanTags(tagMap) + tags := tagMapToTags(tagMap) + title := fmt.Sprintf("Intrusion Detection at %s from %s", i.SiteName, i.SourceName) + r.reportEvent(title, i.Datetime, i.Msg, tags) + r.reportWarnLog(fmt.Sprintf("[%d] %s: %s", i.Datetime.Unix(), title, i.Msg), tagMapToZapFields(tagMap)) +} + +// batchEvents generates events from UniFi for Datadog. +func (u *DatadogUnifi) batchEvent(r report, i *unifi.Event) { // nolint: funlen + if time.Since(i.Datetime) > u.Interval.Duration+time.Second { + return // The event is older than our interval, ignore it. + } + + tagMap := map[string]string{ + "guest": i.Guest, // mac address + "user": i.User, // mac address + "host": i.Host, // usg device? + "hostname": i.Hostname, // client name + "dest_port": strconv.Itoa(i.DestPort), + "src_port": strconv.Itoa(i.SrcPort), + "dest_ip": i.DestIP, + "dst_mac": i.DstMAC, + "ip": i.IP, + "src_ip": i.SrcIP, + "src_mac": i.SrcMAC, + "dstip_asn": fmt.Sprintf("%d", i.DestIPGeo.Asn), + "dstip_latitude": fmt.Sprintf("%0.6f", i.DestIPGeo.Latitude), + "dstip_longitude": fmt.Sprintf("%0.6f", i.DestIPGeo.Longitude), + "dstip_city": i.DestIPGeo.City, + "dstip_continent_code": i.DestIPGeo.ContinentCode, + "dstip_country_code": i.DestIPGeo.CountryCode, + "dstip_country_name": i.DestIPGeo.CountryName, + "dstip_organization": i.DestIPGeo.Organization, + "srcip_asn": fmt.Sprintf("%d", i.SourceIPGeo.Asn), + "srcip_latitude": fmt.Sprintf("%0.6f", i.SourceIPGeo.Latitude), + "srcip_longitude": fmt.Sprintf("%0.6f", i.SourceIPGeo.Longitude), + "srcip_city": i.SourceIPGeo.City, + "srcip_continent_code": i.SourceIPGeo.ContinentCode, + "srcip_country_code": i.SourceIPGeo.CountryCode, + "srcip_country_name": i.SourceIPGeo.CountryName, + "srcip_organization": i.SourceIPGeo.Organization, + "admin": i.Admin, // username + "site_name": i.SiteName, + "source": i.SourceName, + "ap_from": i.ApFrom, + "ap_to": i.ApTo, + "ap": i.Ap, + "ap_name": i.ApName, + "gw": i.Gw, + "gw_name": i.GwName, + "sw": i.Sw, + "sw_name": i.SwName, + "catname": i.Catname, + "radio": i.Radio, + "radio_from": i.RadioFrom, + "radio_to": i.RadioTo, + "key": i.Key, + "in_iface": i.InIface, + "event_type": i.EventType, + "subsystem": i.Subsystem, + "ssid": i.SSID, + "is_admin": i.IsAdmin.Txt, + "channel": i.Channel.Txt, + "channel_from": i.ChannelFrom.Txt, + "channel_to": i.ChannelTo.Txt, + "usgip": i.USGIP, + "network": i.Network, + "app_proto": i.AppProto, + "proto": i.Proto, + "action": i.InnerAlertAction, + } + + r.addCount(eventT) + + tagMap = cleanTags(tagMap) + tags := tagMapToTags(tagMap) + title := fmt.Sprintf("Unifi Event at %s from %s", i.SiteName, i.SourceName) + r.reportEvent(title, i.Datetime, i.Msg, tags) + r.reportInfoLog(fmt.Sprintf("[%d] %s: %s", i.Datetime.Unix(), title, i.Msg), tagMapToZapFields(tagMap)) +} diff --git a/integrations/datadogunifi/go.mod b/integrations/datadogunifi/go.mod index 59f2a409..e4001308 100644 --- a/integrations/datadogunifi/go.mod +++ b/integrations/datadogunifi/go.mod @@ -5,7 +5,10 @@ go 1.15 require ( github.com/DataDog/datadog-go v4.0.0+incompatible github.com/pkg/errors v0.9.1 // indirect - github.com/unifi-poller/poller v0.0.7 - github.com/unifi-poller/unifi v0.0.5 - golift.io/cnfg v0.0.6 + github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28 + github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80 + github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf + go.uber.org/zap v1.19.1 + golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect + golift.io/cnfg v0.0.7 ) diff --git a/integrations/datadogunifi/go.sum b/integrations/datadogunifi/go.sum index f3eb0480..16bdb5f0 100644 --- a/integrations/datadogunifi/go.sum +++ b/integrations/datadogunifi/go.sum @@ -2,6 +2,7 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v4.0.0+incompatible h1:Dq8Dr+4sV1gBO1sHDWdW+4G+PdsA+YSJOK925MxrrCY= github.com/DataDog/datadog-go v4.0.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -9,33 +10,53 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c h1:zqmyTlQyufRC65JnImJ6H1Sf7BDj8bG31EV919NVEQc= github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28 h1:YAv5naMdpOFahnxteFFRidZlrSEwLv8V2nBKJKmLmHg= github.com/unpoller/poller v0.0.0-20210623101401-f12841d79a28/go.mod h1:AbDp60t5WlLSRELAliMJ0RFQpm/0yXpyolVSZqNtero= github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80 h1:XjHGfJhMwnB63DYHgtWGJgDxLhxVcAOtf+cfuvpGoyo= github.com/unpoller/unifi v0.0.9-0.20210623100314-3dccfdbc4c80/go.mod h1:K9QFFGfZws4gzB+Popix19S/rBKqrtqI+tyPORyg3F0= github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf h1:HhXi3qca3kyFEFPh0mqdr0bpQs94hJvMbUJztwPtf2A= github.com/unpoller/webserver v0.0.0-20210623101543-90d89bb0acdf/go.mod h1:77PywuUvspdtoRuH1htFhR3Tp0pLyWj6kJlYR4tBYho= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210415231046-e915ea6b2b7d/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -46,13 +67,21 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golift.io/cnfg v0.0.7 h1:qkNpP5Bq+5Gtoc6HcI8kapMD5zFOVan6qguxqBQF3OY= golift.io/cnfg v0.0.7/go.mod h1:AsB0DJe7nv0bizKaoy3e3MjjOF7upTpMOMvsfv4CNNk= golift.io/version v0.0.2 h1:i0gXRuSDHKs4O0sVDUg4+vNIuOxYoXhaxspftu2FRTE= golift.io/version v0.0.2/go.mod h1:76aHNz8/Pm7CbuxIsDi97jABL5Zui3f2uZxDm4vB6hU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= diff --git a/integrations/datadogunifi/ids.go b/integrations/datadogunifi/ids.go index f4bb29e3..a79b7bb8 100644 --- a/integrations/datadogunifi/ids.go +++ b/integrations/datadogunifi/ids.go @@ -1,7 +1,7 @@ package datadogunifi import ( - "github.com/unifi-poller/unifi" + "github.com/unpoller/unifi" ) // reportIDS generates intrusion detection datapoints for Datadog. diff --git a/integrations/datadogunifi/logger.go b/integrations/datadogunifi/logger.go index a18029d3..eed2685e 100644 --- a/integrations/datadogunifi/logger.go +++ b/integrations/datadogunifi/logger.go @@ -1 +1,38 @@ package datadogunifi + +import ( + "fmt" + "time" + + "github.com/unpoller/webserver" +) + +// Logf logs a message. +func (u *DatadogUnifi) Logf(msg string, v ...interface{}) { + webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{ + Ts: time.Now(), + Msg: fmt.Sprintf(msg, v...), + Tags: map[string]string{"type": "info"}, + }) + u.Collector.Logf(msg, v...) +} + +// LogErrorf logs an error message. +func (u *DatadogUnifi) LogErrorf(msg string, v ...interface{}) { + webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{ + Ts: time.Now(), + Msg: fmt.Sprintf(msg, v...), + Tags: map[string]string{"type": "error"}, + }) + u.Collector.LogErrorf(msg, v...) +} + +// LogDebugf logs a debug message. +func (u *DatadogUnifi) LogDebugf(msg string, v ...interface{}) { + webserver.NewOutputEvent(PluginName, PluginName, &webserver.Event{ + Ts: time.Now(), + Msg: fmt.Sprintf(msg, v...), + Tags: map[string]string{"type": "debug"}, + }) + u.Collector.LogDebugf(msg, v...) +} diff --git a/integrations/datadogunifi/points.go b/integrations/datadogunifi/points.go index 751c2114..c269ee78 100644 --- a/integrations/datadogunifi/points.go +++ b/integrations/datadogunifi/points.go @@ -2,20 +2,78 @@ package datadogunifi import ( "fmt" + + "github.com/unpoller/unifi" + "go.uber.org/zap" ) func tag(name string, value interface{}) string { return fmt.Sprintf("%s:%v", name, value) } +func tagMapToTags(tagMap map[string]string) []string { + tags := make([]string, 0) + for k, v := range tagMap { + tags = append(tags, tag(k, v)) + } + return tags +} + +func tagMapToZapFields(tagMap map[string]string) []zap.Field { + fields := make([]zap.Field, 0) + for k, v := range tagMap { + fields = append(fields, zap.String(k, v)) + } + return fields +} + func metricNamespace(namespace string) func(string) string { return func(name string) string { - return fmt.Sprintf("%s.%s", namespace, name) + return fmt.Sprintf("unifi.%s.%s", namespace, name) } } -func reportGaugeForMap(r report, metricName func(string) string, data map[string]float64, tags []string) { +func reportGaugeForFloat64Map(r report, metricName func(string) string, data map[string]float64, tags map[string]string) { for name, value := range data { - r.reportGauge(metricName(name), value, tags) + r.reportGauge(metricName(name), value, tagMapToTags(tags)) } } + +// cleanTags removes any tag that is empty. +func cleanTags(tags map[string]string) map[string]string { + for i := range tags { + if tags[i] == "" { + delete(tags, i) + } + } + + return tags +} + +// cleanFields removes any field with a default (or empty) value. +func cleanFields(fields map[string]interface{}) map[string]interface{} { //nolint:cyclop + for s := range fields { + switch v := fields[s].(type) { + case nil: + delete(fields, s) + case int, int64, float64: + if v == 0 { + delete(fields, s) + } + case unifi.FlexBool: + if v.Txt == "" { + delete(fields, s) + } + case unifi.FlexInt: + if v.Txt == "" { + delete(fields, s) + } + case string: + if v == "" { + delete(fields, s) + } + } + } + + return fields +} diff --git a/integrations/datadogunifi/report.go b/integrations/datadogunifi/report.go index 5450606c..e1ffaebc 100644 --- a/integrations/datadogunifi/report.go +++ b/integrations/datadogunifi/report.go @@ -1,39 +1,90 @@ package datadogunifi import ( + "sync" "time" "github.com/DataDog/datadog-go/statsd" - "github.com/unifi-poller/poller" + "github.com/unpoller/poller" + "go.uber.org/zap" ) type Report struct { Metrics *poller.Metrics + Events *poller.Events Errors []error - Total int - Fields int + Counts *Counts Start time.Time End time.Time Elapsed time.Duration + Logger *zap.SugaredLogger + + Total int + Fields int + + wg sync.WaitGroup + client statsd.ClientInterface } +// Counts holds counters and has a lock to deal with routines. +type Counts struct { + Val map[item]int + sync.RWMutex +} + type report interface { + add() + done() error(err error) metrics() *poller.Metrics + events() *poller.Events + addCount(item, ...int) + reportGauge(name string, value float64, tags []string) error reportCount(name string, value int64, tags []string) error reportDistribution(name string, value float64, tags []string) error reportTiming(name string, value time.Duration, tags []string) error - reportEvent(title string, message string, tags []string) error + reportEvent(title string, date time.Time, message string, tags []string) error + reportInfoLog(message string, f ...interface{}) + reportWarnLog(message string, f ...interface{}) reportServiceCheck(name string, status statsd.ServiceCheckStatus, message string, tags []string) error } +func (r *Report) add() { + r.wg.Add(1) +} + +func (r *Report) done() { + r.wg.Done() +} + func (r *Report) metrics() *poller.Metrics { return r.Metrics } +func (r *Report) events() *poller.Events { + return r.Events +} + +/* The following methods are not thread safe. */ + +type item string + +func (r *Report) addCount(name item, counts ...int) { + r.Counts.Lock() + defer r.Counts.Unlock() + + if len(counts) == 0 { + r.Counts.Val[name]++ + } + + for _, c := range counts { + r.Counts.Val[name] += c + } +} + func (r *Report) error(err error) { if err != nil { r.Errors = append(r.Errors, err) @@ -56,15 +107,26 @@ func (r *Report) reportTiming(name string, value time.Duration, tags []string) e return r.client.Timing(name, value, tags, 1.0) } -func (r *Report) reportEvent(title string, message string, tags []string) error { +func (r *Report) reportEvent(title string, date time.Time, message string, tags []string) error { + if date.IsZero() { + date = time.Now() + } return r.client.Event(&statsd.Event{ Title: title, Text: message, - Timestamp: time.Now(), + Timestamp: date, Tags: tags, }) } +func (r *Report) reportInfoLog(message string, f ...interface{}) { + r.Logger.Info(message, f) +} + +func (r *Report) reportWarnLog(message string, f ...interface{}) { + r.Logger.Warn(message, f) +} + func (r *Report) reportServiceCheck(name string, status statsd.ServiceCheckStatus, message string, tags []string) error { return r.client.ServiceCheck(&statsd.ServiceCheck{ Name: name, diff --git a/integrations/datadogunifi/site.go b/integrations/datadogunifi/site.go index a9ffe0a1..38c6e7c5 100644 --- a/integrations/datadogunifi/site.go +++ b/integrations/datadogunifi/site.go @@ -1,7 +1,7 @@ package datadogunifi import ( - "github.com/unifi-poller/unifi" + "github.com/unpoller/unifi" ) // reportSite generates Unifi Sites' datapoints for Datadog. diff --git a/integrations/datadogunifi/uap.go b/integrations/datadogunifi/uap.go index 83403c12..37729fba 100644 --- a/integrations/datadogunifi/uap.go +++ b/integrations/datadogunifi/uap.go @@ -1,57 +1,93 @@ package datadogunifi import ( - "github.com/unifi-poller/unifi" + "github.com/unpoller/unifi" ) -// reportUAP generates Wireless-Access-Point datapoints for InfluxDB. -// These points can be passed directly to influx. -func (u *DatadogUnifi) reportUAP(r report, s *unifi.UAP) { +// uapT is used as a name for printed/logged counters. +const uapT = item("UAP") + +// batchRogueAP generates metric points for neighboring access points. +func (u *DatadogUnifi) batchRogueAP(r report, s *unifi.RogueAP) { + if s.Age.Val == 0 { + return // only keep metrics for things that are recent. + } + + tags := cleanTags(map[string]string{ + "security": s.Security, + "oui": s.Oui, + "band": s.Band, + "mac": s.Bssid, + "ap_mac": s.ApMac, + "radio": s.Radio, + "radio_name": s.RadioName, + "site_name": s.SiteName, + "name": s.Essid, + "source": s.SourceName, + }) + + data := map[string]float64{ + "age": s.Age.Val, + "bw": s.Bw.Val, + "center_freq": s.CenterFreq.Val, + "channel": float64(s.Channel), + "freq": s.Freq.Val, + "noise": s.Noise.Val, + "rssi": s.Rssi.Val, + "rssi_age": s.RssiAge.Val, + "signal": s.Signal.Val, + } + + metricName := metricNamespace("uap_rogue") + + reportGaugeForFloat64Map(r, metricName, data, tags) +} + +// batchUAP generates Wireless-Access-Point datapoints for Datadog. +// These points can be passed directly to datadog. +func (u *DatadogUnifi) batchUAP(r report, s *unifi.UAP) { if !s.Adopted.Val || s.Locating.Val { return } - tags := []string{ - tag("ip", s.IP), - tag("mac", s.Mac), - tag("site_name", s.SiteName), - tag("source", s.SourceName), - tag("name", s.Name), - tag("version", s.Version), - tag("model", s.Model), - tag("serial", s.Serial), - tag("type", s.Type), - } + tags := cleanTags(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, + "ip": s.IP, + }) + data := CombineFloat64(u.processUAPstats(s.Stat.Ap), u.batchSysStats(s.SysStats, s.SystemStats)) + data["bytes"] = s.Bytes.Val + data["last_seen"] = s.LastSeen.Val + data["rx_bytes"] = s.RxBytes.Val + data["tx_bytes"] = s.TxBytes.Val + data["uptime"] = s.Uptime.Val + data["user_num_sta"] = s.UserNumSta.Val + data["guest_num_sta"] = s.GuestNumSta.Val + data["num_sta"] = s.NumSta.Val + + r.addCount(uapT) metricName := metricNamespace("uap") - u.reportUAPstats(s.Stat.Ap, r, metricName, tags) - u.reportSysStats(r, metricName, s.SysStats, s.SystemStats, tags) + reportGaugeForFloat64Map(r, metricName, data, tags) - data := map[string]float64{ - "bytes": s.Bytes.Val, - "last_seen": s.LastSeen.Val, - "rx_bytes": s.RxBytes.Val, - "tx_bytes": s.TxBytes.Val, - "uptime": s.Uptime.Val, - "user-num_sta": s.UserNumSta.Val, - "guest-num_sta": s.GuestNumSta.Val, - "num_sta": s.NumSta.Val, - } - reportGaugeForMap(r, metricName, data, tags) - - u.reportRadTable(r, s.Name, s.SiteName, s.SourceName, s.RadioTable, s.RadioTableStats) - u.reportVAPTable(r, s.Name, s.SiteName, s.SourceName, s.VapTable) - u.reportPortTable(r, s.Name, s.SiteName, s.SourceName, s.Type, s.PortTable) + u.processVAPTable(r, tags, s.VapTable) + u.batchPortTable(r, tags, s.PortTable) } -func (u *DatadogUnifi) reportUAPstats(ap *unifi.Ap, r report, metricName func(string) string, tags []string) { +func (u *DatadogUnifi) processUAPstats(ap *unifi.Ap) map[string]float64 { if ap == nil { - return + return map[string]float64{} } // Accumulative Statistics. - data := map[string]float64{ + return map[string]float64{ "stat_user-rx_packets": ap.UserRxPackets.Val, "stat_guest-rx_packets": ap.GuestRxPackets.Val, "stat_rx_packets": ap.RxPackets.Val, @@ -85,27 +121,26 @@ func (u *DatadogUnifi) reportUAPstats(ap *unifi.Ap, r report, metricName func(st "stat_user-tx_retries": ap.UserTxRetries.Val, "stat_guest-tx_retries": ap.GuestTxRetries.Val, } - reportGaugeForMap(r, metricName, data, tags) } -// reportVAPTable creates points for Wifi Radios. This works with several types of UAP-capable devices. -func (u *DatadogUnifi) reportVAPTable(r report, deviceName string, siteName string, source string, vt unifi.VapTable) { // nolint: funlen +// processVAPTable creates points for Wifi Radios. This works with several types of UAP-capable devices. +func (u *DatadogUnifi) processVAPTable(r report, t map[string]string, vt unifi.VapTable) { // nolint: funlen for _, s := range vt { - tags := []string{ - tag("device_name", deviceName), - tag("site_name", siteName), - tag("source", source), - tag("ap_mac", s.ApMac), - tag("bssid", s.Bssid), - tag("id", s.ID), - tag("name", s.Name), - tag("radio_name", s.RadioName), - tag("radio", s.Radio), - tag("essid", s.Essid), - tag("site_id", s.SiteID), - tag("usage", s.Usage), - tag("state", s.State), - tag("is_guest", s.IsGuest.Txt), + 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, } data := map[string]float64{ "ccq": float64(s.Ccq), @@ -150,22 +185,23 @@ func (u *DatadogUnifi) reportVAPTable(r report, deviceName string, siteName stri } metricName := metricNamespace("uap_vaps") - reportGaugeForMap(r, metricName, data, tags) + + reportGaugeForFloat64Map(r, metricName, data, tags) } } -func (u *DatadogUnifi) reportRadTable(r report, deviceName string, siteName string, source string, rt unifi.RadioTable, rts unifi.RadioTableStats) { +func (u *DatadogUnifi) processRadTable(r report, t map[string]string, rt unifi.RadioTable, rts unifi.RadioTableStats) { for _, p := range rt { - tags := []string{ - tag("device_name", deviceName), - tag("site_name", siteName), - tag("source", source), - tag("channel", p.Channel.Txt), - tag("radio", p.Radio), + tags := map[string]string{ + "device_name": t["name"], + "site_name": t["site_name"], + "source": t["source"], + "channel": p.Channel.Txt, + "radio": p.Radio, + "ht": p.Ht.Txt, } data := map[string]float64{ "current_antenna_gain": p.CurrentAntennaGain.Val, - "ht": p.Ht.Val, "max_txpower": p.MaxTxpower.Val, "min_txpower": p.MinTxpower.Val, "nss": p.Nss.Val, @@ -181,12 +217,12 @@ func (u *DatadogUnifi) reportRadTable(r report, deviceName string, siteName stri data["cu_total"] = t.CuTotal.Val data["extchannel"] = t.Extchannel.Val data["gain"] = t.Gain.Val - data["guest-num_sta"] = t.GuestNumSta.Val + data["guest_num_sta"] = t.GuestNumSta.Val data["num_sta"] = t.NumSta.Val data["tx_packets"] = t.TxPackets.Val data["tx_power"] = t.TxPower.Val data["tx_retries"] = t.TxRetries.Val - data["user-num_sta"] = t.UserNumSta.Val + data["user_num_sta"] = t.UserNumSta.Val break } @@ -194,6 +230,6 @@ func (u *DatadogUnifi) reportRadTable(r report, deviceName string, siteName stri metricName := metricNamespace("uap_radios") - reportGaugeForMap(r, metricName, data, tags) + reportGaugeForFloat64Map(r, metricName, data, tags) } } diff --git a/integrations/datadogunifi/udm.go b/integrations/datadogunifi/udm.go index f544648b..4529086c 100644 --- a/integrations/datadogunifi/udm.go +++ b/integrations/datadogunifi/udm.go @@ -1,14 +1,44 @@ package datadogunifi import ( - "fmt" + "strconv" + "strings" - "github.com/unifi-poller/unifi" + "github.com/unpoller/unifi" ) -// reportSysStats is used by all device types. -func (u *DatadogUnifi) reportSysStats(r report, metricName func(string) string, s unifi.SysStats, ss unifi.SystemStats, tags []string) { - data := map[string]float64{ +// udmT is used as a name for printed/logged counters. +const udmT = item("UDM") + +// 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 +} + +// CombineFloat64 concatenates N maps. This will delete things if not used with caution. +func CombineFloat64(in ...map[string]float64) map[string]float64 { + out := make(map[string]float64) + + 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 *DatadogUnifi) batchSysStats(s unifi.SysStats, ss unifi.SystemStats) map[string]float64 { + m := map[string]float64{ "loadavg_1": s.Loadavg1.Val, "loadavg_5": s.Loadavg5.Val, "loadavg_15": s.Loadavg15.Val, @@ -19,123 +49,148 @@ func (u *DatadogUnifi) reportSysStats(r report, metricName func(string) string, "mem": ss.Mem.Val, "system_uptime": ss.Uptime.Val, } - for name, value := range data { - r.reportGauge(metricName(name), value, tags) + + for k, v := range ss.Temps { + temp, _ := strconv.Atoi(strings.Split(v, " ")[0]) + k = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(k, " ", "_"), ")", ""), "(", "") + + if temp != 0 && k != "" { + m["temp_"+strings.ToLower(k)] = float64(temp) + } } + + return m } -func (u *DatadogUnifi) reportUDMtemps(r report, metricName func(string) string, tags []string, temps []unifi.Temperature) { +func (u *DatadogUnifi) batchUDMtemps(temps []unifi.Temperature) map[string]float64 { + output := make(map[string]float64) + for _, t := range temps { - name := fmt.Sprintf("temp_%s", t.Name) - r.reportGauge(metricName(name), t.Value, tags) + output["temp_"+t.Name] = t.Value } + + return output } -// reportUDM generates Unifi Gateway datapoints for InfluxDB. -// These points can be passed directly to influx. -func (u *DatadogUnifi) reportUDM(r report, s *unifi.UDM) { // nolint: funlen +func (u *DatadogUnifi) batchUDMstorage(storage []*unifi.Storage) map[string]float64 { + output := make(map[string]float64) + + for _, t := range storage { + output["storage_"+t.Name+"_size"] = t.Size.Val + output["storage_"+t.Name+"_used"] = t.Used.Val + + if t.Size.Val != 0 && t.Used.Val != 0 && t.Used.Val < t.Size.Val { + output["storage_"+t.Name+"_pct"] = t.Used.Val / t.Size.Val * 100 //nolint:gomnd + } else { + output["storage_"+t.Name+"_pct"] = 0 + } + } + + return output +} + +// batchUDM generates Unifi Gateway datapoints for Datadog. +// These points can be passed directly to datadog. +func (u *DatadogUnifi) batchUDM(r report, s *unifi.UDM) { // nolint: funlen if !s.Adopted.Val || s.Locating.Val { return } + tags := cleanTags(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, + "ip": s.IP, + "license_state": s.LicenseState, + }) + data := CombineFloat64( + u.batchUDMstorage(s.Storage), + u.batchUDMtemps(s.Temperatures), + u.batchUSGstats(s.SpeedtestStatus, s.Stat.Gw, s.Uplink), + u.batchSysStats(s.SysStats, s.SystemStats), + map[string]float64{ + "bytes": s.Bytes.Val, + "last_seen": s.LastSeen.Val, + "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, + "num_desktop": s.NumDesktop.Val, + "num_handheld": s.NumHandheld.Val, + "num_mobile": s.NumMobile.Val, + }, + ) + + r.addCount(udmT) metricName := metricNamespace("usg") + reportGaugeForFloat64Map(r, metricName, data, tags) - tags := []string{ - tag("source", s.SourceName), - tag("ip", s.IP), - tag("license_state", s.LicenseState), - tag("mac", s.Mac), - tag("site_name", s.SiteName), - tag("name", s.Name), - tag("version", s.Version), - tag("model", s.Model), - tag("serial", s.Serial), - tag("type", s.Type), - } - u.reportUDMtemps(r, metricName, tags, s.Temperatures) - u.reportUSGstats(r, metricName, tags, s.SpeedtestStatus, s.Stat.Gw, s.Uplink) - u.reportSysStats(r, metricName, s.SysStats, s.SystemStats, tags) + u.batchNetTable(r, tags, s.NetworkTable) + u.batchUSGwans(r, tags, s.Wan1, s.Wan2) - data := map[string]float64{ - "bytes": s.Bytes.Val, - "last_seen": s.LastSeen.Val, - "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, - "num_desktop": s.NumDesktop.Val, - "num_handheld": s.NumHandheld.Val, - "num_mobile": s.NumMobile.Val, - } - for name, value := range data { - r.reportGauge(metricName(name), value, tags) - } - u.reportNetTable(r, s.Name, s.SiteName, s.SourceName, s.NetworkTable) - u.reportUSGwans(r, s.Name, s.SiteName, s.SourceName, s.Wan1, s.Wan2) + tags = cleanTags(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, + "ip": s.IP, + }) + data = CombineFloat64( + u.batchUSWstat(s.Stat.Sw), + map[string]float64{ + "guest-num_sta": s.GuestNumSta.Val, + "bytes": s.Bytes.Val, + "last_seen": s.LastSeen.Val, + "rx_bytes": s.RxBytes.Val, + "tx_bytes": s.TxBytes.Val, + "uptime": s.Uptime.Val, + }) - tags = []string{ - tag("mac", s.Mac), - tag("site_name", s.SiteName), - tag("source", s.SourceName), - tag("name", s.Name), - tag("version", s.Version), - tag("model", s.Model), - tag("serial", s.Serial), - tag("type", s.Type), - tag("ip", s.IP), - } metricName = metricNamespace("usw") - u.reportUSWstat(r, metricName, tags, s.Stat.Sw) + reportGaugeForFloat64Map(r, metricName, data, tags) - data = map[string]float64{ - "guest-num_sta": s.GuestNumSta.Val, - "bytes": s.Bytes.Val, - "last_seen": s.LastSeen.Val, - "rx_bytes": s.RxBytes.Val, - "tx_bytes": s.TxBytes.Val, - "uptime": s.Uptime.Val, - } - for name, value := range data { - r.reportGauge(metricName(name), value, tags) - } - - u.reportPortTable(r, s.Name, s.SiteName, s.SourceName, s.Type, s.PortTable) // udm has a usw in it. + u.batchPortTable(r, tags, s.PortTable) // udm has a usw in it. if s.Stat.Ap == nil { return // we're done now. the following code process UDM (non-pro) UAP data. } - tags = []string{ - tag("mac", s.Mac), - tag("site_name", s.SiteName), - tag("source", s.SourceName), - tag("name", s.Name), - tag("version", s.Version), - tag("model", s.Model), - tag("serial", s.Serial), - tag("type", s.Type), - } + tags = cleanTags(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, + "ip": s.IP, + }) + data = u.processUAPstats(s.Stat.Ap) + data["bytes"] = s.Bytes.Val + data["last_seen"] = s.LastSeen.Val + data["rx_bytes"] = s.RxBytes.Val + data["tx_bytes"] = s.TxBytes.Val + data["uptime"] = s.Uptime.Val + data["state"] = s.State.Val + data["user-num_sta"] = s.UserNumSta.Val + data["guest-num_sta"] = s.GuestNumSta.Val + data["num_sta"] = s.NumSta.Val metricName = metricNamespace("uap") - u.reportUAPstats(s.Stat.Ap, r, metricName, tags) + reportGaugeForFloat64Map(r, metricName, data, tags) - data = map[string]float64{ - "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, - "user-num_sta": s.UserNumSta.Val, - "guest-num_sta": s.GuestNumSta.Val, - "num_sta": s.NumSta.Val, - } - for name, value := range data { - r.reportGauge(metricName(name), value, tags) - } - - u.reportRadTable(r, s.Name, s.SiteName, s.SourceName, *s.RadioTable, *s.RadioTableStats) - u.reportVAPTable(r, s.Name, s.SiteName, s.SourceName, *s.VapTable) + u.processRadTable(r, tags, *s.RadioTable, *s.RadioTableStats) + u.processVAPTable(r, tags, *s.VapTable) } diff --git a/integrations/datadogunifi/usg.go b/integrations/datadogunifi/usg.go index 613c4c6c..269793c7 100644 --- a/integrations/datadogunifi/usg.go +++ b/integrations/datadogunifi/usg.go @@ -1,56 +1,65 @@ package datadogunifi import ( - "github.com/unifi-poller/unifi" + "github.com/unpoller/unifi" ) -// reportUSG generates Unifi Gateway datapoints for Datadog. +// usgT is used as a name for printed/logged counters. +const usgT = item("USG") + +// batchUSG generates Unifi Gateway datapoints for Datadog. // These points can be passed directly to datadog. -func (u *DatadogUnifi) reportUSG(r report, s *unifi.USG) { +func (u *DatadogUnifi) batchUSG(r report, s *unifi.USG) { if !s.Adopted.Val || s.Locating.Val { return } - tags := []string{ - tag("mac", s.Mac), - tag("site_name", s.SiteName), - tag("source", s.SourceName), - tag("name", s.Name), - tag("version", s.Version), - tag("model", s.Model), - tag("serial", s.Serial), - tag("type", s.Type), - tag("ip", s.IP), - tag("license_state", s.LicenseState), + 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, + "ip": s.IP, + "license_state": s.LicenseState, } + data := CombineFloat64( + u.batchUDMtemps(s.Temperatures), + u.batchSysStats(s.SysStats, s.SystemStats), + u.batchUSGstats(s.SpeedtestStatus, s.Stat.Gw, s.Uplink), + map[string]float64{ + "bytes": s.Bytes.Val, + "last_seen": s.LastSeen.Val, + "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, + "num_desktop": s.NumDesktop.Val, + "num_handheld": s.NumHandheld.Val, + "num_mobile": s.NumMobile.Val, + }, + ) + + r.addCount(usgT) + metricName := metricNamespace("usg") - u.reportSysStats(r, metricName, s.SysStats, s.SystemStats, tags) - u.reportUSGstats(r, metricName, tags, s.SpeedtestStatus, s.Stat.Gw, s.Uplink) + reportGaugeForFloat64Map(r, metricName, data, tags) - data := map[string]float64{ - "bytes": s.Bytes.Val, - "last_seen": s.LastSeen.Val, - "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, - "num_desktop": s.NumDesktop.Val, - "num_handheld": s.NumHandheld.Val, - "num_mobile": s.NumMobile.Val, - } - reportGaugeForMap(r, metricName, data, tags) - - u.reportNetTable(r, s.Name, s.SiteName, s.SourceName, s.NetworkTable) - u.reportUSGwans(r, s.Name, s.SiteName, s.SourceName, s.Wan1, s.Wan2) + u.batchNetTable(r, tags, s.NetworkTable) + u.batchUSGwans(r, tags, s.Wan1, s.Wan2) } -func (u *DatadogUnifi) reportUSGstats(r report, metricName func(string) string, tags []string, ss unifi.SpeedtestStatus, gw *unifi.Gw, ul unifi.Uplink) { +func (u *DatadogUnifi) batchUSGstats(ss unifi.SpeedtestStatus, gw *unifi.Gw, ul unifi.Uplink) map[string]float64 { if gw == nil { - return + return map[string]float64{} } - data := map[string]float64{ + + return map[string]float64{ "uplink_latency": ul.Latency.Val, "uplink_speed": ul.Speed.Val, "speedtest-status_latency": ss.Latency.Val, @@ -65,33 +74,32 @@ func (u *DatadogUnifi) reportUSGstats(r report, metricName func(string) string, "lan-tx_packets": gw.LanTxPackets.Val, "lan-rx_dropped": gw.LanRxDropped.Val, } - reportGaugeForMap(r, metricName, data, tags) } -func (u *DatadogUnifi) reportUSGwans(r report, deviceName string, siteName string, source string, wans ...unifi.Wan) { +func (u *DatadogUnifi) batchUSGwans(r report, tags map[string]string, wans ...unifi.Wan) { for _, wan := range wans { if !wan.Up.Val { continue } - tags := []string{ - tag("device_name", deviceName), - tag("site_name", siteName), - tag("source", source), - tag("ip", wan.IP), - tag("purpose", wan.Name), - tag("mac", wan.Mac), - tag("ifname", wan.Ifname), - tag("type", wan.Type), - tag("up", wan.Up.Txt), - tag("enabled", wan.Enable.Txt), - tag("gateway", wan.Gateway), - } - fullDuplex := float64(0) - if wan.FullDuplex.Val { - fullDuplex = 1 - } + tags := cleanTags(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, + "gateway": wan.Gateway, + }) + fullDuplex := 0.0 + if wan.FullDuplex.Val { + fullDuplex = 1.0 + } data := map[string]float64{ "bytes-r": wan.BytesR.Val, "full_duplex": fullDuplex, @@ -112,26 +120,27 @@ func (u *DatadogUnifi) reportUSGwans(r report, deviceName string, siteName strin "tx_broadcast": wan.TxBroadcast.Val, "tx_multicast": wan.TxMulticast.Val, } - metricName := metricNamespace("usg_wan_ports") - reportGaugeForMap(r, metricName, data, tags) + + metricName := metricNamespace("usg.wan_ports") + reportGaugeForFloat64Map(r, metricName, data, tags) } } -func (u *DatadogUnifi) reportNetTable(r report, deviceName string, siteName string, source string, nt unifi.NetworkTable) { +func (u *DatadogUnifi) batchNetTable(r report, tags map[string]string, nt unifi.NetworkTable) { for _, p := range nt { - tags := []string{ - tag("device_name", deviceName), - tag("site_name", siteName), - tag("source", source), - tag("up", p.Up.Txt), - tag("enabled", p.Enabled.Txt), - tag("ip", p.IP), - tag("mac", p.Mac), - tag("name", p.Name), - tag("domain_name", p.DomainName), - tag("purpose", p.Purpose), - tag("is_guest", p.IsGuest.Txt), - } + tags := cleanTags(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, + }) data := map[string]float64{ "num_sta": p.NumSta.Val, "rx_bytes": p.RxBytes.Val, @@ -139,7 +148,8 @@ func (u *DatadogUnifi) reportNetTable(r report, deviceName string, siteName stri "tx_bytes": p.TxBytes.Val, "tx_packets": p.TxPackets.Val, } - metricName := metricNamespace("usg_networks") - reportGaugeForMap(r, metricName, data, tags) + + metricName := metricNamespace("usg.networks") + reportGaugeForFloat64Map(r, metricName, data, tags) } } diff --git a/integrations/datadogunifi/usw.go b/integrations/datadogunifi/usw.go index 1dd4ea4b..9d0f7948 100644 --- a/integrations/datadogunifi/usw.go +++ b/integrations/datadogunifi/usw.go @@ -1,55 +1,60 @@ package datadogunifi import ( - "fmt" - - "github.com/unifi-poller/unifi" + "github.com/unpoller/unifi" ) -// reportUSW generates Unifi Switch datapoints for Datadog. +// uswT is used as a name for printed/logged counters. +const uswT = item("USW") + +// batchUSW generates Unifi Switch datapoints for Datadog. // These points can be passed directly to datadog. -func (u *DatadogUnifi) reportUSW(r report, s *unifi.USW) { +func (u *DatadogUnifi) batchUSW(r report, s *unifi.USW) { if !s.Adopted.Val || s.Locating.Val { return } - tags := []string{ - tag("mac", s.Mac), - tag("site_name", s.SiteName), - tag("source", s.SourceName), - tag("name", s.Name), - tag("version", s.Version), - tag("model", s.Model), - tag("serial", s.Serial), - tag("type", s.Type), - tag("ip", s.IP), - } - metricName := metricNamespace("usw") - u.reportUSWstat(r, metricName, tags, s.Stat.Sw) - u.reportSysStats(r, metricName, s.SysStats, s.SystemStats, tags) + tags := cleanTags(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, + "ip": s.IP, + }) + data := CombineFloat64( + u.batchUSWstat(s.Stat.Sw), + u.batchSysStats(s.SysStats, s.SystemStats), + map[string]float64{ + "guest-num_sta": s.GuestNumSta.Val, - data := map[string]float64{ - "guest-num_sta": s.GuestNumSta.Val, - "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, - } - reportGaugeForMap(r, metricName, data, tags) - u.reportPortTable(r, s.Name, s.SiteName, s.SourceName, s.Type, s.PortTable) + "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.addCount(uswT) + metricName := metricNamespace("usw") + reportGaugeForFloat64Map(r, metricName, data, tags) + + u.batchPortTable(r, tags, s.PortTable) } -func (u *DatadogUnifi) reportUSWstat(r report, metricName func(string) string, tags []string, sw *unifi.Sw) { +func (u *DatadogUnifi) batchUSWstat(sw *unifi.Sw) map[string]float64 { if sw == nil { - return + return map[string]float64{} } - data := map[string]float64{ + return map[string]float64{ "stat_bytes": sw.Bytes.Val, "stat_rx_bytes": sw.RxBytes.Val, "stat_rx_crypts": sw.RxCrypts.Val, @@ -63,30 +68,35 @@ func (u *DatadogUnifi) reportUSWstat(r report, metricName func(string) string, t "stat_tx_packets": sw.TxPackets.Val, "stat_tx_retries": sw.TxRetries.Val, } - reportGaugeForMap(r, metricName, data, tags) } -func (u *DatadogUnifi) reportPortTable(r report, deviceName string, siteName string, source string, typeTag string, pt []unifi.Port) { +//nolint:funlen +func (u *DatadogUnifi) batchPortTable(r report, t map[string]string, pt []unifi.Port) { for _, p := range pt { - if !p.Up.Val || !p.Enable.Val { + if !u.DeadPorts && (!p.Up.Val || !p.Enable.Val) { continue // only record UP ports. } - tags := []string{ - tag("site_name", siteName), - tag("device_name", deviceName), - tag("source", source), - tag("type", typeTag), - tag("name", p.Name), - tag("poe_mode", p.PoeMode), - tag("port_poe", p.PortPoe.Txt), - tag("port_idx", p.PortIdx.Txt), - tag("port_id", fmt.Sprintf("%s_port_%s", deviceName, p.PortIdx.Txt)), - tag("poe_enable", p.PoeEnable.Txt), - tag("flowctrl_rx", p.FlowctrlRx.Txt), - tag("flowctrl_tx", p.FlowctrlTx.Txt), - tag("media", p.Media), - } + tags := cleanTags(map[string]string{ + "site_name": t["site_name"], + "device_name": t["name"], + "source": t["source"], + "type": t["type"], + "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, + "has_sfp": p.SFPFound.Txt, + "sfp_compliance": p.SFPCompliance, + "sfp_serial": p.SFPSerial, + "sfp_vendor": p.SFPVendor, + "sfp_part": p.SFPPart, + }) data := map[string]float64{ "dbytes_r": p.BytesR.Val, "rx_broadcast": p.RxBroadcast.Val, @@ -113,7 +123,15 @@ func (u *DatadogUnifi) reportPortTable(r report, deviceName string, siteName str data["poe_voltage"] = p.PoeVoltage.Val } - metricName := metricNamespace("usw_ports") - reportGaugeForMap(r, metricName, data, tags) + if p.SFPFound.Val { + data["sfp_current"] = p.SFPCurrent.Val + data["sfp_voltage"] = p.SFPVoltage.Val + data["sfp_temperature"] = p.SFPTemperature.Val + data["sfp_txpower"] = p.SFPTxpower.Val + data["sfp_rxpower"] = p.SFPRxpower.Val + } + + metricName := metricNamespace("usw.ports") + reportGaugeForFloat64Map(r, metricName, data, tags) } } diff --git a/integrations/datadogunifi/uxg.go b/integrations/datadogunifi/uxg.go index a18029d3..df92d9d8 100644 --- a/integrations/datadogunifi/uxg.go +++ b/integrations/datadogunifi/uxg.go @@ -1 +1,83 @@ package datadogunifi + +import ( + "github.com/unpoller/unifi" +) + +// uxgT is used as a name for printed/logged counters. +const uxgT = item("UXG") + +// batchUXG generates 10Gb Unifi Gateway datapoints for Datadog. +// These points can be passed directly to datadog. +func (u *DatadogUnifi) batchUXG(r report, s *unifi.UXG) { // nolint: funlen + if !s.Adopted.Val || s.Locating.Val { + return + } + + tags := cleanTags(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, + "ip": s.IP, + "license_state": s.LicenseState, + }) + data := CombineFloat64( + u.batchUDMstorage(s.Storage), + u.batchUDMtemps(s.Temperatures), + u.batchUSGstats(s.SpeedtestStatus, s.Stat.Gw, s.Uplink), + u.batchSysStats(s.SysStats, s.SystemStats), + map[string]float64{ + "bytes": s.Bytes.Val, + "last_seen": s.LastSeen.Val, + "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, + "num_desktop": s.NumDesktop.Val, + "num_handheld": s.NumHandheld.Val, + "num_mobile": s.NumMobile.Val, + }, + ) + + r.addCount(uxgT) + + metricName := metricNamespace("usg") + reportGaugeForFloat64Map(r, metricName, data, tags) + + u.batchNetTable(r, tags, s.NetworkTable) + u.batchUSGwans(r, tags, s.Wan1, s.Wan2) + + tags = cleanTags(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, + "ip": s.IP, + }) + data = CombineFloat64( + u.batchUSWstat(s.Stat.Sw), + map[string]float64{ + "guest_num_sta": s.GuestNumSta.Val, + "bytes": s.Bytes.Val, + "last_seen": s.LastSeen.Val, + "rx_bytes": s.RxBytes.Val, + "tx_bytes": s.TxBytes.Val, + "uptime": s.Uptime.Val, + }) + + metricName = metricNamespace("usw") + reportGaugeForFloat64Map(r, metricName, data, tags) + + u.batchPortTable(r, tags, s.PortTable) // udm has a usw in it. +}