From 51916c19e74a32d2f77187929f2948edd1581a2c Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Sun, 11 Oct 2020 11:32:45 -0500 Subject: [PATCH] all stats should be reporting --- integrations/datadogunifi/clients.go | 162 ++++++++++++++++++++++ integrations/datadogunifi/datadog.go | 139 +++++++++++-------- integrations/datadogunifi/ids.go | 31 +++++ integrations/datadogunifi/points.go | 21 +++ integrations/datadogunifi/report.go | 76 ++++++++++ integrations/datadogunifi/site.go | 76 +++++++++- integrations/datadogunifi/uap.go | 199 +++++++++++++++++++++++++++ integrations/datadogunifi/udm.go | 141 +++++++++++++++++++ integrations/datadogunifi/usg.go | 145 +++++++++++++++++++ integrations/datadogunifi/usw.go | 119 ++++++++++++++++ 10 files changed, 1047 insertions(+), 62 deletions(-) create mode 100644 integrations/datadogunifi/ids.go create mode 100644 integrations/datadogunifi/points.go create mode 100644 integrations/datadogunifi/report.go create mode 100644 integrations/datadogunifi/uap.go create mode 100644 integrations/datadogunifi/udm.go create mode 100644 integrations/datadogunifi/usg.go create mode 100644 integrations/datadogunifi/usw.go diff --git a/integrations/datadogunifi/clients.go b/integrations/datadogunifi/clients.go index 80fc027a..91070964 100644 --- a/integrations/datadogunifi/clients.go +++ b/integrations/datadogunifi/clients.go @@ -4,3 +4,165 @@ import ( "github.com/unifi-poller/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), + } + + 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) +} + +// 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) { + 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), + } + 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) + } +} + +// 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. +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 := []string{ + tag("site_name", site), + tag("source", controller), + tag("name", 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) + } + } + } + } +} diff --git a/integrations/datadogunifi/datadog.go b/integrations/datadogunifi/datadog.go index 19fc981c..4f9e4be7 100644 --- a/integrations/datadogunifi/datadog.go +++ b/integrations/datadogunifi/datadog.go @@ -3,35 +3,35 @@ package datadogunifi import ( - "crypto/tls" "fmt" - "io/ioutil" "log" - "strings" "time" "github.com/DataDog/datadog-go/statsd" - "github.com/pkg/errors" "github.com/unifi-poller/poller" "github.com/unifi-poller/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"` + // 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"` - + 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. @@ -48,7 +48,7 @@ type Config struct { // 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 *time.Duration `json:"buffer_flush_interval" toml:"buffer_flush_interval" xml:"buffer_flush_interval,attr" yaml:"buffer_flush_interval"` + 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. @@ -58,10 +58,7 @@ type Config struct { // 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 *time.Duration `json:"write_timeout_uds" toml:"write_timeout_uds" xml:"write_timeout_uds,attr" yaml:"write_timeout_uds"` - // Telemetry is a set of metrics automatically injected by the client in the - // dogstatsd stream to be able to monitor the client itself. - Telemetry *bool `json:"telemetry" toml:"telemetry" xml:"telemetry,attr" yaml:"telemetry"` + WriteTimeoutUDS *cnfg.Duration `json:"write_timeout_uds" toml:"write_timeout_uds" xml:"write_timeout_uds,attr" yaml:"write_timeout_uds"` // ReceiveMode determins 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 @@ -86,10 +83,6 @@ type Config struct { 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"` - // [beta] Aggregation enables/disables client side aggregation - Aggregation *bool `json:"aggregation" toml:"aggregation" xml:"aggregation,attr" yaml:"aggregation"` - // TelemetryAddr specify a different endpoint for telemetry metrics. - TelemetryAddr *string `json:"telemetry_address" toml:"telemetry_address" xml:"telemetry_address,attr" yaml:"telemetry_address"` } // Datadog allows the data to be context aware with configuration @@ -101,22 +94,16 @@ type Datadog struct { // DatadogUnifi is returned by New() after you provide a Config. type DatadogUnifi struct { Collector poller.Collect - datadog statsd.ClientInterface + datadog statsd.ClientInterface LastCheck time.Time *Datadog } -type metric struct { - Name string - Tags map[string]string - Fields map[string]interface{} -} - -func init() { // nolint: gochecknoinits +func init() { // nolint: gochecknoinits u := &DatadogUnifi{Datadog: &Datadog{}, LastCheck: time.Now()} poller.NewOutput(&poller.Output{ - Name: "datadog", + Name: "datadog", Config: u.Datadog, Method: u.Run, }) @@ -131,17 +118,62 @@ func (u *DatadogUnifi) setConfigDefaults() { u.Interval = cnfg.Duration{Duration: u.Interval.Duration.Round(time.Second)} - u.options = make([]statsd.Option) + 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 { @@ -153,18 +185,18 @@ func (u *DatadogUnifi) Run(c poller.Collect) error { u.Collector = c u.setConfigDefaults() - u.datadog, err := statsd.New(u.Address, u.options...) + var err error + u.datadog, err = statsd.New(u.Address, u.options...) if err != nil { return err } u.PollController() - + return nil } - -// PollController runs fover, polling UniFi and pushing to Datadog +// 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) @@ -197,53 +229,41 @@ func (u *DatadogUnifi) PollController() { // 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, ch: make(chan *metric), Start: time.Now()} - + r := &Report{Metrics: m, Start: time.Now()} + // batch all the points. + u.loopPoints(r) + r.End = time.Now() + r.Elapsed = r.End.Sub(r.Start) return r, nil } - -// collect runs in a go routine and batches all the points. -func (u *InfluxUnifi) collect(r report, ch chan *metric) { - for m := range ch { - pt, err := influx.NewPoint(m.Table, m.Tags, m.Fields, r.metrics().TS) - if err == nil { - r.batch(m, pt) - } - - r.error(err) - r.done() - } -} - -// loopPoints kicks off 3 or 7 go routines to process metrics and send them -// to the collect routine through the metric channel. +// loopPoints collects all the data to immediately report to Datadog. func (u *DatadogUnifi) loopPoints(r report) { m := r.metrics() for _, s := range m.SitesDPI { - u.batchSiteDPI(r, s) + u.reportSiteDPI(r, s) } for _, s := range m.Sites { - u.batchSite(r, s) + u.reportSite(r, s) } appTotal := make(totalsDPImap) catTotal := make(totalsDPImap) for _, s := range m.ClientsDPI { - u.batchClientDPI(r, s, appTotal, catTotal) + u.reportClientDPI(r, s, appTotal, catTotal) } reportClientDPItotals(r, appTotal, catTotal) for _, s := range m.Clients { - u.batchClient(r, s) + u.reportClient(r, s) } for _, s := range m.IDSList { - u.batchIDS(r, s) + u.reportIDS(r, s) } u.loopDevicePoints(r) @@ -257,19 +277,19 @@ func (u *DatadogUnifi) loopDevicePoints(r report) { } for _, s := range m.UAPs { - u.batchUAP(r, s) + u.reportUAP(r, s) } for _, s := range m.USGs { - u.batchUSG(r, s) + u.reportUSG(r, s) } for _, s := range m.USWs { - u.batchUSW(r, s) + u.reportUSW(r, s) } for _, s := range m.UDMs { - u.batchUDM(r, s) + u.reportUDM(r, s) } } @@ -277,7 +297,6 @@ func (u *DatadogUnifi) loopDevicePoints(r report) { 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), diff --git a/integrations/datadogunifi/ids.go b/integrations/datadogunifi/ids.go new file mode 100644 index 00000000..2b368f79 --- /dev/null +++ b/integrations/datadogunifi/ids.go @@ -0,0 +1,31 @@ +package datadogunifi + +import ( + "github.com/unifi-poller/unifi" +) + +// reportIDS generates intrusion detection datapoints for Datadog. +// These points can be passed directly to datadog. +func (u *DatadogUnifi) reportIDS(r report, i *unifi.IDS) { + tags := []string{ + tag("site_name", i.SiteName), + tag("source", i.SourceName), + tag("in_iface", i.InIface), + tag("event_type", i.EventType), + tag("proto", i.Proto), + tag("app_proto", i.AppProto), + tag("usgip", i.USGIP), + tag("country_code", i.SourceIPGeo.CountryCode), + tag("country_name", i.SourceIPGeo.CountryName), + tag("city", i.SourceIPGeo.City), + tag("organization", i.SourceIPGeo.Organization), + tag("srcipASN", i.SrcIPASN), + tag("usgipASN", i.USGIPASN), + tag("alert_category", i.InnerAlertCategory), + tag("subsystem", i.Subsystem), + tag("catname", i.Catname), + } + + metricName := metricNamespace("intrusion_detect") + r.reportCount(metricName("count"), 1, tags) +} diff --git a/integrations/datadogunifi/points.go b/integrations/datadogunifi/points.go new file mode 100644 index 00000000..751c2114 --- /dev/null +++ b/integrations/datadogunifi/points.go @@ -0,0 +1,21 @@ +package datadogunifi + +import ( + "fmt" +) + +func tag(name string, value interface{}) string { + return fmt.Sprintf("%s:%v", name, value) +} + +func metricNamespace(namespace string) func(string) string { + return func(name string) string { + return fmt.Sprintf("%s.%s", namespace, name) + } +} + +func reportGaugeForMap(r report, metricName func(string) string, data map[string]float64, tags []string) { + for name, value := range data { + r.reportGauge(metricName(name), value, tags) + } +} diff --git a/integrations/datadogunifi/report.go b/integrations/datadogunifi/report.go new file mode 100644 index 00000000..5450606c --- /dev/null +++ b/integrations/datadogunifi/report.go @@ -0,0 +1,76 @@ +package datadogunifi + +import ( + "time" + + "github.com/DataDog/datadog-go/statsd" + "github.com/unifi-poller/poller" +) + +type Report struct { + Metrics *poller.Metrics + Errors []error + Total int + Fields int + Start time.Time + End time.Time + Elapsed time.Duration + + client statsd.ClientInterface +} + +type report interface { + error(err error) + metrics() *poller.Metrics + 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 + reportServiceCheck(name string, status statsd.ServiceCheckStatus, message string, tags []string) error +} + +func (r *Report) metrics() *poller.Metrics { + return r.Metrics +} + +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, message string, tags []string) error { + return r.client.Event(&statsd.Event{ + Title: title, + Text: message, + Timestamp: time.Now(), + Tags: tags, + }) +} + +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 index da3f1b80..a9ffe0a1 100644 --- a/integrations/datadogunifi/site.go +++ b/integrations/datadogunifi/site.go @@ -4,5 +4,77 @@ import ( "github.com/unifi-poller/unifi" ) -// batchSite generates Unifi Sites' datapoints for Datadog. -// These points can be passed directly to datadog +// 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..83403c12 --- /dev/null +++ b/integrations/datadogunifi/uap.go @@ -0,0 +1,199 @@ +package datadogunifi + +import ( + "github.com/unifi-poller/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) { + 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), + } + + metricName := metricNamespace("uap") + + u.reportUAPstats(s.Stat.Ap, r, metricName, tags) + u.reportSysStats(r, metricName, s.SysStats, s.SystemStats, 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) +} + +func (u *DatadogUnifi) reportUAPstats(ap *unifi.Ap, r report, metricName func(string) string, tags []string) { + if ap == nil { + return + } + + // Accumulative Statistics. + data := 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, + } + 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 + 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), + } + 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") + reportGaugeForMap(r, metricName, data, tags) + } +} + +func (u *DatadogUnifi) reportRadTable(r report, deviceName string, siteName string, source 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), + } + 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, + "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["extchannel"] = 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") + + reportGaugeForMap(r, metricName, data, tags) + } +} diff --git a/integrations/datadogunifi/udm.go b/integrations/datadogunifi/udm.go new file mode 100644 index 00000000..f544648b --- /dev/null +++ b/integrations/datadogunifi/udm.go @@ -0,0 +1,141 @@ +package datadogunifi + +import ( + "fmt" + + "github.com/unifi-poller/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{ + "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 name, value := range data { + r.reportGauge(metricName(name), value, tags) + } +} + +func (u *DatadogUnifi) reportUDMtemps(r report, metricName func(string) string, tags []string, temps []unifi.Temperature) { + for _, t := range temps { + name := fmt.Sprintf("temp_%s", t.Name) + r.reportGauge(metricName(name), t.Value, tags) + } +} + +// 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 + if !s.Adopted.Val || s.Locating.Val { + return + } + + metricName := metricNamespace("usg") + + 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) + + 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 = []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) + + 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. + + 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), + } + + metricName = metricNamespace("uap") + u.reportUAPstats(s.Stat.Ap, r, metricName, 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) +} diff --git a/integrations/datadogunifi/usg.go b/integrations/datadogunifi/usg.go new file mode 100644 index 00000000..613c4c6c --- /dev/null +++ b/integrations/datadogunifi/usg.go @@ -0,0 +1,145 @@ +package datadogunifi + +import ( + "github.com/unifi-poller/unifi" +) + +// reportUSG generates Unifi Gateway datapoints for Datadog. +// These points can be passed directly to datadog. +func (u *DatadogUnifi) reportUSG(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), + } + metricName := metricNamespace("usg") + u.reportSysStats(r, metricName, s.SysStats, s.SystemStats, tags) + u.reportUSGstats(r, metricName, tags, s.SpeedtestStatus, s.Stat.Gw, s.Uplink) + + 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) +} + +func (u *DatadogUnifi) reportUSGstats(r report, metricName func(string) string, tags []string, ss unifi.SpeedtestStatus, gw *unifi.Gw, ul unifi.Uplink) { + if gw == nil { + return + } + data := 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, + } + reportGaugeForMap(r, metricName, data, tags) +} + +func (u *DatadogUnifi) reportUSGwans(r report, deviceName string, siteName string, source 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 + } + + 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") + reportGaugeForMap(r, metricName, data, tags) + } +} + +func (u *DatadogUnifi) reportNetTable(r report, deviceName string, siteName string, source 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), + } + 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") + reportGaugeForMap(r, metricName, data, tags) + } +} diff --git a/integrations/datadogunifi/usw.go b/integrations/datadogunifi/usw.go new file mode 100644 index 00000000..1dd4ea4b --- /dev/null +++ b/integrations/datadogunifi/usw.go @@ -0,0 +1,119 @@ +package datadogunifi + +import ( + "fmt" + + "github.com/unifi-poller/unifi" +) + +// reportUSW generates Unifi Switch datapoints for Datadog. +// These points can be passed directly to datadog. +func (u *DatadogUnifi) reportUSW(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) + + 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) +} + +func (u *DatadogUnifi) reportUSWstat(r report, metricName func(string) string, tags []string, sw *unifi.Sw) { + if sw == nil { + return + } + + data := 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, + } + reportGaugeForMap(r, metricName, data, tags) +} + +func (u *DatadogUnifi) reportPortTable(r report, deviceName string, siteName string, source string, typeTag string, pt []unifi.Port) { + for _, p := range pt { + if !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), + } + data := map[string]float64{ + "dbytes_r": p.BytesR.Val, + "rx_broadcast": p.RxBroadcast.Val, + "rx_bytes": p.RxBytes.Val, + "rx_bytes-r": p.RxBytesR.Val, + "rx_dropped": p.RxDropped.Val, + "rx_errors": p.RxErrors.Val, + "rx_multicast": p.RxMulticast.Val, + "rx_packets": p.RxPackets.Val, + "speed": p.Speed.Val, + "stp_pathcost": p.StpPathcost.Val, + "tx_broadcast": p.TxBroadcast.Val, + "tx_bytes": p.TxBytes.Val, + "tx_bytes-r": p.TxBytesR.Val, + "tx_dropped": p.TxDropped.Val, + "tx_errors": p.TxErrors.Val, + "tx_multicast": p.TxMulticast.Val, + "tx_packets": p.TxPackets.Val, + } + + if p.PoeEnable.Val && p.PortPoe.Val { + data["poe_current"] = p.PoeCurrent.Val + data["poe_power"] = p.PoePower.Val + data["poe_voltage"] = p.PoeVoltage.Val + } + + metricName := metricNamespace("usw_ports") + reportGaugeForMap(r, metricName, data, tags) + } +}