mirror of
https://github.com/unpoller/unpoller.git
synced 2026-03-31 06:24:21 -04:00
* feat: add Site Magic site-to-site VPN metrics (closes #926) Bump github.com/unpoller/unifi/v5 to v5.25.0 which adds: - GetMagicSiteToSiteVPN / GetMagicSiteToSiteVPNSite API methods - MagicSiteToSiteVPN types with mesh, connection, device, and status structs - Missing VPN health fields on Site.Health (SiteToSiteNumActive/Inactive, SiteToSiteRxBytes/TxBytes/RxPackets/TxPackets) Implement VPN metrics collection across all output plugins: - Collect Site Magic VPN mesh data per-site in inputunifi pollController - Propagate VPNMeshes through poller.Metrics / AppendMetrics - Apply DefaultSiteNameOverride for VPN meshes in augmentMetrics / applySiteNameOverride - influxunifi: vpn_mesh, vpn_mesh_connection, vpn_mesh_status tables - promunifi: vpn_mesh_*, vpn_tunnel_*, vpn_mesh_status_* gauges - datadogunifi: unifi.vpn_mesh.*, unifi.vpn_tunnel.*, unifi.vpn_mesh_status.* Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * feat(otelunifi): add Site Magic VPN metrics to OpenTelemetry output Adds exportVPNMeshes to the otel output plugin, emitting the same unifi_vpn_mesh_*, unifi_vpn_tunnel_*, and unifi_vpn_mesh_status_* gauges as the other output plugins. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -333,6 +333,10 @@ func (u *DatadogUnifi) loopPoints(r report) {
|
||||
for _, a := range m.PortAnomalies {
|
||||
u.switchExport(r, a)
|
||||
}
|
||||
|
||||
for _, v := range m.VPNMeshes {
|
||||
u.switchExport(r, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||
@@ -379,6 +383,8 @@ func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||
u.batchTopology(r, v)
|
||||
case *unifi.PortAnomaly:
|
||||
u.batchPortAnomaly(r, v)
|
||||
case *unifi.MagicSiteToSiteVPN:
|
||||
u.batchMagicSiteToSiteVPN(r, v)
|
||||
default:
|
||||
if u.Collector != nil && u.Collector.Poller().LogUnknownTypes {
|
||||
u.LogDebugf("unknown export type: %T", v)
|
||||
|
||||
67
pkg/datadogunifi/vpn.go
Normal file
67
pkg/datadogunifi/vpn.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package datadogunifi
|
||||
|
||||
import (
|
||||
"github.com/unpoller/unifi/v5"
|
||||
)
|
||||
|
||||
// batchMagicSiteToSiteVPN generates Site Magic VPN datapoints for Datadog.
|
||||
func (u *DatadogUnifi) batchMagicSiteToSiteVPN(r report, m *unifi.MagicSiteToSiteVPN) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
meshMetric := metricNamespace("vpn_mesh")
|
||||
|
||||
meshTags := []string{
|
||||
tag("site_name", m.SiteName),
|
||||
tag("source", m.SourceName),
|
||||
tag("mesh_name", m.Name),
|
||||
}
|
||||
|
||||
paused := 0.0
|
||||
if m.Pause.Val {
|
||||
paused = 1.0
|
||||
}
|
||||
|
||||
_ = r.reportGauge(meshMetric("paused"), paused, meshTags)
|
||||
_ = r.reportGauge(meshMetric("connections_total"), float64(len(m.Connections)), meshTags)
|
||||
_ = r.reportGauge(meshMetric("devices_total"), float64(len(m.Devices)), meshTags)
|
||||
|
||||
tunnelMetric := metricNamespace("vpn_tunnel")
|
||||
statusMetric := metricNamespace("vpn_mesh_status")
|
||||
|
||||
for i := range m.Status {
|
||||
s := &m.Status[i]
|
||||
|
||||
statusTags := []string{
|
||||
tag("site_name", m.SiteName),
|
||||
tag("source", m.SourceName),
|
||||
tag("mesh_name", m.Name),
|
||||
tag("status_site", s.SiteID),
|
||||
}
|
||||
|
||||
_ = r.reportGauge(statusMetric("errors"), float64(len(s.Errors)), statusTags)
|
||||
_ = r.reportGauge(statusMetric("warnings"), float64(len(s.Warnings)), statusTags)
|
||||
|
||||
for j := range s.Connections {
|
||||
conn := &s.Connections[j]
|
||||
|
||||
connected := 0.0
|
||||
if conn.Connected.Val {
|
||||
connected = 1.0
|
||||
}
|
||||
|
||||
connTags := []string{
|
||||
tag("site_name", m.SiteName),
|
||||
tag("source", m.SourceName),
|
||||
tag("mesh_name", m.Name),
|
||||
tag("connection_id", conn.ConnectionID),
|
||||
tag("status_site", s.SiteID),
|
||||
}
|
||||
|
||||
_ = r.reportGauge(tunnelMetric("connected"), connected, connTags)
|
||||
_ = r.reportGauge(tunnelMetric("association_time"), conn.AssociationTime.Val, connTags)
|
||||
_ = r.reportGauge(tunnelMetric("errors"), float64(len(conn.Errors)), connTags)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -446,6 +446,10 @@ func (u *InfluxUnifi) loopPoints(r report) {
|
||||
for _, a := range m.PortAnomalies {
|
||||
u.switchExport(r, a)
|
||||
}
|
||||
|
||||
for _, v := range m.VPNMeshes {
|
||||
u.switchExport(r, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||
@@ -492,6 +496,8 @@ func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||
u.batchTopology(r, v)
|
||||
case *unifi.PortAnomaly:
|
||||
u.batchPortAnomaly(r, v)
|
||||
case *unifi.MagicSiteToSiteVPN:
|
||||
u.batchMagicSiteToSiteVPN(r, v)
|
||||
default:
|
||||
if u.Collector.Poller().LogUnknownTypes {
|
||||
u.LogDebugf("unknown export type: %T", v)
|
||||
|
||||
75
pkg/influxunifi/vpn.go
Normal file
75
pkg/influxunifi/vpn.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package influxunifi
|
||||
|
||||
import (
|
||||
"github.com/unpoller/unifi/v5"
|
||||
)
|
||||
|
||||
// batchMagicSiteToSiteVPN generates Site Magic VPN datapoints for InfluxDB.
|
||||
func (u *InfluxUnifi) batchMagicSiteToSiteVPN(r report, m *unifi.MagicSiteToSiteVPN) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
meshTags := map[string]string{
|
||||
"site_name": m.SiteName,
|
||||
"source": m.SourceName,
|
||||
"mesh_id": m.ID,
|
||||
"mesh_name": m.Name,
|
||||
}
|
||||
|
||||
paused := 0.0
|
||||
if m.Pause.Val {
|
||||
paused = 1.0
|
||||
}
|
||||
|
||||
meshFields := map[string]any{
|
||||
"paused": paused,
|
||||
"connections_total": len(m.Connections),
|
||||
"devices_total": len(m.Devices),
|
||||
}
|
||||
|
||||
r.send(&metric{Table: "vpn_mesh", Tags: meshTags, Fields: meshFields})
|
||||
|
||||
for i := range m.Status {
|
||||
s := &m.Status[i]
|
||||
|
||||
for j := range s.Connections {
|
||||
conn := &s.Connections[j]
|
||||
|
||||
connected := 0.0
|
||||
if conn.Connected.Val {
|
||||
connected = 1.0
|
||||
}
|
||||
|
||||
connTags := map[string]string{
|
||||
"site_name": m.SiteName,
|
||||
"source": m.SourceName,
|
||||
"mesh_name": m.Name,
|
||||
"connection_id": conn.ConnectionID,
|
||||
"status_site": s.SiteID,
|
||||
}
|
||||
|
||||
connFields := map[string]any{
|
||||
"connected": connected,
|
||||
"association_time": conn.AssociationTime.Val,
|
||||
"errors": len(conn.Errors),
|
||||
}
|
||||
|
||||
r.send(&metric{Table: "vpn_mesh_connection", Tags: connTags, Fields: connFields})
|
||||
}
|
||||
|
||||
statusTags := map[string]string{
|
||||
"site_name": m.SiteName,
|
||||
"source": m.SourceName,
|
||||
"mesh_name": m.Name,
|
||||
"status_site": s.SiteID,
|
||||
}
|
||||
|
||||
statusFields := map[string]any{
|
||||
"errors": len(s.Errors),
|
||||
"warnings": len(s.Warnings),
|
||||
}
|
||||
|
||||
r.send(&metric{Table: "vpn_mesh_status", Tags: statusTags, Fields: statusFields})
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,7 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) {
|
||||
if c == nil {
|
||||
return nil, fmt.Errorf("controller is nil")
|
||||
}
|
||||
|
||||
if c.Unifi == nil {
|
||||
return nil, fmt.Errorf("controller client is nil (e.g. after 429 or auth failure): %s", c.URL)
|
||||
}
|
||||
@@ -246,6 +247,14 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) {
|
||||
u.LogDebugf("Found %d PortAnomalies entries", len(m.PortAnomalies))
|
||||
}
|
||||
|
||||
// Get Site Magic site-to-site VPN mesh data
|
||||
if m.VPNMeshes, err = c.Unifi.GetMagicSiteToSiteVPN(sites); err != nil {
|
||||
// Don't fail collection if VPN data fails - older controllers may not have this endpoint
|
||||
u.LogDebugf("unifi.GetMagicSiteToSiteVPN(%s): %v (continuing)", c.URL, err)
|
||||
} else {
|
||||
u.LogDebugf("Found %d VPNMeshes entries", len(m.VPNMeshes))
|
||||
}
|
||||
|
||||
// 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.
|
||||
if c != nil && c.Unifi != nil {
|
||||
@@ -255,9 +264,11 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) {
|
||||
u.LogErrorf("updateWeb panic recovered (upgrade image if this persists): %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
updateWeb(c, m)
|
||||
}()
|
||||
}
|
||||
|
||||
return u.augmentMetrics(c, m), nil
|
||||
}
|
||||
|
||||
@@ -410,10 +421,12 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
||||
if isDefaultSiteName(site.Name) {
|
||||
site.Name = c.DefaultSiteNameOverride
|
||||
}
|
||||
|
||||
if isDefaultSiteName(site.SiteName) {
|
||||
site.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
}
|
||||
|
||||
m.Sites = append(m.Sites, site)
|
||||
}
|
||||
|
||||
@@ -422,6 +435,7 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(site.SiteName) {
|
||||
site.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
|
||||
m.SitesDPI = append(m.SitesDPI, site)
|
||||
}
|
||||
}
|
||||
@@ -431,6 +445,7 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(speedTest.SiteName) {
|
||||
speedTest.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
|
||||
m.SpeedTests = append(m.SpeedTests, speedTest)
|
||||
}
|
||||
|
||||
@@ -440,6 +455,7 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(traffic.TrafficSite.SiteName) {
|
||||
traffic.TrafficSite.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
|
||||
m.CountryTraffic = append(m.CountryTraffic, traffic)
|
||||
}
|
||||
|
||||
@@ -448,6 +464,7 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(lease.SiteName) {
|
||||
lease.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
|
||||
m.DHCPLeases = append(m.DHCPLeases, lease)
|
||||
}
|
||||
|
||||
@@ -487,6 +504,14 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
||||
m.PortAnomalies = append(m.PortAnomalies, anomaly)
|
||||
}
|
||||
|
||||
for _, mesh := range metrics.VPNMeshes {
|
||||
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(mesh.SiteName) {
|
||||
mesh.SiteName = c.DefaultSiteNameOverride
|
||||
}
|
||||
|
||||
m.VPNMeshes = append(m.VPNMeshes, mesh)
|
||||
}
|
||||
|
||||
// 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 allows us to use the console name for Cloud Gateways while keeping
|
||||
@@ -504,6 +529,7 @@ func isDefaultSiteName(siteName string) bool {
|
||||
if siteName == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
lower := strings.ToLower(siteName)
|
||||
// Check for exact match or if it contains "default" as a word
|
||||
return lower == "default" || strings.Contains(lower, "default")
|
||||
@@ -572,6 +598,7 @@ func applySiteNameOverride(m *poller.Metrics, overrideName string) {
|
||||
if isDefaultSiteName(site.Name) {
|
||||
site.Name = overrideName
|
||||
}
|
||||
|
||||
if isDefaultSiteName(site.SiteName) {
|
||||
site.SiteName = overrideName
|
||||
}
|
||||
@@ -637,6 +664,14 @@ func applySiteNameOverride(m *poller.Metrics, overrideName string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := range m.VPNMeshes {
|
||||
if mesh, ok := m.VPNMeshes[i].(*unifi.MagicSiteToSiteVPN); ok {
|
||||
if isDefaultSiteName(mesh.SiteName) {
|
||||
mesh.SiteName = overrideName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// this is a helper function for augmentMetrics.
|
||||
|
||||
@@ -60,7 +60,7 @@ type Controller struct {
|
||||
Sites []string `json:"sites" toml:"sites" xml:"site" yaml:"sites"`
|
||||
DefaultSiteNameOverride string `json:"default_site_name_override" toml:"default_site_name_override" xml:"default_site_name_override" yaml:"default_site_name_override"`
|
||||
Remote bool `json:"remote" toml:"remote" xml:"remote,attr" yaml:"remote"`
|
||||
ConsoleID string `json:"console_id,omitempty" toml:"console_id,omitempty" xml:"console_id,omitempty" yaml:"console_id,omitempty"`
|
||||
ConsoleID string `json:"console_id,omitempty" toml:"console_id,omitempty" xml:"console_id,omitempty" yaml:"console_id,omitempty"`
|
||||
Unifi *unifi.Unifi `json:"-" toml:"-" xml:"-" yaml:"-"`
|
||||
ID string `json:"id,omitempty"` // this is an output, not an input.
|
||||
}
|
||||
@@ -68,31 +68,32 @@ type Controller struct {
|
||||
// Config contains our configuration data.
|
||||
type Config struct {
|
||||
sync.RWMutex // locks the Unifi struct member when re-authing to unifi.
|
||||
Default Controller `json:"defaults" toml:"defaults" xml:"default" yaml:"defaults"`
|
||||
Disable bool `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"`
|
||||
Dynamic bool `json:"dynamic" toml:"dynamic" xml:"dynamic,attr" yaml:"dynamic"`
|
||||
Remote bool `json:"remote" toml:"remote" xml:"remote,attr" yaml:"remote"`
|
||||
Default Controller `json:"defaults" toml:"defaults" xml:"default" yaml:"defaults"`
|
||||
Disable bool `json:"disable" toml:"disable" xml:"disable,attr" yaml:"disable"`
|
||||
Dynamic bool `json:"dynamic" toml:"dynamic" xml:"dynamic,attr" yaml:"dynamic"`
|
||||
Remote bool `json:"remote" toml:"remote" xml:"remote,attr" yaml:"remote"`
|
||||
RemoteAPIKey string `json:"remote_api_key" toml:"remote_api_key" xml:"remote_api_key" yaml:"remote_api_key"`
|
||||
Controllers []*Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"`
|
||||
Controllers []*Controller `json:"controllers" toml:"controller" xml:"controller" yaml:"controllers"`
|
||||
}
|
||||
|
||||
// Metrics is simply a useful container for everything.
|
||||
type Metrics struct {
|
||||
TS time.Time
|
||||
Sites []*unifi.Site
|
||||
Clients []*unifi.Client
|
||||
SitesDPI []*unifi.DPITable
|
||||
ClientsDPI []*unifi.DPITable
|
||||
CountryTraffic []*unifi.UsageByCountry
|
||||
RogueAPs []*unifi.RogueAP
|
||||
SpeedTests []*unifi.SpeedTestResult
|
||||
Devices *unifi.Devices
|
||||
TS time.Time
|
||||
Sites []*unifi.Site
|
||||
Clients []*unifi.Client
|
||||
SitesDPI []*unifi.DPITable
|
||||
ClientsDPI []*unifi.DPITable
|
||||
CountryTraffic []*unifi.UsageByCountry
|
||||
RogueAPs []*unifi.RogueAP
|
||||
SpeedTests []*unifi.SpeedTestResult
|
||||
Devices *unifi.Devices
|
||||
DHCPLeases []*unifi.DHCPLease
|
||||
WANConfigs []*unifi.WANEnrichedConfiguration
|
||||
Sysinfos []*unifi.Sysinfo
|
||||
FirewallPolicies []*unifi.FirewallPolicy
|
||||
Topologies []*unifi.Topology
|
||||
PortAnomalies []*unifi.PortAnomaly
|
||||
VPNMeshes []*unifi.MagicSiteToSiteVPN
|
||||
}
|
||||
|
||||
func init() { // nolint: gochecknoinits
|
||||
@@ -158,17 +159,20 @@ func (u *InputUnifi) getUnifi(c *Controller) error {
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
|
||||
backoff := 30 * time.Second
|
||||
|
||||
for attempt := 0; attempt < maxAuthRetries; attempt++ {
|
||||
c.Unifi, lastErr = unifi.NewUnifi(cfg)
|
||||
if lastErr == nil {
|
||||
u.LogDebugf("Authenticated with controller successfully, %s", c.URL)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !errors.Is(lastErr, unifi.ErrTooManyRequests) {
|
||||
c.Unifi = nil
|
||||
|
||||
return fmt.Errorf("unifi controller: %w", lastErr)
|
||||
}
|
||||
|
||||
@@ -181,6 +185,7 @@ func (u *InputUnifi) getUnifi(c *Controller) error {
|
||||
u.Logf("Controller %s returned 429 Too Many Requests; waiting %v before retry (%d/%d)",
|
||||
c.URL, backoff, attempt+1, maxAuthRetries)
|
||||
time.Sleep(backoff)
|
||||
|
||||
if backoff < 5*time.Minute {
|
||||
backoff = backoff * 2
|
||||
}
|
||||
@@ -188,6 +193,7 @@ func (u *InputUnifi) getUnifi(c *Controller) error {
|
||||
}
|
||||
|
||||
c.Unifi = nil
|
||||
|
||||
return fmt.Errorf("unifi controller: %w (gave up after %d retries)", lastErr, maxAuthRetries)
|
||||
}
|
||||
|
||||
@@ -451,6 +457,7 @@ func (u *InputUnifi) setControllerDefaults(c *Controller) *Controller { //nolint
|
||||
if c.APIKey == "" {
|
||||
c.APIKey = u.Default.APIKey
|
||||
}
|
||||
|
||||
c.User = ""
|
||||
c.Pass = ""
|
||||
} else {
|
||||
|
||||
@@ -50,6 +50,7 @@ func (u *OtelOutput) reportMetrics(m *poller.Metrics, _ *poller.Events) (*Report
|
||||
u.exportFirewallPolicies(ctx, meter, m, r)
|
||||
u.exportTopology(ctx, meter, m, r)
|
||||
u.exportPortAnomalies(ctx, meter, m, r)
|
||||
u.exportVPNMeshes(ctx, meter, m, r)
|
||||
|
||||
r.Elapsed = time.Since(start)
|
||||
|
||||
@@ -215,6 +216,7 @@ func (u *OtelOutput) recordGauge(
|
||||
g, err := meter.Float64ObservableGauge(name, metric.WithDescription(description))
|
||||
if err != nil {
|
||||
r.Errors++
|
||||
|
||||
u.LogDebugf("otel: creating gauge %s: %v", name, err)
|
||||
|
||||
return
|
||||
@@ -227,6 +229,7 @@ func (u *OtelOutput) recordGauge(
|
||||
}, g)
|
||||
if err != nil {
|
||||
r.Errors++
|
||||
|
||||
u.LogDebugf("otel: registering callback for %s: %v", name, err)
|
||||
|
||||
return
|
||||
|
||||
79
pkg/otelunifi/vpn.go
Normal file
79
pkg/otelunifi/vpn.go
Normal file
@@ -0,0 +1,79 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// exportVPNMeshes emits Site Magic site-to-site VPN mesh metrics.
|
||||
func (u *OtelOutput) exportVPNMeshes(ctx context.Context, meter metric.Meter, m *poller.Metrics, r *Report) {
|
||||
for _, item := range m.VPNMeshes {
|
||||
mesh, ok := item.(*unifi.MagicSiteToSiteVPN)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
meshAttrs := attribute.NewSet(
|
||||
attribute.String("site_name", mesh.SiteName),
|
||||
attribute.String("source", mesh.SourceName),
|
||||
attribute.String("mesh_name", mesh.Name),
|
||||
)
|
||||
|
||||
paused := 0.0
|
||||
if mesh.Pause.Val {
|
||||
paused = 1.0
|
||||
}
|
||||
|
||||
u.recordGauge(ctx, meter, r, "unifi_vpn_mesh_paused",
|
||||
"Site Magic VPN mesh paused (1/0)", paused, meshAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_vpn_mesh_connections_total",
|
||||
"Total connections in Site Magic VPN mesh", float64(len(mesh.Connections)), meshAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_vpn_mesh_devices_total",
|
||||
"Total devices in Site Magic VPN mesh", float64(len(mesh.Devices)), meshAttrs)
|
||||
|
||||
for i := range mesh.Status {
|
||||
s := &mesh.Status[i]
|
||||
|
||||
statusAttrs := attribute.NewSet(
|
||||
attribute.String("site_name", mesh.SiteName),
|
||||
attribute.String("source", mesh.SourceName),
|
||||
attribute.String("mesh_name", mesh.Name),
|
||||
attribute.String("status_site", s.SiteID),
|
||||
)
|
||||
|
||||
u.recordGauge(ctx, meter, r, "unifi_vpn_mesh_status_errors",
|
||||
"Number of errors for a site in a Site Magic VPN mesh", float64(len(s.Errors)), statusAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_vpn_mesh_status_warnings",
|
||||
"Number of warnings for a site in a Site Magic VPN mesh", float64(len(s.Warnings)), statusAttrs)
|
||||
|
||||
for j := range s.Connections {
|
||||
conn := &s.Connections[j]
|
||||
|
||||
connected := 0.0
|
||||
if conn.Connected.Val {
|
||||
connected = 1.0
|
||||
}
|
||||
|
||||
connAttrs := attribute.NewSet(
|
||||
attribute.String("site_name", mesh.SiteName),
|
||||
attribute.String("source", mesh.SourceName),
|
||||
attribute.String("mesh_name", mesh.Name),
|
||||
attribute.String("connection_id", conn.ConnectionID),
|
||||
attribute.String("status_site", s.SiteID),
|
||||
)
|
||||
|
||||
u.recordGauge(ctx, meter, r, "unifi_vpn_tunnel_connected",
|
||||
"Site Magic VPN tunnel connection status (1=connected, 0=disconnected)", connected, connAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_vpn_tunnel_association_time",
|
||||
"Site Magic VPN tunnel association Unix timestamp", conn.AssociationTime.Val, connAttrs)
|
||||
u.recordGauge(ctx, meter, r, "unifi_vpn_tunnel_errors",
|
||||
"Number of errors on a Site Magic VPN tunnel connection", float64(len(conn.Errors)), connAttrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,7 @@ type Metrics struct {
|
||||
FirewallPolicies []any
|
||||
Topologies []any
|
||||
PortAnomalies []any
|
||||
VPNMeshes []any
|
||||
ControllerStatuses []ControllerStatus
|
||||
}
|
||||
|
||||
@@ -120,10 +121,10 @@ type Config struct {
|
||||
|
||||
// Poller is the global config values.
|
||||
type Poller struct {
|
||||
Plugins []string `json:"plugins" toml:"plugins" xml:"plugin" yaml:"plugins"`
|
||||
Debug bool `json:"debug" toml:"debug" xml:"debug,attr" yaml:"debug"`
|
||||
Quiet bool `json:"quiet" toml:"quiet" xml:"quiet,attr" yaml:"quiet"`
|
||||
LogUnknownTypes bool `json:"log_unknown_types" toml:"log_unknown_types" xml:"log_unknown_types" yaml:"log_unknown_types"`
|
||||
Plugins []string `json:"plugins" toml:"plugins" xml:"plugin" yaml:"plugins"`
|
||||
Debug bool `json:"debug" toml:"debug" xml:"debug,attr" yaml:"debug"`
|
||||
Quiet bool `json:"quiet" toml:"quiet" xml:"quiet,attr" yaml:"quiet"`
|
||||
LogUnknownTypes bool `json:"log_unknown_types" toml:"log_unknown_types" xml:"log_unknown_types" yaml:"log_unknown_types"`
|
||||
}
|
||||
|
||||
// LoadPlugins reads-in dynamic shared libraries.
|
||||
|
||||
@@ -280,6 +280,7 @@ func AppendMetrics(existing *Metrics, m *Metrics) *Metrics {
|
||||
existing.FirewallPolicies = append(existing.FirewallPolicies, m.FirewallPolicies...)
|
||||
existing.Topologies = append(existing.Topologies, m.Topologies...)
|
||||
existing.PortAnomalies = append(existing.PortAnomalies, m.PortAnomalies...)
|
||||
existing.VPNMeshes = append(existing.VPNMeshes, m.VPNMeshes...)
|
||||
existing.ControllerStatuses = append(existing.ControllerStatuses, m.ControllerStatuses...)
|
||||
|
||||
return existing
|
||||
|
||||
@@ -53,6 +53,7 @@ type promUnifi struct {
|
||||
FirewallPolicy *firewallpolicy
|
||||
Topology *topology
|
||||
PortAnomaly *portanomaly
|
||||
VPNMesh *vpnmesh
|
||||
// controllerUp tracks per-controller poll success (1) or failure (0).
|
||||
controllerUp *prometheus.GaugeVec
|
||||
// This interface is passed to the Collect() method. The Collect method uses
|
||||
@@ -221,6 +222,7 @@ func (u *promUnifi) Run(c poller.Collect) error {
|
||||
u.FirewallPolicy = descFirewallPolicy(u.Namespace + "_")
|
||||
u.Topology = descTopology(u.Namespace + "_")
|
||||
u.PortAnomaly = descPortAnomaly(u.Namespace + "_")
|
||||
u.VPNMesh = descVPNMesh(u.Namespace + "_")
|
||||
u.controllerUp = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: u.Namespace + "_controller_up",
|
||||
Help: "Whether the last poll of the UniFi controller succeeded (1) or failed (0).",
|
||||
@@ -309,7 +311,7 @@ func (t *target) Describe(ch chan<- *prometheus.Desc) {
|
||||
// Describe satisfies the prometheus Collector. This returns all of the
|
||||
// metric descriptions that this packages produces.
|
||||
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, u.Topology, u.PortAnomaly} {
|
||||
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, u.PortAnomaly, u.VPNMesh} {
|
||||
v := reflect.Indirect(reflect.ValueOf(f))
|
||||
|
||||
// Loop each struct member and send it to the provided channel.
|
||||
@@ -451,6 +453,7 @@ func (u *promUnifi) loopExports(r report) {
|
||||
dhcpLeases = append(dhcpLeases, l)
|
||||
}
|
||||
}
|
||||
|
||||
if len(dhcpLeases) > 0 {
|
||||
u.exportDHCPNetworkPool(r, dhcpLeases)
|
||||
}
|
||||
@@ -501,6 +504,12 @@ func (u *promUnifi) loopExports(r report) {
|
||||
|
||||
u.exportPortAnomalies(r, portAnomalies)
|
||||
|
||||
for _, v := range m.VPNMeshes {
|
||||
if mesh, ok := v.(*unifi.MagicSiteToSiteVPN); ok {
|
||||
u.exportVPNMesh(r, mesh)
|
||||
}
|
||||
}
|
||||
|
||||
u.exportClientDPItotals(r, appTotal, catTotal)
|
||||
}
|
||||
|
||||
|
||||
83
pkg/promunifi/vpn.go
Normal file
83
pkg/promunifi/vpn.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package promunifi
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/unpoller/unifi/v5"
|
||||
)
|
||||
|
||||
type vpnmesh struct {
|
||||
MeshPaused *prometheus.Desc
|
||||
MeshConnectionsTotal *prometheus.Desc
|
||||
MeshDevicesTotal *prometheus.Desc
|
||||
TunnelConnected *prometheus.Desc
|
||||
TunnelAssociationTime *prometheus.Desc
|
||||
TunnelErrors *prometheus.Desc
|
||||
StatusErrors *prometheus.Desc
|
||||
StatusWarnings *prometheus.Desc
|
||||
}
|
||||
|
||||
func descVPNMesh(ns string) *vpnmesh {
|
||||
meshLabels := []string{"site_name", "source", "mesh_name"}
|
||||
connLabels := []string{"site_name", "source", "mesh_name", "connection_id", "status_site"}
|
||||
statusLabels := []string{"site_name", "source", "mesh_name", "status_site"}
|
||||
|
||||
nd := prometheus.NewDesc
|
||||
|
||||
return &vpnmesh{
|
||||
MeshPaused: nd(ns+"vpn_mesh_paused", "Site Magic VPN mesh paused (1/0)", meshLabels, nil),
|
||||
MeshConnectionsTotal: nd(ns+"vpn_mesh_connections_total", "Total connections in Site Magic VPN mesh", meshLabels, nil),
|
||||
MeshDevicesTotal: nd(ns+"vpn_mesh_devices_total", "Total devices in Site Magic VPN mesh", meshLabels, nil),
|
||||
TunnelConnected: nd(ns+"vpn_tunnel_connected", "Site Magic VPN tunnel connection status (1=connected, 0=disconnected)", connLabels, nil),
|
||||
TunnelAssociationTime: nd(ns+"vpn_tunnel_association_time", "Site Magic VPN tunnel association Unix timestamp", connLabels, nil),
|
||||
TunnelErrors: nd(ns+"vpn_tunnel_errors", "Number of errors on a Site Magic VPN tunnel connection", connLabels, nil),
|
||||
StatusErrors: nd(ns+"vpn_mesh_status_errors", "Number of errors for a site in a Site Magic VPN mesh", statusLabels, nil),
|
||||
StatusWarnings: nd(ns+"vpn_mesh_status_warnings", "Number of warnings for a site in a Site Magic VPN mesh", statusLabels, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *promUnifi) exportVPNMesh(r report, m *unifi.MagicSiteToSiteVPN) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
meshLabels := []string{m.SiteName, m.SourceName, m.Name}
|
||||
|
||||
paused := 0.0
|
||||
if m.Pause.Val {
|
||||
paused = 1.0
|
||||
}
|
||||
|
||||
r.send([]*metric{
|
||||
{u.VPNMesh.MeshPaused, gauge, paused, meshLabels},
|
||||
{u.VPNMesh.MeshConnectionsTotal, gauge, float64(len(m.Connections)), meshLabels},
|
||||
{u.VPNMesh.MeshDevicesTotal, gauge, float64(len(m.Devices)), meshLabels},
|
||||
})
|
||||
|
||||
for i := range m.Status {
|
||||
s := &m.Status[i]
|
||||
|
||||
statusLabels := []string{m.SiteName, m.SourceName, m.Name, s.SiteID}
|
||||
|
||||
r.send([]*metric{
|
||||
{u.VPNMesh.StatusErrors, gauge, float64(len(s.Errors)), statusLabels},
|
||||
{u.VPNMesh.StatusWarnings, gauge, float64(len(s.Warnings)), statusLabels},
|
||||
})
|
||||
|
||||
for j := range s.Connections {
|
||||
conn := &s.Connections[j]
|
||||
|
||||
connected := 0.0
|
||||
if conn.Connected.Val {
|
||||
connected = 1.0
|
||||
}
|
||||
|
||||
connLabels := []string{m.SiteName, m.SourceName, m.Name, conn.ConnectionID, s.SiteID}
|
||||
|
||||
r.send([]*metric{
|
||||
{u.VPNMesh.TunnelConnected, gauge, connected, connLabels},
|
||||
{u.VPNMesh.TunnelAssociationTime, gauge, conn.AssociationTime.Val, connLabels},
|
||||
{u.VPNMesh.TunnelErrors, gauge, float64(len(conn.Errors)), connLabels},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user