mirror of
https://github.com/unpoller/unpoller.git
synced 2026-03-31 06:33:57 -04:00
Bumps github.com/unpoller/unifi/v5 to v5.23.0 which adds
GetTopology() fetching vertices (devices/clients) and edges
(wired/wireless connections) from /proxy/network/v2/api/site/{site}/topology.
Changes across the stack:
- poller.Metrics: add Topologies []any field + AppendMetrics support
- inputunifi: collect topology per-site (non-fatal on older controllers),
pass through augmentMetrics with site name override support
- promunifi: new topology.go with summary, connection-type, link-quality,
and band-distribution gauges
- influxunifi: new topology.go with topology_summary and topology_edge
measurements
- datadogunifi: new topology.go with equivalent Datadog gauges
- otelunifi: new topology.go with OpenTelemetry gauge observations
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
go.mod
2
go.mod
@@ -12,7 +12,7 @@ require (
|
|||||||
github.com/prometheus/common v0.67.5
|
github.com/prometheus/common v0.67.5
|
||||||
github.com/spf13/pflag v1.0.10
|
github.com/spf13/pflag v1.0.10
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/unpoller/unifi/v5 v5.22.0
|
github.com/unpoller/unifi/v5 v5.23.0
|
||||||
go.opentelemetry.io/otel v1.42.0
|
go.opentelemetry.io/otel v1.42.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -89,6 +89,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/unpoller/unifi/v5 v5.22.0 h1:ftLZcdXCtSfmd1a9nytajVCPuUoDxB1JyOPqoxPt8cI=
|
github.com/unpoller/unifi/v5 v5.22.0 h1:ftLZcdXCtSfmd1a9nytajVCPuUoDxB1JyOPqoxPt8cI=
|
||||||
github.com/unpoller/unifi/v5 v5.22.0/go.mod h1:0R6t/SKaS8eoOrTkSYwzVb292KG5eQfbKEuevuES0So=
|
github.com/unpoller/unifi/v5 v5.22.0/go.mod h1:0R6t/SKaS8eoOrTkSYwzVb292KG5eQfbKEuevuES0So=
|
||||||
|
github.com/unpoller/unifi/v5 v5.23.0 h1:aJ7qM/UNtNNa9+iCfd6Quom8F7riFPQOe5g9rMsX8os=
|
||||||
|
github.com/unpoller/unifi/v5 v5.23.0/go.mod h1:0R6t/SKaS8eoOrTkSYwzVb292KG5eQfbKEuevuES0So=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
|||||||
@@ -325,6 +325,10 @@ func (u *DatadogUnifi) loopPoints(r report) {
|
|||||||
for _, p := range m.FirewallPolicies {
|
for _, p := range m.FirewallPolicies {
|
||||||
u.switchExport(r, p)
|
u.switchExport(r, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, t := range m.Topologies {
|
||||||
|
u.switchExport(r, t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
|
func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||||
@@ -367,6 +371,8 @@ func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
|
|||||||
u.batchWAN(r, v)
|
u.batchWAN(r, v)
|
||||||
case *unifi.FirewallPolicy:
|
case *unifi.FirewallPolicy:
|
||||||
u.batchFirewallPolicy(r, v)
|
u.batchFirewallPolicy(r, v)
|
||||||
|
case *unifi.Topology:
|
||||||
|
u.batchTopology(r, v)
|
||||||
default:
|
default:
|
||||||
if u.Collector != nil && u.Collector.Poller().LogUnknownTypes {
|
if u.Collector != nil && u.Collector.Poller().LogUnknownTypes {
|
||||||
u.LogDebugf("unknown export type: %T", v)
|
u.LogDebugf("unknown export type: %T", v)
|
||||||
|
|||||||
101
pkg/datadogunifi/topology.go
Normal file
101
pkg/datadogunifi/topology.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package datadogunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// batchTopology generates topology datapoints for Datadog.
|
||||||
|
func (u *DatadogUnifi) batchTopology(r report, t *unifi.Topology) {
|
||||||
|
if t == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metricName := metricNamespace("topology")
|
||||||
|
|
||||||
|
siteTags := []string{
|
||||||
|
tag("site_name", t.SiteName),
|
||||||
|
tag("source", t.SourceName),
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
devices int
|
||||||
|
clients int
|
||||||
|
wired int
|
||||||
|
wireless int
|
||||||
|
fullDuplex int
|
||||||
|
)
|
||||||
|
|
||||||
|
unknownSwitch := 0.0
|
||||||
|
if t.HasUnknownSwitch {
|
||||||
|
unknownSwitch = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range t.Vertices {
|
||||||
|
switch t.Vertices[i].Type {
|
||||||
|
case "DEVICE":
|
||||||
|
devices++
|
||||||
|
case "CLIENT":
|
||||||
|
clients++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bandCounts := make(map[string]int)
|
||||||
|
|
||||||
|
for i := range t.Edges {
|
||||||
|
e := &t.Edges[i]
|
||||||
|
|
||||||
|
edgeTags := []string{
|
||||||
|
tag("uplink_mac", e.UplinkMac),
|
||||||
|
tag("downlink_mac", e.DownlinkMac),
|
||||||
|
tag("link_type", e.Type),
|
||||||
|
tag("site_name", t.SiteName),
|
||||||
|
tag("source", t.SourceName),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Type {
|
||||||
|
case "WIRED":
|
||||||
|
wired++
|
||||||
|
|
||||||
|
if e.Duplex == "FULL_DUPLEX" {
|
||||||
|
fullDuplex++
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.reportGauge(metricName("link_rate_mbps"), e.RateMbps.Val, edgeTags)
|
||||||
|
|
||||||
|
case "WIRELESS":
|
||||||
|
wireless++
|
||||||
|
|
||||||
|
if e.RadioBand != "" {
|
||||||
|
bandCounts[e.RadioBand]++
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.ExperienceScore.Val > 0 {
|
||||||
|
_ = r.reportGauge(metricName("link_experience_score"), e.ExperienceScore.Val, edgeTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := map[string]float64{
|
||||||
|
"vertices_total": float64(len(t.Vertices)),
|
||||||
|
"edges_total": float64(len(t.Edges)),
|
||||||
|
"devices_total": float64(devices),
|
||||||
|
"clients_total": float64(clients),
|
||||||
|
"connections_wired": float64(wired),
|
||||||
|
"connections_wireless": float64(wireless),
|
||||||
|
"wired_full_duplex": float64(fullDuplex),
|
||||||
|
"has_unknown_switch": unknownSwitch,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, value := range summary {
|
||||||
|
_ = r.reportGauge(metricName(name), value, siteTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
for band, count := range bandCounts {
|
||||||
|
bandTags := []string{
|
||||||
|
tag("band", band),
|
||||||
|
tag("site_name", t.SiteName),
|
||||||
|
tag("source", t.SourceName),
|
||||||
|
}
|
||||||
|
_ = r.reportGauge(metricName("connections_by_band"), float64(count), bandTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -438,6 +438,10 @@ func (u *InfluxUnifi) loopPoints(r report) {
|
|||||||
for _, p := range m.FirewallPolicies {
|
for _, p := range m.FirewallPolicies {
|
||||||
u.switchExport(r, p)
|
u.switchExport(r, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, t := range m.Topologies {
|
||||||
|
u.switchExport(r, t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
|
func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||||
@@ -480,6 +484,8 @@ func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
|
|||||||
u.batchWAN(r, v)
|
u.batchWAN(r, v)
|
||||||
case *unifi.FirewallPolicy:
|
case *unifi.FirewallPolicy:
|
||||||
u.batchFirewallPolicy(r, v)
|
u.batchFirewallPolicy(r, v)
|
||||||
|
case *unifi.Topology:
|
||||||
|
u.batchTopology(r, v)
|
||||||
default:
|
default:
|
||||||
if u.Collector.Poller().LogUnknownTypes {
|
if u.Collector.Poller().LogUnknownTypes {
|
||||||
u.LogDebugf("unknown export type: %T", v)
|
u.LogDebugf("unknown export type: %T", v)
|
||||||
|
|||||||
93
pkg/influxunifi/topology.go
Normal file
93
pkg/influxunifi/topology.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package influxunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// batchTopology generates topology datapoints for InfluxDB.
|
||||||
|
func (u *InfluxUnifi) batchTopology(r report, t *unifi.Topology) {
|
||||||
|
if t == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
devices int
|
||||||
|
clients int
|
||||||
|
wired int
|
||||||
|
wireless int
|
||||||
|
fullDuplex int
|
||||||
|
)
|
||||||
|
|
||||||
|
unknownSwitch := 0
|
||||||
|
if t.HasUnknownSwitch {
|
||||||
|
unknownSwitch = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range t.Vertices {
|
||||||
|
switch t.Vertices[i].Type {
|
||||||
|
case "DEVICE":
|
||||||
|
devices++
|
||||||
|
case "CLIENT":
|
||||||
|
clients++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range t.Edges {
|
||||||
|
e := &t.Edges[i]
|
||||||
|
|
||||||
|
edgeTags := map[string]string{
|
||||||
|
"uplink_mac": e.UplinkMac,
|
||||||
|
"downlink_mac": e.DownlinkMac,
|
||||||
|
"link_type": e.Type,
|
||||||
|
"site_name": t.SiteName,
|
||||||
|
"source": t.SourceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Type {
|
||||||
|
case "WIRED":
|
||||||
|
wired++
|
||||||
|
|
||||||
|
if e.Duplex == "FULL_DUPLEX" {
|
||||||
|
fullDuplex++
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeFields := map[string]any{
|
||||||
|
"rate_mbps": e.RateMbps.Val,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.send(&metric{Table: "topology_edge", Tags: edgeTags, Fields: edgeFields})
|
||||||
|
|
||||||
|
case "WIRELESS":
|
||||||
|
wireless++
|
||||||
|
|
||||||
|
edgeTags["essid"] = e.Essid
|
||||||
|
edgeTags["radio_band"] = e.RadioBand
|
||||||
|
edgeTags["protocol"] = e.Protocol
|
||||||
|
|
||||||
|
edgeFields := map[string]any{
|
||||||
|
"experience_score": e.ExperienceScore.Val,
|
||||||
|
"channel": e.Channel.Val,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.send(&metric{Table: "topology_edge", Tags: edgeTags, Fields: edgeFields})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryTags := map[string]string{
|
||||||
|
"site_name": t.SiteName,
|
||||||
|
"source": t.SourceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryFields := map[string]any{
|
||||||
|
"vertices_total": len(t.Vertices),
|
||||||
|
"edges_total": len(t.Edges),
|
||||||
|
"devices_total": devices,
|
||||||
|
"clients_total": clients,
|
||||||
|
"connections_wired": wired,
|
||||||
|
"connections_wireless": wireless,
|
||||||
|
"wired_full_duplex": fullDuplex,
|
||||||
|
"has_unknown_switch": unknownSwitch,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.send(&metric{Table: "topology_summary", Tags: summaryTags, Fields: summaryFields})
|
||||||
|
}
|
||||||
@@ -230,6 +230,14 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) {
|
|||||||
u.LogDebugf("Found %d Sysinfo entries", len(m.Sysinfos))
|
u.LogDebugf("Found %d Sysinfo entries", len(m.Sysinfos))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get network topology
|
||||||
|
if m.Topologies, err = c.Unifi.GetTopology(sites); err != nil {
|
||||||
|
// Don't fail collection if topology fails - older controllers may not have this endpoint
|
||||||
|
u.LogDebugf("unifi.GetTopology(%s): %v (continuing)", c.URL, err)
|
||||||
|
} else {
|
||||||
|
u.LogDebugf("Found %d Topology entries", len(m.Topologies))
|
||||||
|
}
|
||||||
|
|
||||||
// Update web UI only on success; call explicitly so we never run with nil c/c.Unifi (no defer).
|
// Update web UI only on success; call explicitly so we never run with nil c/c.Unifi (no defer).
|
||||||
// Recover so a panic in updateWeb (e.g. old image, race) never kills the poller.
|
// Recover so a panic in updateWeb (e.g. old image, race) never kills the poller.
|
||||||
if c != nil && c.Unifi != nil {
|
if c != nil && c.Unifi != nil {
|
||||||
@@ -454,6 +462,15 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
|||||||
m.FirewallPolicies = append(m.FirewallPolicies, policy)
|
m.FirewallPolicies = append(m.FirewallPolicies, policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, topo := range metrics.Topologies {
|
||||||
|
// Apply site name override for topology if configured
|
||||||
|
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(topo.SiteName) {
|
||||||
|
topo.SiteName = c.DefaultSiteNameOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Topologies = append(m.Topologies, topo)
|
||||||
|
}
|
||||||
|
|
||||||
// Apply default_site_name_override to all metrics if configured.
|
// Apply default_site_name_override to all metrics if configured.
|
||||||
// This must be done AFTER all metrics are added to m, so everything is included.
|
// This must be done AFTER all metrics are added to m, so everything is included.
|
||||||
// This allows us to use the console name for Cloud Gateways while keeping
|
// This allows us to use the console name for Cloud Gateways while keeping
|
||||||
@@ -588,6 +605,14 @@ func applySiteNameOverride(m *poller.Metrics, overrideName string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := range m.Topologies {
|
||||||
|
if topo, ok := m.Topologies[i].(*unifi.Topology); ok {
|
||||||
|
if isDefaultSiteName(topo.SiteName) {
|
||||||
|
topo.SiteName = overrideName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is a helper function for augmentMetrics.
|
// this is a helper function for augmentMetrics.
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ type Metrics struct {
|
|||||||
WANConfigs []*unifi.WANEnrichedConfiguration
|
WANConfigs []*unifi.WANEnrichedConfiguration
|
||||||
Sysinfos []*unifi.Sysinfo
|
Sysinfos []*unifi.Sysinfo
|
||||||
FirewallPolicies []*unifi.FirewallPolicy
|
FirewallPolicies []*unifi.FirewallPolicy
|
||||||
|
Topologies []*unifi.Topology
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { // nolint: gochecknoinits
|
func init() { // nolint: gochecknoinits
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func (u *OtelOutput) reportMetrics(m *poller.Metrics, _ *poller.Events) (*Report
|
|||||||
u.exportClients(ctx, meter, m, r)
|
u.exportClients(ctx, meter, m, r)
|
||||||
u.exportDevices(ctx, meter, m, r)
|
u.exportDevices(ctx, meter, m, r)
|
||||||
u.exportFirewallPolicies(ctx, meter, m, r)
|
u.exportFirewallPolicies(ctx, meter, m, r)
|
||||||
|
u.exportTopology(ctx, meter, m, r)
|
||||||
|
|
||||||
r.Elapsed = time.Since(start)
|
r.Elapsed = time.Since(start)
|
||||||
|
|
||||||
|
|||||||
114
pkg/otelunifi/topology.go
Normal file
114
pkg/otelunifi/topology.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package otelunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
"github.com/unpoller/unpoller/pkg/poller"
|
||||||
|
)
|
||||||
|
|
||||||
|
// exportTopology emits network topology metrics.
|
||||||
|
func (u *OtelOutput) exportTopology(ctx context.Context, meter metric.Meter, m *poller.Metrics, r *Report) {
|
||||||
|
for _, item := range m.Topologies {
|
||||||
|
t, ok := item.(*unifi.Topology)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
siteAttrs := attribute.NewSet(
|
||||||
|
attribute.String("site_name", t.SiteName),
|
||||||
|
attribute.String("source", t.SourceName),
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
devices int
|
||||||
|
clients int
|
||||||
|
wired int
|
||||||
|
wireless int
|
||||||
|
fullDuplex int
|
||||||
|
)
|
||||||
|
|
||||||
|
unknownSwitch := 0.0
|
||||||
|
if t.HasUnknownSwitch {
|
||||||
|
unknownSwitch = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range t.Vertices {
|
||||||
|
switch t.Vertices[i].Type {
|
||||||
|
case "DEVICE":
|
||||||
|
devices++
|
||||||
|
case "CLIENT":
|
||||||
|
clients++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bandCounts := make(map[string]int)
|
||||||
|
|
||||||
|
for i := range t.Edges {
|
||||||
|
e := &t.Edges[i]
|
||||||
|
|
||||||
|
edgeAttrs := attribute.NewSet(
|
||||||
|
attribute.String("uplink_mac", e.UplinkMac),
|
||||||
|
attribute.String("downlink_mac", e.DownlinkMac),
|
||||||
|
attribute.String("link_type", e.Type),
|
||||||
|
attribute.String("site_name", t.SiteName),
|
||||||
|
attribute.String("source", t.SourceName),
|
||||||
|
)
|
||||||
|
|
||||||
|
switch e.Type {
|
||||||
|
case "WIRED":
|
||||||
|
wired++
|
||||||
|
|
||||||
|
if e.Duplex == "FULL_DUPLEX" {
|
||||||
|
fullDuplex++
|
||||||
|
}
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_link_rate_mbps",
|
||||||
|
"Wired link rate in Mbps", e.RateMbps.Val, edgeAttrs)
|
||||||
|
|
||||||
|
case "WIRELESS":
|
||||||
|
wireless++
|
||||||
|
|
||||||
|
if e.RadioBand != "" {
|
||||||
|
bandCounts[e.RadioBand]++
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.ExperienceScore.Val > 0 {
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_link_experience_score",
|
||||||
|
"Wireless link experience score (0-100)", e.ExperienceScore.Val, edgeAttrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_vertices_total",
|
||||||
|
"Total vertices in topology", float64(len(t.Vertices)), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_edges_total",
|
||||||
|
"Total edges/connections in topology", float64(len(t.Edges)), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_devices_total",
|
||||||
|
"UniFi devices in topology", float64(devices), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_clients_total",
|
||||||
|
"Clients in topology", float64(clients), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_has_unknown_switch",
|
||||||
|
"Unknown switch detected in topology (1/0)", unknownSwitch, siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_connections_wired",
|
||||||
|
"Number of wired connections", float64(wired), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_connections_wireless",
|
||||||
|
"Number of wireless connections", float64(wireless), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_wired_full_duplex",
|
||||||
|
"Number of full-duplex wired links", float64(fullDuplex), siteAttrs)
|
||||||
|
|
||||||
|
for band, count := range bandCounts {
|
||||||
|
bandAttrs := attribute.NewSet(
|
||||||
|
attribute.String("band", band),
|
||||||
|
attribute.String("site_name", t.SiteName),
|
||||||
|
attribute.String("source", t.SourceName),
|
||||||
|
)
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_topology_connections_by_band",
|
||||||
|
"Number of wireless connections by radio band", float64(count), bandAttrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,7 @@ type Metrics struct {
|
|||||||
WANConfigs []any
|
WANConfigs []any
|
||||||
Sysinfos []any
|
Sysinfos []any
|
||||||
FirewallPolicies []any
|
FirewallPolicies []any
|
||||||
|
Topologies []any
|
||||||
ControllerStatuses []ControllerStatus
|
ControllerStatuses []ControllerStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ func AppendMetrics(existing *Metrics, m *Metrics) *Metrics {
|
|||||||
existing.WANConfigs = append(existing.WANConfigs, m.WANConfigs...)
|
existing.WANConfigs = append(existing.WANConfigs, m.WANConfigs...)
|
||||||
existing.Sysinfos = append(existing.Sysinfos, m.Sysinfos...)
|
existing.Sysinfos = append(existing.Sysinfos, m.Sysinfos...)
|
||||||
existing.FirewallPolicies = append(existing.FirewallPolicies, m.FirewallPolicies...)
|
existing.FirewallPolicies = append(existing.FirewallPolicies, m.FirewallPolicies...)
|
||||||
|
existing.Topologies = append(existing.Topologies, m.Topologies...)
|
||||||
existing.ControllerStatuses = append(existing.ControllerStatuses, m.ControllerStatuses...)
|
existing.ControllerStatuses = append(existing.ControllerStatuses, m.ControllerStatuses...)
|
||||||
|
|
||||||
return existing
|
return existing
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ type promUnifi struct {
|
|||||||
WAN *wan
|
WAN *wan
|
||||||
Controller *controller
|
Controller *controller
|
||||||
FirewallPolicy *firewallpolicy
|
FirewallPolicy *firewallpolicy
|
||||||
|
Topology *topology
|
||||||
// controllerUp tracks per-controller poll success (1) or failure (0).
|
// controllerUp tracks per-controller poll success (1) or failure (0).
|
||||||
controllerUp *prometheus.GaugeVec
|
controllerUp *prometheus.GaugeVec
|
||||||
// This interface is passed to the Collect() method. The Collect method uses
|
// This interface is passed to the Collect() method. The Collect method uses
|
||||||
@@ -217,6 +218,7 @@ func (u *promUnifi) Run(c poller.Collect) error {
|
|||||||
u.WAN = descWAN(u.Namespace + "_")
|
u.WAN = descWAN(u.Namespace + "_")
|
||||||
u.Controller = descController(u.Namespace + "_")
|
u.Controller = descController(u.Namespace + "_")
|
||||||
u.FirewallPolicy = descFirewallPolicy(u.Namespace + "_")
|
u.FirewallPolicy = descFirewallPolicy(u.Namespace + "_")
|
||||||
|
u.Topology = descTopology(u.Namespace + "_")
|
||||||
u.controllerUp = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
u.controllerUp = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
Name: u.Namespace + "_controller_up",
|
Name: u.Namespace + "_controller_up",
|
||||||
Help: "Whether the last poll of the UniFi controller succeeded (1) or failed (0).",
|
Help: "Whether the last poll of the UniFi controller succeeded (1) or failed (0).",
|
||||||
@@ -305,7 +307,7 @@ func (t *target) Describe(ch chan<- *prometheus.Desc) {
|
|||||||
// Describe satisfies the prometheus Collector. This returns all of the
|
// Describe satisfies the prometheus Collector. This returns all of the
|
||||||
// metric descriptions that this packages produces.
|
// metric descriptions that this packages produces.
|
||||||
func (u *promUnifi) Describe(ch chan<- *prometheus.Desc) {
|
func (u *promUnifi) Describe(ch chan<- *prometheus.Desc) {
|
||||||
for _, f := range []any{u.Client, u.Device, u.UAP, u.USG, u.USW, u.PDU, u.Site, u.SpeedTest, u.DHCPLease, u.WAN, u.FirewallPolicy} {
|
for _, f := range []any{u.Client, u.Device, u.UAP, u.USG, u.USW, u.PDU, u.Site, u.SpeedTest, u.DHCPLease, u.WAN, u.FirewallPolicy, u.Topology} {
|
||||||
v := reflect.Indirect(reflect.ValueOf(f))
|
v := reflect.Indirect(reflect.ValueOf(f))
|
||||||
|
|
||||||
// Loop each struct member and send it to the provided channel.
|
// Loop each struct member and send it to the provided channel.
|
||||||
@@ -482,6 +484,12 @@ func (u *promUnifi) loopExports(r report) {
|
|||||||
|
|
||||||
u.exportFirewallPolicies(r, firewallPolicies)
|
u.exportFirewallPolicies(r, firewallPolicies)
|
||||||
|
|
||||||
|
for _, t := range m.Topologies {
|
||||||
|
if topo, ok := t.(*unifi.Topology); ok {
|
||||||
|
u.exportTopology(r, topo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
u.exportClientDPItotals(r, appTotal, catTotal)
|
u.exportClientDPItotals(r, appTotal, catTotal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
121
pkg/promunifi/topology.go
Normal file
121
pkg/promunifi/topology.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package promunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type topology struct {
|
||||||
|
// Summary metrics
|
||||||
|
VerticesTotal *prometheus.Desc
|
||||||
|
EdgesTotal *prometheus.Desc
|
||||||
|
DevicesTotal *prometheus.Desc
|
||||||
|
ClientsTotal *prometheus.Desc
|
||||||
|
HasUnknownSwitch *prometheus.Desc
|
||||||
|
|
||||||
|
// Connection type metrics
|
||||||
|
ConnectionsWired *prometheus.Desc
|
||||||
|
ConnectionsWireless *prometheus.Desc
|
||||||
|
ConnectionsByBand *prometheus.Desc
|
||||||
|
|
||||||
|
// Link quality metrics
|
||||||
|
LinkExperienceScore *prometheus.Desc
|
||||||
|
LinkRateMbps *prometheus.Desc
|
||||||
|
WiredFullDuplex *prometheus.Desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func descTopology(ns string) *topology {
|
||||||
|
siteLabels := []string{"site_name", "source"}
|
||||||
|
linkLabels := []string{"uplink_mac", "downlink_mac", "link_type", "site_name", "source"}
|
||||||
|
bandLabels := []string{"band", "site_name", "source"}
|
||||||
|
|
||||||
|
nd := prometheus.NewDesc
|
||||||
|
|
||||||
|
return &topology{
|
||||||
|
VerticesTotal: nd(ns+"topology_vertices_total", "Total vertices in topology", siteLabels, nil),
|
||||||
|
EdgesTotal: nd(ns+"topology_edges_total", "Total edges/connections in topology", siteLabels, nil),
|
||||||
|
DevicesTotal: nd(ns+"topology_devices_total", "UniFi devices in topology", siteLabels, nil),
|
||||||
|
ClientsTotal: nd(ns+"topology_clients_total", "Clients in topology", siteLabels, nil),
|
||||||
|
HasUnknownSwitch: nd(ns+"topology_has_unknown_switch", "Unknown switch detected in topology (1/0)", siteLabels, nil),
|
||||||
|
ConnectionsWired: nd(ns+"topology_connections_wired", "Number of wired connections", siteLabels, nil),
|
||||||
|
ConnectionsWireless: nd(ns+"topology_connections_wireless", "Number of wireless connections", siteLabels, nil),
|
||||||
|
ConnectionsByBand: nd(ns+"topology_connections_by_band", "Number of wireless connections by radio band", bandLabels, nil),
|
||||||
|
LinkExperienceScore: nd(ns+"topology_link_experience_score", "Link experience score (0-100)", linkLabels, nil),
|
||||||
|
LinkRateMbps: nd(ns+"topology_link_rate_mbps", "Link rate in Mbps", linkLabels, nil),
|
||||||
|
WiredFullDuplex: nd(ns+"topology_wired_full_duplex", "Number of full-duplex wired links", siteLabels, nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *promUnifi) exportTopology(r report, t *unifi.Topology) {
|
||||||
|
if t == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
siteLabels := []string{t.SiteName, t.SourceName}
|
||||||
|
|
||||||
|
var (
|
||||||
|
devices int
|
||||||
|
clients int
|
||||||
|
wired int
|
||||||
|
wireless int
|
||||||
|
fullDuplex int
|
||||||
|
bandCounts = make(map[string]int)
|
||||||
|
unknownSwitch float64
|
||||||
|
)
|
||||||
|
|
||||||
|
if t.HasUnknownSwitch {
|
||||||
|
unknownSwitch = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range t.Vertices {
|
||||||
|
switch t.Vertices[i].Type {
|
||||||
|
case "DEVICE":
|
||||||
|
devices++
|
||||||
|
case "CLIENT":
|
||||||
|
clients++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range t.Edges {
|
||||||
|
e := &t.Edges[i]
|
||||||
|
linkLabels := []string{e.UplinkMac, e.DownlinkMac, e.Type, t.SiteName, t.SourceName}
|
||||||
|
|
||||||
|
switch e.Type {
|
||||||
|
case "WIRED":
|
||||||
|
wired++
|
||||||
|
|
||||||
|
if e.Duplex == "FULL_DUPLEX" {
|
||||||
|
fullDuplex++
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.RateMbps.Val > 0 {
|
||||||
|
r.send([]*metric{{u.Topology.LinkRateMbps, gauge, e.RateMbps.Val, linkLabels}})
|
||||||
|
}
|
||||||
|
case "WIRELESS":
|
||||||
|
wireless++
|
||||||
|
|
||||||
|
if e.RadioBand != "" {
|
||||||
|
bandCounts[e.RadioBand]++
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.ExperienceScore.Val > 0 {
|
||||||
|
r.send([]*metric{{u.Topology.LinkExperienceScore, gauge, e.ExperienceScore.Val, linkLabels}})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.send([]*metric{
|
||||||
|
{u.Topology.VerticesTotal, gauge, float64(len(t.Vertices)), siteLabels},
|
||||||
|
{u.Topology.EdgesTotal, gauge, float64(len(t.Edges)), siteLabels},
|
||||||
|
{u.Topology.DevicesTotal, gauge, float64(devices), siteLabels},
|
||||||
|
{u.Topology.ClientsTotal, gauge, float64(clients), siteLabels},
|
||||||
|
{u.Topology.HasUnknownSwitch, gauge, unknownSwitch, siteLabels},
|
||||||
|
{u.Topology.ConnectionsWired, gauge, float64(wired), siteLabels},
|
||||||
|
{u.Topology.ConnectionsWireless, gauge, float64(wireless), siteLabels},
|
||||||
|
{u.Topology.WiredFullDuplex, gauge, float64(fullDuplex), siteLabels},
|
||||||
|
})
|
||||||
|
|
||||||
|
for band, count := range bandCounts {
|
||||||
|
r.send([]*metric{{u.Topology.ConnectionsByBand, gauge, float64(count), []string{band, t.SiteName, t.SourceName}}})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user