diff --git a/integrations/datadogunifi/.gitignore b/integrations/datadogunifi/.gitignore new file mode 100644 index 00000000..4c6c1ca3 --- /dev/null +++ b/integrations/datadogunifi/.gitignore @@ -0,0 +1,17 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +.vscode/ + +# Dependency directories (remove the comment below to include it) +vendor/ diff --git a/integrations/datadogunifi/.travis.yml b/integrations/datadogunifi/.travis.yml new file mode 100644 index 00000000..dd5ff174 --- /dev/null +++ b/integrations/datadogunifi/.travis.yml @@ -0,0 +1,9 @@ +language: go +go: +- 1.14.x +- 1.18.x +before_install: + # download super-linter: golangci-lint +- curl -sL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin latest +script: +- go test ./... diff --git a/integrations/datadogunifi/LICENSE b/integrations/datadogunifi/LICENSE new file mode 100644 index 00000000..f452416f --- /dev/null +++ b/integrations/datadogunifi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Cody Lee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/integrations/datadogunifi/README.md b/integrations/datadogunifi/README.md new file mode 100644 index 00000000..a3755288 --- /dev/null +++ b/integrations/datadogunifi/README.md @@ -0,0 +1,83 @@ +# datadogunifi + +UniFi Poller Output Plugin for DataDog + +## Configuration + +```yaml +datadog: + # How often to poll UniFi and report to Datadog. + interval: "2m" + + # To disable this output plugin + disable: false + + # Datadog Custom Options + + # address to talk to the datadog agent, by default this uses the local statsd UDP interface + # address: "..." + + # namespace to prepend to all data + # namespace: "" + + # tags to append to all data + # tags: + # - foo + + # max_bytes_per_payload is the maximum number of bytes a single payload will contain. + # The magic value 0 will set the option to the optimal size for the transport + # protocol used when creating the client: 1432 for UDP and 8192 for UDS. + # max_bytes_per_payload: 0 + + # max_messages_per_payload is the maximum number of metrics, events and/or service checks a single payload will contain. + # This option can be set to `1` to create an unbuffered client. + # max_messages_per_payload: 0 + + # BufferPoolSize is the size of the pool of buffers in number of buffers. + # The magic value 0 will set the option to the optimal size for the transport + # protocol used when creating the client: 2048 for UDP and 512 for UDS. + # buffer_pool_size: 0 + + # buffer_flush_interval is the interval after which the current buffer will get flushed. + # buffer_flush_interval: 0 + + # buffer_shard_count is the number of buffer "shards" that will be used. + # Those shards allows the use of multiple buffers at the same time to reduce + # lock contention. + # buffer_shard_count: 0 + + # sender_queue_size is the size of the sender queue in number of buffers. + # The magic value 0 will set the option to the optimal size for the transport + # protocol used when creating the client: 2048 for UDP and 512 for UDS. + # sender_queue_size: 0 + + # write_timeout_uds is the timeout after which a UDS packet is dropped. + # write_timeout_uds: 5000 + + # receive_mode determines the behavior of the client when receiving to many + # metrics. The client will either drop the metrics if its buffers are + # full (ChannelMode mode) or block the caller until the metric can be + # handled (MutexMode mode). By default the client will MutexMode. This + # option should be set to ChannelMode only when use under very high + # load. + # + # MutexMode uses a mutex internally which is much faster than + # channel but causes some lock contention when used with a high number + # of threads. Mutex are sharded based on the metrics name which + # limit mutex contention when goroutines send different metrics. + # + # ChannelMode: uses channel (of ChannelModeBufferSize size) to send + # metrics and drop metrics if the channel is full. Sending metrics in + # this mode is slower that MutexMode (because of the channel), but + # will not block the application. This mode is made for application + # using many goroutines, sending the same metrics at a very high + # volume. The goal is to not slow down the application at the cost of + # dropping metrics and having a lower max throughput. + # receive_mode: 0 + + # channel_mode_buffer_size is the size of the channel holding incoming metrics + # channel_mode_buffer_size: 0 + + # aggregation_flush_interval is the interval for the aggregator to flush metrics + # aggregation_flush_interval: 0 +``` \ No newline at end of file diff --git a/integrations/datadogunifi/alarms.go b/integrations/datadogunifi/alarms.go new file mode 100644 index 00000000..7db18b9c --- /dev/null +++ b/integrations/datadogunifi/alarms.go @@ -0,0 +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{ + "dst_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, + "dst_ip_asn": fmt.Sprintf("%d", event.DestIPGeo.Asn), + "dst_ip_latitude": fmt.Sprintf("%0.6f", event.DestIPGeo.Latitude), + "dst_ip_longitude": fmt.Sprintf("%0.6f", event.DestIPGeo.Longitude), + "dst_ip_city": event.DestIPGeo.City, + "dst_ip_continent_code": event.DestIPGeo.ContinentCode, + "dst_ip_country_code": event.DestIPGeo.CountryCode, + "dst_ip_country_name": event.DestIPGeo.CountryName, + "dst_ip_organization": event.DestIPGeo.Organization, + "src_ip_asn": fmt.Sprintf("%d", event.SourceIPGeo.Asn), + "src_ip_latitude": fmt.Sprintf("%0.6f", event.SourceIPGeo.Latitude), + "src_ip_longitude": fmt.Sprintf("%0.6f", event.SourceIPGeo.Longitude), + "src_ip_city": event.SourceIPGeo.City, + "src_ip_continent_code": event.SourceIPGeo.ContinentCode, + "src_ip_country_code": event.SourceIPGeo.CountryCode, + "src_ip_country_name": event.SourceIPGeo.CountryName, + "src_ip_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, + "usg_ip": 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 - %s", event.Datetime.Unix(), title, event.Msg, tagMapToSimpleStrings(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 - %s", event.Datetime.Unix(), title, event.Anomaly, tagMapToSimpleStrings(tagMap))) +} diff --git a/integrations/datadogunifi/clients.go b/integrations/datadogunifi/clients.go new file mode 100644 index 00000000..8c2ef504 --- /dev/null +++ b/integrations/datadogunifi/clients.go @@ -0,0 +1,189 @@ +package datadogunifi + +import ( + "github.com/unpoller/unifi" +) + +// 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_fixed_ip": 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), + } + + metricName := metricNamespace("clients") + + 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) 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 := 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("client_dpi") + + reportGaugeForFloat64Map(r, metricName, data, tags) + } +} + +// fillDPIMapTotals fills in totals for categories and applications. maybe clients too. +// 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) + } + + if m[controller][site] == nil { + m[controller][site] = make(map[string]unifi.DPIData) + } + + existing := m[controller][site][name] + existing.TxPackets += dpi.TxPackets + existing.RxPackets += dpi.RxPackets + existing.TxBytes += dpi.TxBytes + existing.RxBytes += dpi.RxBytes + m[controller][site][name] = existing +} + +func reportClientDPItotals(r report, appTotal, catTotal totalsDPImap) { + type all []struct { + kind string + val totalsDPImap + } + + // This produces 7000+ metrics per site. Disabled for now. + if appTotal != nil { + appTotal = nil + } + + // This can allow us to aggregate other data types later, like `name` or `mac`, or anything else unifi adds. + a := all{ + // This produces 7000+ metrics per site. Disabled for now. + { + kind: "application", + val: appTotal, + }, + { + kind: "category", + val: catTotal, + }, + } + + for _, k := range a { + for controller, s := range k.val { + for site, c := range s { + for name, m := range c { + 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("client_dpi") + + reportGaugeForFloat64Map(r, metricName, data, tags) + } + } + } + } +} diff --git a/integrations/datadogunifi/datadog.go b/integrations/datadogunifi/datadog.go new file mode 100644 index 00000000..37630cc1 --- /dev/null +++ b/integrations/datadogunifi/datadog.go @@ -0,0 +1,361 @@ +// Package datadogunifi provides the methods to turn UniFi measurements into Datadog +// data points with appropriate tags and fields. +package datadogunifi + +import ( + "reflect" + "time" + + "github.com/DataDog/datadog-go/statsd" + "github.com/unpoller/poller" + "github.com/unpoller/unifi" + "golift.io/cnfg" +) + +const ( + defaultInterval = 30 * time.Second + minimumInterval = 10 * time.Second +) + +// Config defines the data needed to store metrics in Datadog. +type Config struct { + // Required Config + + // 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 + Address string `json:"address" toml:"address" xml:"address,attr" yaml:"address"` + + // Optional Statsd Options - mirrored from statsd.Options + + // Namespace to prepend to all metrics, events and service checks name. + Namespace *string `json:"namespace" toml:"namespace" xml:"namespace,attr" yaml:"namespace"` + + // Tags are global tags to be applied to every metrics, events and service checks. + Tags []string `json:"tags" toml:"tags" xml:"tags,attr" yaml:"tags"` + + // MaxBytesPerPayload is the maximum number of bytes a single payload will contain. + // The magic value 0 will set the option to the optimal size for the transport + // protocol used when creating the client: 1432 for UDP and 8192 for UDS. + MaxBytesPerPayload *int `json:"max_bytes_per_payload" toml:"max_bytes_per_payload" xml:"max_bytes_per_payload,attr" yaml:"max_bytes_per_payload"` + + // MaxMessagesPerPayload is the maximum number of metrics, events and/or service checks a single payload will contain. + // This option can be set to `1` to create an unbuffered client. + MaxMessagesPerPayload *int `json:"max_messages_per_payload" toml:"max_messages_per_payload" xml:"max_messages_per_payload,attr" yaml:"max_messages_per_payload"` + + // BufferPoolSize is the size of the pool of buffers in number of buffers. + // The magic value 0 will set the option to the optimal size for the transport + // protocol used when creating the client: 2048 for UDP and 512 for UDS. + BufferPoolSize *int `json:"buffer_pool_size" toml:"buffer_pool_size" xml:"buffer_pool_size,attr" yaml:"buffer_pool_size"` + + // BufferFlushInterval is the interval after which the current buffer will get flushed. + BufferFlushInterval *cnfg.Duration `json:"buffer_flush_interval" toml:"buffer_flush_interval" xml:"buffer_flush_interval,attr" yaml:"buffer_flush_interval"` + + // BufferShardCount is the number of buffer "shards" that will be used. + // Those shards allows the use of multiple buffers at the same time to reduce + // lock contention. + BufferShardCount *int `json:"buffer_shard_count" toml:"buffer_shard_count" xml:"buffer_shard_count,attr" yaml:"buffer_shard_count"` + + // SenderQueueSize is the size of the sender queue in number of buffers. + // The magic value 0 will set the option to the optimal size for the transport + // protocol used when creating the client: 2048 for UDP and 512 for UDS. + SenderQueueSize *int `json:"sender_queue_size" toml:"sender_queue_size" xml:"sender_queue_size,attr" yaml:"sender_queue_size"` + + // WriteTimeoutUDS is the timeout after which a UDS packet is dropped. + WriteTimeoutUDS *cnfg.Duration `json:"write_timeout_uds" toml:"write_timeout_uds" xml:"write_timeout_uds,attr" yaml:"write_timeout_uds"` + + // ReceiveMode determines the behavior of the client when receiving to many + // metrics. The client will either drop the metrics if its buffers are + // full (ChannelMode mode) or block the caller until the metric can be + // handled (MutexMode mode). By default the client will MutexMode. This + // option should be set to ChannelMode only when use under very high + // load. + // + // MutexMode uses a mutex internally which is much faster than + // channel but causes some lock contention when used with a high number + // of threads. Mutex are sharded based on the metrics name which + // limit mutex contention when goroutines send different metrics. + // + // ChannelMode: uses channel (of ChannelModeBufferSize size) to send + // metrics and drop metrics if the channel is full. Sending metrics in + // this mode is slower that MutexMode (because of the channel), but + // will not block the application. This mode is made for application + // using many goroutines, sending the same metrics at a very high + // volume. The goal is to not slow down the application at the cost of + // dropping metrics and having a lower max throughput. + ReceiveMode *statsd.ReceivingMode `json:"receive_mode" toml:"receive_mode" xml:"receive_mode,attr" yaml:"receive_mode"` + + // ChannelModeBufferSize is the size of the channel holding incoming metrics + ChannelModeBufferSize *int `json:"channel_mode_buffer_size" toml:"channel_mode_buffer_size" xml:"channel_mode_buffer_size,attr" yaml:"channel_mode_buffer_size"` + + // AggregationFlushInterval is the interval for the aggregator to flush metrics + AggregationFlushInterval *time.Duration `json:"aggregation_flush_interval" toml:"aggregation_flush_interval" xml:"aggregation_flush_interval,attr" yaml:"aggregation_flush_interval"` +} + +// Datadog allows the data to be context aware with configuration +type Datadog struct { + *Config `json:"datadog" toml:"datadog" xml:"datadog" yaml:"datadog"` + options []statsd.Option // nolint +} + +// DatadogUnifi is returned by New() after you provide a Config. +type DatadogUnifi struct { + Collector poller.Collect + datadog statsd.ClientInterface + LastCheck time.Time + *Datadog +} + +func init() { // nolint: gochecknoinits + u := &DatadogUnifi{Datadog: &Datadog{}, LastCheck: time.Now()} + + poller.NewOutput(&poller.Output{ + Name: "datadog", + Config: u.Datadog, + Method: u.Run, + }) +} + +func (u *DatadogUnifi) setConfigDefaults() { + if u.Interval.Duration == 0 { + u.Interval = cnfg.Duration{Duration: defaultInterval} + } else if u.Interval.Duration < minimumInterval { + u.Interval = cnfg.Duration{Duration: minimumInterval} + } + + u.Interval = cnfg.Duration{Duration: u.Interval.Duration.Round(time.Second)} + + u.options = make([]statsd.Option, 0) + + if u.Namespace != nil { + u.options = append(u.options, statsd.WithNamespace(*u.Namespace)) + } + + if u.Tags != nil && len(u.Tags) > 0 { + u.options = append(u.options, statsd.WithTags(u.Tags)) + } + + if u.MaxBytesPerPayload != nil { + u.options = append(u.options, statsd.WithMaxBytesPerPayload(*u.MaxBytesPerPayload)) + } + + if u.MaxMessagesPerPayload != nil { + u.options = append(u.options, statsd.WithMaxMessagesPerPayload(*u.MaxMessagesPerPayload)) + } + + if u.BufferPoolSize != nil { + u.options = append(u.options, statsd.WithBufferPoolSize(*u.BufferPoolSize)) + } + + if u.BufferFlushInterval != nil { + u.options = append(u.options, statsd.WithBufferFlushInterval((*u.BufferFlushInterval).Duration)) + } + + if u.BufferShardCount != nil { + u.options = append(u.options, statsd.WithBufferShardCount(*u.BufferShardCount)) + } + + if u.SenderQueueSize != nil { + u.options = append(u.options, statsd.WithSenderQueueSize(*u.SenderQueueSize)) + } + + if u.WriteTimeoutUDS != nil { + u.options = append(u.options, statsd.WithWriteTimeoutUDS((*u.WriteTimeoutUDS).Duration)) + } + + if u.ReceiveMode != nil { + switch *u.ReceiveMode { + case statsd.ChannelMode: + u.options = append(u.options, statsd.WithChannelMode()) + case statsd.MutexMode: + u.options = append(u.options, statsd.WithMutexMode()) + } + } + + if u.ChannelModeBufferSize != nil { + u.options = append(u.options, statsd.WithChannelModeBufferSize(*u.ChannelModeBufferSize)) + } + + if u.AggregationFlushInterval != nil { + u.options = append(u.options, statsd.WithAggregationInterval(*u.AggregationFlushInterval)) + } + +} + +// Run runs a ticker to poll the unifi server and update Datadog. +func (u *DatadogUnifi) Run(c poller.Collect) error { + u.Collector = c + disabled := u.Disable == nil || *u.Disable == true + if disabled { + u.LogDebugf("Datadog config is disabled, output is disabled.") + return nil + } + if u.Config == nil && !disabled { + u.LogErrorf("DataDog config is missing and is not disabled: Datadog output is disabled!") + return nil + } + u.Logf("Datadog is configured.") + u.setConfigDefaults() + + var err error + u.datadog, err = statsd.New(u.Address, u.options...) + if err != nil { + u.LogErrorf("Error configuration Datadog agent reporting: %+v", err) + return err + } + + u.PollController() + + return nil +} + +// PollController runs forever, polling UniFi and pushing to Datadog +// This is started by Run() or RunBoth() after everything is validated. +func (u *DatadogUnifi) PollController() { + interval := u.Interval.Round(time.Second) + ticker := time.NewTicker(interval) + u.Logf("Everything checks out! Poller started, interval=%+v", interval) + + for u.LastCheck = range ticker.C { + metrics, err := u.Collector.Metrics(&poller.Filter{Name: "unifi"}) + if err != nil { + u.LogErrorf("metric fetch for Datadog failed: %v", err) + continue + } + + events, err := u.Collector.Events(&poller.Filter{Name: "unifi", Dur: interval}) + if err != nil { + u.LogErrorf("event fetch for Datadog failed", err) + continue + } + + report, err := u.ReportMetrics(metrics, events) + if err != nil { + // Is the agent down? + u.LogErrorf("unable to report metrics and events", err) + report.reportCount("unifi.collect.errors", 1, []string{}) + continue + } + report.reportCount("unifi.collect.success", 1, []string{}) + u.LogDatadogReport(report) + } +} + +// 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, e *poller.Events) (*Report, error) { + r := &Report{ + Metrics: m, + Events: e, + Start: time.Now(), + Counts: &Counts{Val: make(map[item]int)}, + Collector: u.Collector, + client: u.datadog, + } + // 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 +} + +// loopPoints collects all the data to immediately report to Datadog. +func (u *DatadogUnifi) loopPoints(r report) { + m := r.metrics() + + for _, s := range m.RogueAPs { + u.switchExport(r, s) + } + + for _, s := range m.Sites { + 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.batchClientDPI(r, s, appTotal, catTotal) + } + + reportClientDPItotals(r, appTotal, catTotal) +} + +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.LogErrorf("invalid export, type=%+v", reflect.TypeOf(v)) + } +} + +// LogDatadogReport writes a log message after exporting to Datadog. +func (u *DatadogUnifi) LogDatadogReport(r *Report) { + m := r.Metrics + u.Logf("UniFi Metrics Recorded num_sites=%d num_sites_dpi=%d num_clients=%d num_clients_dpi=%d num_rogue_ap=%d num_devices=%d errors=%v elapsec=%v", + len(m.Sites), + len(m.SitesDPI), + len(m.Clients), + len(m.ClientsDPI), + len(m.RogueAPs), + len(m.Devices), + r.Errors, + r.Elapsed, + ) + metricName := metricNamespace("collector") + r.reportCount(metricName("num_sites"), int64(len(m.Sites)), u.Tags) + r.reportCount(metricName("num_sites_dpi"), int64(len(m.SitesDPI)), u.Tags) + r.reportCount(metricName("num_clients"), int64(len(m.Clients)), u.Tags) + r.reportCount(metricName("num_clients_dpi"), int64(len(m.ClientsDPI)), u.Tags) + r.reportCount(metricName("num_rogue_ap"), int64(len(m.RogueAPs)), u.Tags) + r.reportCount(metricName("num_devices"), int64(len(m.Devices)), u.Tags) + r.reportCount(metricName("num_errors"), int64(len(r.Errors)), u.Tags) + r.reportTiming(metricName("elapsed_time"), r.Elapsed, u.Tags) +} diff --git a/integrations/datadogunifi/events.go b/integrations/datadogunifi/events.go new file mode 100644 index 00000000..ce0b0c4a --- /dev/null +++ b/integrations/datadogunifi/events.go @@ -0,0 +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, + "dst_ip_asn": fmt.Sprintf("%d", i.DestIPGeo.Asn), + "dst_ip_latitude": fmt.Sprintf("%0.6f", i.DestIPGeo.Latitude), + "dst_ip_longitude": fmt.Sprintf("%0.6f", i.DestIPGeo.Longitude), + "dst_ip_city": i.DestIPGeo.City, + "dst_ip_continent_code": i.DestIPGeo.ContinentCode, + "dst_ip_country_code": i.DestIPGeo.CountryCode, + "dst_ip_country_name": i.DestIPGeo.CountryName, + "dst_ip_organization": i.DestIPGeo.Organization, + "src_ip_asn": fmt.Sprintf("%d", i.SourceIPGeo.Asn), + "src_ip_latitude": fmt.Sprintf("%0.6f", i.SourceIPGeo.Latitude), + "src_ip_longitude": fmt.Sprintf("%0.6f", i.SourceIPGeo.Longitude), + "src_ip_city": i.SourceIPGeo.City, + "src_ip_continent_code": i.SourceIPGeo.ContinentCode, + "src_ip_country_code": i.SourceIPGeo.CountryCode, + "src_ip_country_name": i.SourceIPGeo.CountryName, + "src_ip_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, + "usg_ip": 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 - %s", i.Datetime.Unix(), title, i.Msg, tagMapToSimpleStrings(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), + "dst_ip": i.DestIP, + "dst_mac": i.DstMAC, + "ip": i.IP, + "src_ip": i.SrcIP, + "src_mac": i.SrcMAC, + "dst_ip_asn": fmt.Sprintf("%d", i.DestIPGeo.Asn), + "dst_ip_latitude": fmt.Sprintf("%0.6f", i.DestIPGeo.Latitude), + "dst_ip_longitude": fmt.Sprintf("%0.6f", i.DestIPGeo.Longitude), + "dst_ip_city": i.DestIPGeo.City, + "dst_ip_continent_code": i.DestIPGeo.ContinentCode, + "dst_ip_country_code": i.DestIPGeo.CountryCode, + "dst_ip_country_name": i.DestIPGeo.CountryName, + "dst_ip_organization": i.DestIPGeo.Organization, + "src_ip_asn": fmt.Sprintf("%d", i.SourceIPGeo.Asn), + "src_ip_latitude": fmt.Sprintf("%0.6f", i.SourceIPGeo.Latitude), + "src_ip_longitude": fmt.Sprintf("%0.6f", i.SourceIPGeo.Longitude), + "src_ip_city": i.SourceIPGeo.City, + "src_ip_continent_code": i.SourceIPGeo.ContinentCode, + "src_ip_country_code": i.SourceIPGeo.CountryCode, + "src_ip_country_name": i.SourceIPGeo.CountryName, + "src_ip_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, + "usg_ip": 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 - %s", i.Datetime.Unix(), title, i.Msg, tagMapToSimpleStrings(tagMap))) +} diff --git a/integrations/datadogunifi/go.mod b/integrations/datadogunifi/go.mod new file mode 100644 index 00000000..4107ab01 --- /dev/null +++ b/integrations/datadogunifi/go.mod @@ -0,0 +1,13 @@ +module github.com/unpoller/datadogunifi + +go 1.16 + +require ( + github.com/DataDog/datadog-go v4.0.0+incompatible + github.com/kr/pretty v0.1.0 // indirect + github.com/unpoller/poller v0.0.0-20210623104748-50161c195d5e + github.com/unpoller/unifi v0.0.0-20221124010147-8d83427af67b + golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect + golift.io/cnfg v0.0.7 + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect +) diff --git a/integrations/datadogunifi/go.sum b/integrations/datadogunifi/go.sum new file mode 100644 index 00000000..05ad81c6 --- /dev/null +++ b/integrations/datadogunifi/go.sum @@ -0,0 +1,55 @@ +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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/unpoller/poller v0.0.0-20210623104748-50161c195d5e h1:tNBIBCmtc7whuhkjKyEzpU3OHzYHyGCBy/LERhHxh3A= +github.com/unpoller/poller v0.0.0-20210623104748-50161c195d5e/go.mod h1:AbDp60t5WlLSRELAliMJ0RFQpm/0yXpyolVSZqNtero= +github.com/unpoller/unifi v0.0.0-20221124010147-8d83427af67b h1:QMDlntRuc73sVRBMa3VO82p9gDzyub4sgLxaKwK4nHo= +github.com/unpoller/unifi v0.0.0-20221124010147-8d83427af67b/go.mod h1:pJGPtjikPcYO+rZMpgYOj6Zs044Dl4R+u3MsV3TMenk= +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/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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/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-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= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +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= +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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +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.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= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integrations/datadogunifi/logger.go b/integrations/datadogunifi/logger.go new file mode 100644 index 00000000..eddf5313 --- /dev/null +++ b/integrations/datadogunifi/logger.go @@ -0,0 +1,22 @@ +package datadogunifi + +// Logf logs a message. +func (u *DatadogUnifi) Logf(msg string, v ...interface{}) { + if u.Collector != nil { + u.Collector.Logf(msg, v...) + } +} + +// LogErrorf logs an error message. +func (u *DatadogUnifi) LogErrorf(msg string, v ...interface{}) { + if u.Collector != nil { + u.Collector.LogErrorf(msg, v...) + } +} + +// LogDebugf logs a debug message. +func (u *DatadogUnifi) LogDebugf(msg string, v ...interface{}) { + if u.Collector != nil { + u.Collector.LogDebugf(msg, v...) + } +} diff --git a/integrations/datadogunifi/points.go b/integrations/datadogunifi/points.go new file mode 100644 index 00000000..8c6832ee --- /dev/null +++ b/integrations/datadogunifi/points.go @@ -0,0 +1,49 @@ +package datadogunifi + +import ( + "fmt" + "strings" +) + +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 tagMapToSimpleStrings(tagMap map[string]string) string { + result := "" + for k, v := range tagMap { + result = fmt.Sprintf("%s%s=\"%v\", ", result, k, v) + } + return strings.TrimRight(result, ", ") +} + +func metricNamespace(namespace string) func(string) string { + return func(name string) string { + return fmt.Sprintf("unifi.%s.%s", namespace, name) + } +} + +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, 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 +} diff --git a/integrations/datadogunifi/report.go b/integrations/datadogunifi/report.go new file mode 100644 index 00000000..f86a1cfd --- /dev/null +++ b/integrations/datadogunifi/report.go @@ -0,0 +1,138 @@ +package datadogunifi + +import ( + "sync" + "time" + + "github.com/DataDog/datadog-go/statsd" + "github.com/unpoller/poller" +) + +// Report is a will report the current collection run data. +type Report struct { + Metrics *poller.Metrics + Events *poller.Events + Errors []error + Counts *Counts + Start time.Time + End time.Time + Elapsed time.Duration + + Collector poller.Collect + + 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, 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) + } +} + +func (r *Report) reportGauge(name string, value float64, tags []string) error { + return r.client.Gauge(name, value, tags, 1.0) +} + +func (r *Report) reportCount(name string, value int64, tags []string) error { + return r.client.Count(name, value, tags, 1.0) +} + +func (r *Report) reportDistribution(name string, value float64, tags []string) error { + return r.client.Distribution(name, value, tags, 1.0) +} + +func (r *Report) reportTiming(name string, value time.Duration, tags []string) error { + return r.client.Timing(name, value, tags, 1.0) +} + +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: date, + Tags: tags, + }) +} + +func (r *Report) reportInfoLog(message string, f ...interface{}) { + r.Collector.Logf(message, f) +} + +func (r *Report) reportWarnLog(message string, f ...interface{}) { + r.Collector.Logf(message, f) +} + +func (r *Report) reportServiceCheck(name string, status statsd.ServiceCheckStatus, message string, tags []string) error { + return r.client.ServiceCheck(&statsd.ServiceCheck{ + Name: name, + Status: status, + Timestamp: time.Now(), + Message: message, + Tags: tags, + }) +} diff --git a/integrations/datadogunifi/site.go b/integrations/datadogunifi/site.go new file mode 100644 index 00000000..f21dbc8a --- /dev/null +++ b/integrations/datadogunifi/site.go @@ -0,0 +1,80 @@ +package datadogunifi + +import ( + "github.com/unpoller/unifi" +) + +// reportSite generates Unifi Sites' datapoints for Datadog. +// These points can be passed directly to Datadog. +func (u *DatadogUnifi) reportSite(r report, s *unifi.Site) { + metricName := metricNamespace("subsystems") + + for _, h := range s.Health { + tags := []string{ + tag("name", s.Name), + tag("site_name", s.SiteName), + tag("source", s.SourceName), + tag("desc", s.Desc), + tag("status", h.Status), + tag("subsystem", h.Subsystem), + tag("wan_ip", h.WanIP), + tag("gw_name", h.GwName), + tag("lan_ip", h.LanIP), + } + + data := map[string]float64{ + "num_user": h.NumUser.Val, + "num_guest": h.NumGuest.Val, + "num_iot": h.NumIot.Val, + "tx_bytes_r": h.TxBytesR.Val, + "rx_bytes_r": h.RxBytesR.Val, + "num_ap": h.NumAp.Val, + "num_adopted": h.NumAdopted.Val, + "num_disabled": h.NumDisabled.Val, + "num_disconnected": h.NumDisconnected.Val, + "num_pending": h.NumPending.Val, + "num_gw": h.NumGw.Val, + "num_sta": h.NumSta.Val, + "gw_cpu": h.GwSystemStats.CPU.Val, + "gw_mem": h.GwSystemStats.Mem.Val, + "gw_uptime": h.GwSystemStats.Uptime.Val, + "latency": h.Latency.Val, + "uptime": h.Uptime.Val, + "drops": h.Drops.Val, + "xput_up": h.XputUp.Val, + "xput_down": h.XputDown.Val, + "speedtest_ping": h.SpeedtestPing.Val, + "speedtest_lastrun": h.SpeedtestLastrun.Val, + "num_sw": h.NumSw.Val, + "remote_user_num_active": h.RemoteUserNumActive.Val, + "remote_user_num_inactive": h.RemoteUserNumInactive.Val, + "remote_user_rx_bytes": h.RemoteUserRxBytes.Val, + "remote_user_tx_bytes": h.RemoteUserTxBytes.Val, + "remote_user_rx_packets": h.RemoteUserRxPackets.Val, + "remote_user_tx_packets": h.RemoteUserTxPackets.Val, + "num_new_alarms": s.NumNewAlarms.Val, + } + + for name, value := range data { + r.reportGauge(metricName(name), value, tags) + } + } +} + +func (u *DatadogUnifi) reportSiteDPI(r report, s *unifi.DPITable) { + for _, dpi := range s.ByApp { + metricName := metricNamespace("sitedpi") + + tags := []string{ + tag("category", unifi.DPICats.Get(dpi.Cat)), + tag("application", unifi.DPIApps.GetApp(dpi.Cat, dpi.App)), + tag("site_name", s.SiteName), + tag("source", s.SourceName), + } + + r.reportCount(metricName("tx_packets"), dpi.TxPackets, tags) + r.reportCount(metricName("rx_packets"), dpi.RxPackets, tags) + r.reportCount(metricName("tx_bytes"), dpi.TxBytes, tags) + r.reportCount(metricName("rx_bytes"), dpi.RxBytes, tags) + } +} diff --git a/integrations/datadogunifi/uap.go b/integrations/datadogunifi/uap.go new file mode 100644 index 00000000..62ebe3ae --- /dev/null +++ b/integrations/datadogunifi/uap.go @@ -0,0 +1,235 @@ +package datadogunifi + +import ( + "github.com/unpoller/unifi" +) + +// 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 := 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") + + reportGaugeForFloat64Map(r, metricName, data, tags) + + u.processVAPTable(r, tags, s.VapTable) + u.batchPortTable(r, tags, s.PortTable) +} + +func (u *DatadogUnifi) processUAPstats(ap *unifi.Ap) map[string]float64 { + if ap == nil { + return map[string]float64{} + } + + // Accumulative Statistics. + return map[string]float64{ + "stat_user-rx_packets": ap.UserRxPackets.Val, + "stat_guest-rx_packets": ap.GuestRxPackets.Val, + "stat_rx_packets": ap.RxPackets.Val, + "stat_user-rx_bytes": ap.UserRxBytes.Val, + "stat_guest-rx_bytes": ap.GuestRxBytes.Val, + "stat_rx_bytes": ap.RxBytes.Val, + "stat_user-rx_errors": ap.UserRxErrors.Val, + "stat_guest-rx_errors": ap.GuestRxErrors.Val, + "stat_rx_errors": ap.RxErrors.Val, + "stat_user-rx_dropped": ap.UserRxDropped.Val, + "stat_guest-rx_dropped": ap.GuestRxDropped.Val, + "stat_rx_dropped": ap.RxDropped.Val, + "stat_user-rx_crypts": ap.UserRxCrypts.Val, + "stat_guest-rx_crypts": ap.GuestRxCrypts.Val, + "stat_rx_crypts": ap.RxCrypts.Val, + "stat_user-rx_frags": ap.UserRxFrags.Val, + "stat_guest-rx_frags": ap.GuestRxFrags.Val, + "stat_rx_frags": ap.RxFrags.Val, + "stat_user-tx_packets": ap.UserTxPackets.Val, + "stat_guest-tx_packets": ap.GuestTxPackets.Val, + "stat_tx_packets": ap.TxPackets.Val, + "stat_user-tx_bytes": ap.UserTxBytes.Val, + "stat_guest-tx_bytes": ap.GuestTxBytes.Val, + "stat_tx_bytes": ap.TxBytes.Val, + "stat_user-tx_errors": ap.UserTxErrors.Val, + "stat_guest-tx_errors": ap.GuestTxErrors.Val, + "stat_tx_errors": ap.TxErrors.Val, + "stat_user-tx_dropped": ap.UserTxDropped.Val, + "stat_guest-tx_dropped": ap.GuestTxDropped.Val, + "stat_tx_dropped": ap.TxDropped.Val, + "stat_user-tx_retries": ap.UserTxRetries.Val, + "stat_guest-tx_retries": ap.GuestTxRetries.Val, + } +} + +// processVAPTable creates points for Wifi Radios. This works with several types of UAP-capable devices. +func (u *DatadogUnifi) processVAPTable(r report, t map[string]string, vt unifi.VapTable) { // nolint: funlen + for _, s := range vt { + tags := map[string]string{ + "device_name": t["name"], + "site_name": t["site_name"], + "source": t["source"], + "ap_mac": s.ApMac, + "bssid": s.Bssid, + "id": s.ID, + "name": s.Name, + "radio_name": s.RadioName, + "radio": s.Radio, + "essid": s.Essid, + "site_id": s.SiteID, + "usage": s.Usage, + "state": s.State, + "is_guest": s.IsGuest.Txt, + } + data := map[string]float64{ + "ccq": float64(s.Ccq), + "mac_filter_rejections": float64(s.MacFilterRejections), + "num_satisfaction_sta": s.NumSatisfactionSta.Val, + "avg_client_signal": s.AvgClientSignal.Val, + "satisfaction": s.Satisfaction.Val, + "satisfaction_now": s.SatisfactionNow.Val, + "num_sta": float64(s.NumSta), + "channel": s.Channel.Val, + "rx_bytes": s.RxBytes.Val, + "rx_crypts": s.RxCrypts.Val, + "rx_dropped": s.RxDropped.Val, + "rx_errors": s.RxErrors.Val, + "rx_frags": s.RxFrags.Val, + "rx_nwids": s.RxNwids.Val, + "rx_packets": s.RxPackets.Val, + "tx_bytes": s.TxBytes.Val, + "tx_dropped": s.TxDropped.Val, + "tx_errors": s.TxErrors.Val, + "tx_packets": s.TxPackets.Val, + "tx_power": s.TxPower.Val, + "tx_retries": s.TxRetries.Val, + "tx_combined_retries": s.TxCombinedRetries.Val, + "tx_data_mpdu_bytes": s.TxDataMpduBytes.Val, + "tx_rts_retries": s.TxRtsRetries.Val, + "tx_success": s.TxSuccess.Val, + "tx_total": s.TxTotal.Val, + "tx_tcp_goodbytes": s.TxTCPStats.Goodbytes.Val, + "tx_tcp_lat_avg": s.TxTCPStats.LatAvg.Val, + "tx_tcp_lat_max": s.TxTCPStats.LatMax.Val, + "tx_tcp_lat_min": s.TxTCPStats.LatMin.Val, + "rx_tcp_goodbytes": s.RxTCPStats.Goodbytes.Val, + "rx_tcp_lat_avg": s.RxTCPStats.LatAvg.Val, + "rx_tcp_lat_max": s.RxTCPStats.LatMax.Val, + "rx_tcp_lat_min": s.RxTCPStats.LatMin.Val, + "wifi_tx_latency_mov_avg": s.WifiTxLatencyMov.Avg.Val, + "wifi_tx_latency_mov_max": s.WifiTxLatencyMov.Max.Val, + "wifi_tx_latency_mov_min": s.WifiTxLatencyMov.Min.Val, + "wifi_tx_latency_mov_total": s.WifiTxLatencyMov.Total.Val, + "wifi_tx_latency_mov_cuont": s.WifiTxLatencyMov.TotalCount.Val, + } + + metricName := metricNamespace("uap_vaps") + + reportGaugeForFloat64Map(r, metricName, data, tags) + } +} + +func (u *DatadogUnifi) processRadTable(r report, t map[string]string, rt unifi.RadioTable, rts unifi.RadioTableStats) { + for _, p := range rt { + tags := map[string]string{ + "device_name": t["name"], + "site_name": t["site_name"], + "source": t["source"], + "channel": p.Channel.Txt, + "radio": p.Radio, + "ht": p.Ht.Txt, + } + data := map[string]float64{ + "current_antenna_gain": p.CurrentAntennaGain.Val, + "max_txpower": p.MaxTxpower.Val, + "min_txpower": p.MinTxpower.Val, + "nss": p.Nss.Val, + "radio_caps": p.RadioCaps.Val, + } + + for _, t := range rts { + if t.Name == p.Name { + data["ast_be_xmit"] = t.AstBeXmit.Val + data["channel"] = t.Channel.Val + data["cu_self_rx"] = t.CuSelfRx.Val + data["cu_self_tx"] = t.CuSelfTx.Val + data["cu_total"] = t.CuTotal.Val + data["ext_channel"] = t.Extchannel.Val + data["gain"] = t.Gain.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 + + break + } + } + + metricName := metricNamespace("uap_radios") + + reportGaugeForFloat64Map(r, metricName, data, tags) + } +} diff --git a/integrations/datadogunifi/udm.go b/integrations/datadogunifi/udm.go new file mode 100644 index 00000000..796beb95 --- /dev/null +++ b/integrations/datadogunifi/udm.go @@ -0,0 +1,196 @@ +package datadogunifi + +import ( + "strconv" + "strings" + + "github.com/unpoller/unifi" +) + +// 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, + "mem_used": s.MemUsed.Val, + "mem_buffer": s.MemBuffer.Val, + "mem_total": s.MemTotal.Val, + "cpu": ss.CPU.Val, + "mem": ss.Mem.Val, + "system_uptime": ss.Uptime.Val, + } + + 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) batchUDMtemps(temps []unifi.Temperature) map[string]float64 { + output := make(map[string]float64) + + for _, t := range temps { + output["temp_"+t.Name] = t.Value + } + + return output +} + +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) + + 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. + + if s.Stat.Ap == nil { + return // we're done now. the following code process UDM (non-pro) UAP data. + } + + 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") + reportGaugeForFloat64Map(r, metricName, data, tags) + + 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 new file mode 100644 index 00000000..95065e27 --- /dev/null +++ b/integrations/datadogunifi/usg.go @@ -0,0 +1,155 @@ +package datadogunifi + +import ( + "github.com/unpoller/unifi" +) + +// 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) batchUSG(r report, s *unifi.USG) { + if !s.Adopted.Val || s.Locating.Val { + return + } + + tags := map[string]string{ + "mac": s.Mac, + "site_name": s.SiteName, + "source": s.SourceName, + "name": s.Name, + "version": s.Version, + "model": s.Model, + "serial": s.Serial, + "type": s.Type, + "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") + reportGaugeForFloat64Map(r, metricName, data, tags) + + u.batchNetTable(r, tags, s.NetworkTable) + u.batchUSGwans(r, tags, s.Wan1, s.Wan2) +} + +func (u *DatadogUnifi) batchUSGstats(ss unifi.SpeedtestStatus, gw *unifi.Gw, ul unifi.Uplink) map[string]float64 { + if gw == nil { + return map[string]float64{} + } + + return map[string]float64{ + "uplink_latency": ul.Latency.Val, + "uplink_speed": ul.Speed.Val, + "speedtest_status_latency": ss.Latency.Val, + "speedtest_status_runtime": ss.Runtime.Val, + "speedtest_status_rundate": ss.Rundate.Val, + "speedtest_status_ping": ss.StatusPing.Val, + "speedtest_status_xput_download": ss.XputDownload.Val, + "speedtest_status_xput_upload": ss.XputUpload.Val, + "lan_rx_bytes": gw.LanRxBytes.Val, + "lan_rx_packets": gw.LanRxPackets.Val, + "lan_tx_bytes": gw.LanTxBytes.Val, + "lan_tx_packets": gw.LanTxPackets.Val, + "lan_rx_dropped": gw.LanRxDropped.Val, + } +} + +func (u *DatadogUnifi) batchUSGwans(r report, tags map[string]string, wans ...unifi.Wan) { + for _, wan := range wans { + if !wan.Up.Val { + continue + } + + 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, + "max_speed": wan.MaxSpeed.Val, + "rx_bytes": wan.RxBytes.Val, + "rx_bytes_r": wan.RxBytesR.Val, + "rx_dropped": wan.RxDropped.Val, + "rx_errors": wan.RxErrors.Val, + "rx_broadcast": wan.RxBroadcast.Val, + "rx_multicast": wan.RxMulticast.Val, + "rx_packets": wan.RxPackets.Val, + "speed": wan.Speed.Val, + "tx_bytes": wan.TxBytes.Val, + "tx_bytes_r": wan.TxBytesR.Val, + "tx_dropped": wan.TxDropped.Val, + "tx_errors": wan.TxErrors.Val, + "tx_packets": wan.TxPackets.Val, + "tx_broadcast": wan.TxBroadcast.Val, + "tx_multicast": wan.TxMulticast.Val, + } + + metricName := metricNamespace("usg.wan_ports") + reportGaugeForFloat64Map(r, metricName, data, tags) + } +} + +func (u *DatadogUnifi) batchNetTable(r report, tags map[string]string, nt unifi.NetworkTable) { + for _, p := range nt { + 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, + "rx_packets": p.RxPackets.Val, + "tx_bytes": p.TxBytes.Val, + "tx_packets": p.TxPackets.Val, + } + + metricName := metricNamespace("usg.networks") + reportGaugeForFloat64Map(r, metricName, data, tags) + } +} diff --git a/integrations/datadogunifi/usw.go b/integrations/datadogunifi/usw.go new file mode 100644 index 00000000..aac3ae0b --- /dev/null +++ b/integrations/datadogunifi/usw.go @@ -0,0 +1,136 @@ +package datadogunifi + +import ( + "github.com/unpoller/unifi" +) + +// 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) batchUSW(r report, s *unifi.USW) { + if !s.Adopted.Val || s.Locating.Val { + return + } + + 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, + "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) batchUSWstat(sw *unifi.Sw) map[string]float64 { + if sw == nil { + return map[string]float64{} + } + + return map[string]float64{ + "stat_bytes": sw.Bytes.Val, + "stat_rx_bytes": sw.RxBytes.Val, + "stat_rx_crypts": sw.RxCrypts.Val, + "stat_rx_dropped": sw.RxDropped.Val, + "stat_rx_errors": sw.RxErrors.Val, + "stat_rx_frags": sw.RxFrags.Val, + "stat_rx_packets": sw.TxPackets.Val, + "stat_tx_bytes": sw.TxBytes.Val, + "stat_tx_dropped": sw.TxDropped.Val, + "stat_tx_errors": sw.TxErrors.Val, + "stat_tx_packets": sw.TxPackets.Val, + "stat_tx_retries": sw.TxRetries.Val, + } +} + +//nolint:funlen +func (u *DatadogUnifi) batchPortTable(r report, t map[string]string, pt []unifi.Port) { + for _, p := range pt { + if !u.DeadPorts && (!p.Up.Val || !p.Enable.Val) { + continue // only record UP ports. + } + + 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, + "flow_ctrl_rx": p.FlowctrlRx.Txt, + "flow_ctrl_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{ + "bytes_r": p.BytesR.Val, + "rx_broadcast": p.RxBroadcast.Val, + "rx_bytes": p.RxBytes.Val, + "rx_bytes_r": p.RxBytesR.Val, + "rx_dropped": p.RxDropped.Val, + "rx_errors": p.RxErrors.Val, + "rx_multicast": p.RxMulticast.Val, + "rx_packets": p.RxPackets.Val, + "speed": p.Speed.Val, + "stp_path_cost": p.StpPathcost.Val, + "tx_broadcast": p.TxBroadcast.Val, + "tx_bytes": p.TxBytes.Val, + "tx_bytes_r": p.TxBytesR.Val, + "tx_dropped": p.TxDropped.Val, + "tx_errors": p.TxErrors.Val, + "tx_multicast": p.TxMulticast.Val, + "tx_packets": p.TxPackets.Val, + } + + if p.PoeEnable.Val && p.PortPoe.Val { + data["poe_current"] = p.PoeCurrent.Val + data["poe_power"] = p.PoePower.Val + data["poe_voltage"] = p.PoeVoltage.Val + } + + if p.SFPFound.Val { + data["sfp_current"] = p.SFPCurrent.Val + data["sfp_voltage"] = p.SFPVoltage.Val + data["sfp_temperature"] = p.SFPTemperature.Val + data["sfp_tx_power"] = p.SFPTxpower.Val + data["sfp_rx_power"] = p.SFPRxpower.Val + } + + metricName := metricNamespace("usw.ports") + reportGaugeForFloat64Map(r, metricName, data, tags) + } +} diff --git a/integrations/datadogunifi/uxg.go b/integrations/datadogunifi/uxg.go new file mode 100644 index 00000000..c79f5e05 --- /dev/null +++ b/integrations/datadogunifi/uxg.go @@ -0,0 +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. +}