mirror of
https://github.com/unpoller/unpoller.git
synced 2026-04-05 08:54:09 -04:00
* feat(promunifi): add firewall policy metrics (closes #928) Bump unifi client to v5.22.0 and wire up firewall policy data end-to-end: - poller.Metrics: add FirewallPolicies []any slice - inputunifi: collect GetFirewallPolicies() per poll cycle; apply DefaultSiteNameOverride; augment into poller.Metrics - promunifi: export per-rule (rule_enabled, rule_index) and per-site aggregate metrics (rules_total, rules_enabled, rules_disabled, rules_by_action, rules_predefined, rules_custom, rules_logging_enabled) Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * feat: export firewall policies to influx, datadog, and otel outputs Extends firewall policy support (PR #979) to all remaining output plugins: - influxunifi: batchFirewallPolicy() writes measurement "firewall_policy" with tags (rule_name, action, protocol, ip_version, source/dest zone, site_name, source) and fields (enabled, index, predefined, logging) - datadogunifi: batchFirewallPolicy() emits the same data as Datadog gauges under the "firewall_policy.*" namespace - otelunifi: exportFirewallPolicies() emits per-rule gauges (unifi_firewall_rule_enabled, unifi_firewall_rule_index) and per-site aggregates (rules_total, rules_enabled, rules_disabled, rules_by_action, rules_predefined, rules_custom, rules_logging_enabled) Also rebases onto master to pick up the otelunifi plugin (PR #978). 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:
4
go.mod
4
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.20.1-0.20260323223726-f363f61cdbe3
|
github.com/unpoller/unifi/v5 v5.22.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
|
||||||
@@ -36,7 +36,7 @@ require (
|
|||||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/net v0.51.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -89,6 +89,10 @@ 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.20.1-0.20260323223726-f363f61cdbe3 h1:gBBSmwjzzTAVN56mthkm7J7sN/M6bgjR0RUMTtBwPO0=
|
github.com/unpoller/unifi/v5 v5.20.1-0.20260323223726-f363f61cdbe3 h1:gBBSmwjzzTAVN56mthkm7J7sN/M6bgjR0RUMTtBwPO0=
|
||||||
github.com/unpoller/unifi/v5 v5.20.1-0.20260323223726-f363f61cdbe3/go.mod h1:vSIXIclPG9dpKxUp+pavfgENHWaTZXvDg7F036R1YCo=
|
github.com/unpoller/unifi/v5 v5.20.1-0.20260323223726-f363f61cdbe3/go.mod h1:vSIXIclPG9dpKxUp+pavfgENHWaTZXvDg7F036R1YCo=
|
||||||
|
github.com/unpoller/unifi/v5 v5.21.0 h1:rVmZjiKDwu35JYuFhhJTfCU2itcFy9uEfySmjOf5JFU=
|
||||||
|
github.com/unpoller/unifi/v5 v5.21.0/go.mod h1:0R6t/SKaS8eoOrTkSYwzVb292KG5eQfbKEuevuES0So=
|
||||||
|
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/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=
|
||||||
@@ -122,6 +126,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
|||||||
@@ -321,6 +321,10 @@ func (u *DatadogUnifi) loopPoints(r report) {
|
|||||||
for _, w := range m.WANConfigs {
|
for _, w := range m.WANConfigs {
|
||||||
u.switchExport(r, w)
|
u.switchExport(r, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, p := range m.FirewallPolicies {
|
||||||
|
u.switchExport(r, p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
|
func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||||
@@ -361,6 +365,8 @@ func (u *DatadogUnifi) switchExport(r report, v any) { //nolint:cyclop
|
|||||||
u.batchSpeedTest(r, v)
|
u.batchSpeedTest(r, v)
|
||||||
case *unifi.WANEnrichedConfiguration:
|
case *unifi.WANEnrichedConfiguration:
|
||||||
u.batchWAN(r, v)
|
u.batchWAN(r, v)
|
||||||
|
case *unifi.FirewallPolicy:
|
||||||
|
u.batchFirewallPolicy(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)
|
||||||
|
|||||||
51
pkg/datadogunifi/firewall_policies.go
Normal file
51
pkg/datadogunifi/firewall_policies.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package datadogunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// batchFirewallPolicy generates firewall policy datapoints for Datadog.
|
||||||
|
func (u *DatadogUnifi) batchFirewallPolicy(r report, p *unifi.FirewallPolicy) {
|
||||||
|
if p == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
metricName := metricNamespace("firewall_policy")
|
||||||
|
|
||||||
|
tags := []string{
|
||||||
|
tag("rule_name", p.Name),
|
||||||
|
tag("action", p.Action),
|
||||||
|
tag("protocol", p.Protocol),
|
||||||
|
tag("ip_version", p.IPVersion),
|
||||||
|
tag("source_zone", p.Source.ZoneID),
|
||||||
|
tag("dest_zone", p.Destination.ZoneID),
|
||||||
|
tag("site_name", p.SiteName),
|
||||||
|
tag("source", p.SourceName),
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled := 0.0
|
||||||
|
if p.Enabled.Val {
|
||||||
|
enabled = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
predefined := 0.0
|
||||||
|
if p.Predefined.Val {
|
||||||
|
predefined = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
logging := 0.0
|
||||||
|
if p.Logging.Val {
|
||||||
|
logging = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]float64{
|
||||||
|
"enabled": enabled,
|
||||||
|
"index": p.Index.Val,
|
||||||
|
"predefined": predefined,
|
||||||
|
"logging": logging,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, value := range data {
|
||||||
|
_ = r.reportGauge(metricName(name), value, tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
pkg/influxunifi/firewall_policies.go
Normal file
47
pkg/influxunifi/firewall_policies.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package influxunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// batchFirewallPolicy generates a firewall policy datapoint for InfluxDB.
|
||||||
|
func (u *InfluxUnifi) batchFirewallPolicy(r report, p *unifi.FirewallPolicy) {
|
||||||
|
if p == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := map[string]string{
|
||||||
|
"rule_name": p.Name,
|
||||||
|
"action": p.Action,
|
||||||
|
"protocol": p.Protocol,
|
||||||
|
"ip_version": p.IPVersion,
|
||||||
|
"source_zone": p.Source.ZoneID,
|
||||||
|
"dest_zone": p.Destination.ZoneID,
|
||||||
|
"site_name": p.SiteName,
|
||||||
|
"source": p.SourceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled := 0
|
||||||
|
if p.Enabled.Val {
|
||||||
|
enabled = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
predefined := 0
|
||||||
|
if p.Predefined.Val {
|
||||||
|
predefined = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
logging := 0
|
||||||
|
if p.Logging.Val {
|
||||||
|
logging = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := map[string]any{
|
||||||
|
"enabled": enabled,
|
||||||
|
"index": p.Index.Val,
|
||||||
|
"predefined": predefined,
|
||||||
|
"logging": logging,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.send(&metric{Table: "firewall_policy", Tags: tags, Fields: fields})
|
||||||
|
}
|
||||||
@@ -434,6 +434,10 @@ func (u *InfluxUnifi) loopPoints(r report) {
|
|||||||
for _, w := range m.WANConfigs {
|
for _, w := range m.WANConfigs {
|
||||||
u.switchExport(r, w)
|
u.switchExport(r, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, p := range m.FirewallPolicies {
|
||||||
|
u.switchExport(r, p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
|
func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
|
||||||
@@ -474,6 +478,8 @@ func (u *InfluxUnifi) switchExport(r report, v any) { //nolint:cyclop
|
|||||||
u.batchSpeedTest(r, v)
|
u.batchSpeedTest(r, v)
|
||||||
case *unifi.WANEnrichedConfiguration:
|
case *unifi.WANEnrichedConfiguration:
|
||||||
u.batchWAN(r, v)
|
u.batchWAN(r, v)
|
||||||
|
case *unifi.FirewallPolicy:
|
||||||
|
u.batchFirewallPolicy(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)
|
||||||
|
|||||||
@@ -214,6 +214,14 @@ func (u *InputUnifi) pollController(c *Controller) (*poller.Metrics, error) {
|
|||||||
u.LogDebugf("Found %d WAN configuration entries", len(m.WANConfigs))
|
u.LogDebugf("Found %d WAN configuration entries", len(m.WANConfigs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get firewall policies
|
||||||
|
if m.FirewallPolicies, err = c.Unifi.GetFirewallPolicies(sites); err != nil {
|
||||||
|
// Don't fail collection if firewall policies fail - older controllers may not have this endpoint
|
||||||
|
u.LogDebugf("unifi.GetFirewallPolicies(%s): %v (continuing)", c.URL, err)
|
||||||
|
} else {
|
||||||
|
u.LogDebugf("Found %d FirewallPolicies entries", len(m.FirewallPolicies))
|
||||||
|
}
|
||||||
|
|
||||||
// Get controller system info (UniFi OS only)
|
// Get controller system info (UniFi OS only)
|
||||||
if m.Sysinfos, err = c.Unifi.GetSysinfo(sites); err != nil {
|
if m.Sysinfos, err = c.Unifi.GetSysinfo(sites); err != nil {
|
||||||
// Don't fail collection if sysinfo fails - older controllers may not have this endpoint
|
// Don't fail collection if sysinfo fails - older controllers may not have this endpoint
|
||||||
@@ -437,6 +445,15 @@ func (u *InputUnifi) augmentMetrics(c *Controller, metrics *Metrics) *poller.Met
|
|||||||
m.Sysinfos = append(m.Sysinfos, sysinfo)
|
m.Sysinfos = append(m.Sysinfos, sysinfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, policy := range metrics.FirewallPolicies {
|
||||||
|
// Apply site name override for firewall policies if configured
|
||||||
|
if c.DefaultSiteNameOverride != "" && isDefaultSiteName(policy.SiteName) {
|
||||||
|
policy.SiteName = c.DefaultSiteNameOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
m.FirewallPolicies = append(m.FirewallPolicies, policy)
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@@ -562,6 +579,15 @@ func applySiteNameOverride(m *poller.Metrics, overrideName string) {
|
|||||||
_ = wanConfig
|
_ = wanConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply to firewall policies
|
||||||
|
for i := range m.FirewallPolicies {
|
||||||
|
if policy, ok := m.FirewallPolicies[i].(*unifi.FirewallPolicy); ok {
|
||||||
|
if isDefaultSiteName(policy.SiteName) {
|
||||||
|
policy.SiteName = overrideName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is a helper function for augmentMetrics.
|
// this is a helper function for augmentMetrics.
|
||||||
|
|||||||
@@ -87,9 +87,10 @@ type Metrics struct {
|
|||||||
RogueAPs []*unifi.RogueAP
|
RogueAPs []*unifi.RogueAP
|
||||||
SpeedTests []*unifi.SpeedTestResult
|
SpeedTests []*unifi.SpeedTestResult
|
||||||
Devices *unifi.Devices
|
Devices *unifi.Devices
|
||||||
DHCPLeases []*unifi.DHCPLease
|
DHCPLeases []*unifi.DHCPLease
|
||||||
WANConfigs []*unifi.WANEnrichedConfiguration
|
WANConfigs []*unifi.WANEnrichedConfiguration
|
||||||
Sysinfos []*unifi.Sysinfo
|
Sysinfos []*unifi.Sysinfo
|
||||||
|
FirewallPolicies []*unifi.FirewallPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { // nolint: gochecknoinits
|
func init() { // nolint: gochecknoinits
|
||||||
|
|||||||
116
pkg/otelunifi/firewall_policies.go
Normal file
116
pkg/otelunifi/firewall_policies.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// exportFirewallPolicies emits per-rule and per-site aggregate firewall policy metrics.
|
||||||
|
func (u *OtelOutput) exportFirewallPolicies(ctx context.Context, meter metric.Meter, m *poller.Metrics, r *Report) {
|
||||||
|
type siteKey struct{ site, source string }
|
||||||
|
type siteStats struct {
|
||||||
|
total int
|
||||||
|
enabled int
|
||||||
|
disabled int
|
||||||
|
predef int
|
||||||
|
custom int
|
||||||
|
logging int
|
||||||
|
byAction map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
sites := make(map[siteKey]*siteStats)
|
||||||
|
|
||||||
|
for _, item := range m.FirewallPolicies {
|
||||||
|
p, ok := item.(*unifi.FirewallPolicy)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := attribute.NewSet(
|
||||||
|
attribute.String("rule_name", p.Name),
|
||||||
|
attribute.String("action", p.Action),
|
||||||
|
attribute.String("protocol", p.Protocol),
|
||||||
|
attribute.String("ip_version", p.IPVersion),
|
||||||
|
attribute.String("source_zone", p.Source.ZoneID),
|
||||||
|
attribute.String("dest_zone", p.Destination.ZoneID),
|
||||||
|
attribute.String("site_name", p.SiteName),
|
||||||
|
attribute.String("source", p.SourceName),
|
||||||
|
)
|
||||||
|
|
||||||
|
enabled := 0.0
|
||||||
|
if p.Enabled.Val {
|
||||||
|
enabled = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_firewall_rule_enabled",
|
||||||
|
"Firewall rule enabled status (1=enabled, 0=disabled)", enabled, attrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_firewall_rule_index",
|
||||||
|
"Firewall rule priority index", p.Index.Val, attrs)
|
||||||
|
|
||||||
|
// Accumulate site-level stats
|
||||||
|
key := siteKey{p.SiteName, p.SourceName}
|
||||||
|
if _, ok := sites[key]; !ok {
|
||||||
|
sites[key] = &siteStats{byAction: make(map[string]int)}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := sites[key]
|
||||||
|
s.total++
|
||||||
|
|
||||||
|
if p.Enabled.Val {
|
||||||
|
s.enabled++
|
||||||
|
} else {
|
||||||
|
s.disabled++
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Predefined.Val {
|
||||||
|
s.predef++
|
||||||
|
} else {
|
||||||
|
s.custom++
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Logging.Val {
|
||||||
|
s.logging++
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Action != "" {
|
||||||
|
s.byAction[p.Action]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit per-site aggregate metrics
|
||||||
|
for key, s := range sites {
|
||||||
|
siteAttrs := attribute.NewSet(
|
||||||
|
attribute.String("site_name", key.site),
|
||||||
|
attribute.String("source", key.source),
|
||||||
|
)
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_firewall_rules_total",
|
||||||
|
"Total number of firewall rules", float64(s.total), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_firewall_rules_enabled",
|
||||||
|
"Number of enabled firewall rules", float64(s.enabled), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_firewall_rules_disabled",
|
||||||
|
"Number of disabled firewall rules", float64(s.disabled), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_firewall_rules_predefined",
|
||||||
|
"Number of predefined firewall rules", float64(s.predef), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_firewall_rules_custom",
|
||||||
|
"Number of custom firewall rules", float64(s.custom), siteAttrs)
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_firewall_rules_logging_enabled",
|
||||||
|
"Number of firewall rules with logging enabled", float64(s.logging), siteAttrs)
|
||||||
|
|
||||||
|
for action, count := range s.byAction {
|
||||||
|
actionAttrs := attribute.NewSet(
|
||||||
|
attribute.String("action", action),
|
||||||
|
attribute.String("site_name", key.site),
|
||||||
|
attribute.String("source", key.source),
|
||||||
|
)
|
||||||
|
|
||||||
|
u.recordGauge(ctx, meter, r, "unifi_firewall_rules_by_action",
|
||||||
|
"Number of firewall rules grouped by action", float64(count), actionAttrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ func (u *OtelOutput) reportMetrics(m *poller.Metrics, _ *poller.Events) (*Report
|
|||||||
u.exportSites(ctx, meter, m, r)
|
u.exportSites(ctx, meter, m, r)
|
||||||
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)
|
||||||
|
|
||||||
r.Elapsed = time.Since(start)
|
r.Elapsed = time.Since(start)
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ type Metrics struct {
|
|||||||
DHCPLeases []any
|
DHCPLeases []any
|
||||||
WANConfigs []any
|
WANConfigs []any
|
||||||
Sysinfos []any
|
Sysinfos []any
|
||||||
|
FirewallPolicies []any
|
||||||
ControllerStatuses []ControllerStatus
|
ControllerStatuses []ControllerStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,7 @@ func AppendMetrics(existing *Metrics, m *Metrics) *Metrics {
|
|||||||
existing.DHCPLeases = append(existing.DHCPLeases, m.DHCPLeases...)
|
existing.DHCPLeases = append(existing.DHCPLeases, m.DHCPLeases...)
|
||||||
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.ControllerStatuses = append(existing.ControllerStatuses, m.ControllerStatuses...)
|
existing.ControllerStatuses = append(existing.ControllerStatuses, m.ControllerStatuses...)
|
||||||
|
|
||||||
return existing
|
return existing
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ type promUnifi struct {
|
|||||||
DHCPLease *dhcplease
|
DHCPLease *dhcplease
|
||||||
WAN *wan
|
WAN *wan
|
||||||
Controller *controller
|
Controller *controller
|
||||||
|
FirewallPolicy *firewallpolicy
|
||||||
// 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
|
||||||
@@ -215,6 +216,7 @@ func (u *promUnifi) Run(c poller.Collect) error {
|
|||||||
u.DHCPLease = descDHCPLease(u.Namespace + "_")
|
u.DHCPLease = descDHCPLease(u.Namespace + "_")
|
||||||
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.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).",
|
||||||
@@ -303,7 +305,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} {
|
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} {
|
||||||
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.
|
||||||
@@ -470,6 +472,16 @@ func (u *promUnifi) loopExports(r report) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export firewall policy metrics
|
||||||
|
firewallPolicies := make([]*unifi.FirewallPolicy, 0, len(m.FirewallPolicies))
|
||||||
|
for _, p := range m.FirewallPolicies {
|
||||||
|
if policy, ok := p.(*unifi.FirewallPolicy); ok {
|
||||||
|
firewallPolicies = append(firewallPolicies, policy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u.exportFirewallPolicies(r, firewallPolicies)
|
||||||
|
|
||||||
u.exportClientDPItotals(r, appTotal, catTotal)
|
u.exportClientDPItotals(r, appTotal, catTotal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
150
pkg/promunifi/firewall_policies.go
Normal file
150
pkg/promunifi/firewall_policies.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package promunifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/unpoller/unifi/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type firewallpolicy struct {
|
||||||
|
// Per-rule metrics
|
||||||
|
RuleEnabled *prometheus.Desc
|
||||||
|
RuleIndex *prometheus.Desc
|
||||||
|
// Aggregate metrics
|
||||||
|
RulesTotal *prometheus.Desc
|
||||||
|
RulesEnabled *prometheus.Desc
|
||||||
|
RulesDisabled *prometheus.Desc
|
||||||
|
RulesByAction *prometheus.Desc
|
||||||
|
RulesPredefined *prometheus.Desc
|
||||||
|
RulesCustom *prometheus.Desc
|
||||||
|
RulesLoggingEnabled *prometheus.Desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func descFirewallPolicy(ns string) *firewallpolicy {
|
||||||
|
// Per-rule labels
|
||||||
|
ruleLabels := []string{
|
||||||
|
"rule_name",
|
||||||
|
"action",
|
||||||
|
"protocol",
|
||||||
|
"ip_version",
|
||||||
|
"source_zone",
|
||||||
|
"dest_zone",
|
||||||
|
"site_name",
|
||||||
|
"source",
|
||||||
|
}
|
||||||
|
// Site-level labels
|
||||||
|
siteLabels := []string{"site_name", "source"}
|
||||||
|
// Action-level labels
|
||||||
|
actionLabels := []string{"action", "site_name", "source"}
|
||||||
|
|
||||||
|
nd := prometheus.NewDesc
|
||||||
|
|
||||||
|
return &firewallpolicy{
|
||||||
|
RuleEnabled: nd(ns+"firewall_rule_enabled", "Firewall rule enabled status (1=enabled, 0=disabled)", ruleLabels, nil),
|
||||||
|
RuleIndex: nd(ns+"firewall_rule_index", "Firewall rule priority index", ruleLabels, nil),
|
||||||
|
RulesTotal: nd(ns+"firewall_rules_total", "Total number of firewall rules", siteLabels, nil),
|
||||||
|
RulesEnabled: nd(ns+"firewall_rules_enabled", "Number of enabled firewall rules", siteLabels, nil),
|
||||||
|
RulesDisabled: nd(ns+"firewall_rules_disabled", "Number of disabled firewall rules", siteLabels, nil),
|
||||||
|
RulesByAction: nd(ns+"firewall_rules_by_action", "Number of firewall rules grouped by action", actionLabels, nil),
|
||||||
|
RulesPredefined: nd(ns+"firewall_rules_predefined", "Number of predefined firewall rules", siteLabels, nil),
|
||||||
|
RulesCustom: nd(ns+"firewall_rules_custom", "Number of custom firewall rules", siteLabels, nil),
|
||||||
|
RulesLoggingEnabled: nd(ns+"firewall_rules_logging_enabled", "Number of firewall rules with logging enabled", siteLabels, nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *promUnifi) exportFirewallPolicies(r report, policies []*unifi.FirewallPolicy) {
|
||||||
|
if len(policies) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-site aggregate counters, keyed by "siteName|source"
|
||||||
|
type siteKey struct{ site, source string }
|
||||||
|
type siteStats struct {
|
||||||
|
total int
|
||||||
|
enabled int
|
||||||
|
disabled int
|
||||||
|
predef int
|
||||||
|
custom int
|
||||||
|
logging int
|
||||||
|
site string
|
||||||
|
source string
|
||||||
|
byAction map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
sites := make(map[siteKey]*siteStats)
|
||||||
|
|
||||||
|
for _, p := range policies {
|
||||||
|
key := siteKey{p.SiteName, p.SourceName}
|
||||||
|
if _, ok := sites[key]; !ok {
|
||||||
|
sites[key] = &siteStats{
|
||||||
|
site: p.SiteName,
|
||||||
|
source: p.SourceName,
|
||||||
|
byAction: make(map[string]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := sites[key]
|
||||||
|
s.total++
|
||||||
|
|
||||||
|
if p.Enabled.Val {
|
||||||
|
s.enabled++
|
||||||
|
} else {
|
||||||
|
s.disabled++
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Predefined.Val {
|
||||||
|
s.predef++
|
||||||
|
} else {
|
||||||
|
s.custom++
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Logging.Val {
|
||||||
|
s.logging++
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.Action != "" {
|
||||||
|
s.byAction[p.Action]++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-rule metrics
|
||||||
|
ruleLabels := []string{
|
||||||
|
p.Name,
|
||||||
|
p.Action,
|
||||||
|
p.Protocol,
|
||||||
|
p.IPVersion,
|
||||||
|
p.Source.ZoneID,
|
||||||
|
p.Destination.ZoneID,
|
||||||
|
p.SiteName,
|
||||||
|
p.SourceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledVal := 0.0
|
||||||
|
if p.Enabled.Val {
|
||||||
|
enabledVal = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
r.send([]*metric{
|
||||||
|
{u.FirewallPolicy.RuleEnabled, gauge, enabledVal, ruleLabels},
|
||||||
|
{u.FirewallPolicy.RuleIndex, gauge, p.Index.Val, ruleLabels},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Site-level aggregate metrics
|
||||||
|
for _, s := range sites {
|
||||||
|
siteLabels := []string{s.site, s.source}
|
||||||
|
|
||||||
|
r.send([]*metric{
|
||||||
|
{u.FirewallPolicy.RulesTotal, gauge, float64(s.total), siteLabels},
|
||||||
|
{u.FirewallPolicy.RulesEnabled, gauge, float64(s.enabled), siteLabels},
|
||||||
|
{u.FirewallPolicy.RulesDisabled, gauge, float64(s.disabled), siteLabels},
|
||||||
|
{u.FirewallPolicy.RulesPredefined, gauge, float64(s.predef), siteLabels},
|
||||||
|
{u.FirewallPolicy.RulesCustom, gauge, float64(s.custom), siteLabels},
|
||||||
|
{u.FirewallPolicy.RulesLoggingEnabled, gauge, float64(s.logging), siteLabels},
|
||||||
|
})
|
||||||
|
|
||||||
|
for action, count := range s.byAction {
|
||||||
|
r.send([]*metric{
|
||||||
|
{u.FirewallPolicy.RulesByAction, gauge, float64(count), []string{action, s.site, s.source}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user