From 511c524e6e45f93a54022fff762aa86cf461b51d Mon Sep 17 00:00:00 2001 From: Cody Lee Date: Sat, 9 May 2026 08:59:16 -0500 Subject: [PATCH] feat(influxunifi): add global tags applied to every measurement Closes #1001. Mirrors the DataDog plugin's global tags feature for InfluxDB. Per-metric tags take precedence on key collision so site/device identifiers can never be overwritten by a misconfigured global. Configurable via TOML/JSON/YAML under influxdb.tags. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/up.conf.example | 6 ++++++ examples/up.json.example | 6 +++++- examples/up.yaml.example | 3 +++ pkg/influxunifi/README.md | 21 +++++++++++++++++++++ pkg/influxunifi/influxdb.go | 29 +++++++++++++++++++++++++++-- 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/examples/up.conf.example b/examples/up.conf.example index 24394482..ca38bb8d 100644 --- a/examples/up.conf.example +++ b/examples/up.conf.example @@ -56,6 +56,12 @@ ## Record data for disabled or down (unlinked) switch ports. dead_ports = false + # Global tags applied to every InfluxDB measurement. Per-metric tags + # (site, device id, etc.) always win on key collision. + # [influxdb.tags] + # customer = "abc_corp" + # env = "prod" + # To enable output of UniFi Events to Loki, add a URL; it's disabled otherwise. # User, pass and tenant_id are optional and most folks wont set them. # Pick which logs you want per-controller in the [unifi.controller] section. diff --git a/examples/up.json.example b/examples/up.json.example index 85438477..e5bdebcc 100644 --- a/examples/up.json.example +++ b/examples/up.json.example @@ -21,7 +21,11 @@ "pass": "unifipoller", "db": "unifi", "verify_ssl": false, - "interval": "30s" + "interval": "30s", + "tags": { + "customer": "abc_corp", + "env": "prod" + } }, "webserver": { diff --git a/examples/up.yaml.example b/examples/up.yaml.example index 616939b2..1c8d3a0e 100644 --- a/examples/up.yaml.example +++ b/examples/up.yaml.example @@ -29,6 +29,9 @@ influxdb: pass: "unifipoller" db: "unifi" verify_ssl: false + tags: + customer: abc_corp + env: prod webserver: enable: false diff --git a/pkg/influxunifi/README.md b/pkg/influxunifi/README.md index 4eadfda6..afbfc9af 100644 --- a/pkg/influxunifi/README.md +++ b/pkg/influxunifi/README.md @@ -45,3 +45,24 @@ influxdb: # the influxdb api password pass: supersecret ``` + +### Global Tags + +Tags configured under `tags` are attached to every measurement written to +InfluxDB. Per-metric tags (site, device id, etc.) take precedence on key +collision so they cannot be overwritten by a misconfigured global tag. + +```yaml +influxdb: + tags: + customer: abc_corp + env: prod +``` + +Equivalent TOML: + +```toml +[influxdb.tags] + customer = "abc_corp" + env = "prod" +``` diff --git a/pkg/influxunifi/influxdb.go b/pkg/influxunifi/influxdb.go index ca54c91c..28fa59ba 100644 --- a/pkg/influxunifi/influxdb.go +++ b/pkg/influxunifi/influxdb.go @@ -61,6 +61,9 @@ type Config struct { VerifySSL bool `json:"verify_ssl" toml:"verify_ssl" xml:"verify_ssl" yaml:"verify_ssl"` // DeadPorts when true will save data for dead ports, for example ports that are down or disabled. DeadPorts bool `json:"dead_ports" toml:"dead_ports" xml:"dead_ports" yaml:"dead_ports"` + // Tags are global tags applied to every metric written to InfluxDB. Per-metric + // tags take precedence and will not be overwritten by a global tag of the same name. + Tags map[string]string `json:"tags,omitempty" toml:"tags,omitempty" xml:"tags" yaml:"tags,omitempty"` } // InfluxDB allows the data to be nested in the config file. @@ -373,11 +376,13 @@ func (u *InfluxUnifi) collect(r report, ch chan *metric) { m.TS = r.metrics().TS } + tags := u.mergeGlobalTags(m.Tags) + if u.IsVersion2 { - pt := influx.NewPoint(m.Table, m.Tags, m.Fields, m.TS) + pt := influx.NewPoint(m.Table, tags, m.Fields, m.TS) r.batchV2(m, pt) } else { - pt, err := influxV1.NewPoint(m.Table, m.Tags, m.Fields, m.TS) + pt, err := influxV1.NewPoint(m.Table, tags, m.Fields, m.TS) if err == nil { r.batchV1(m, pt) } @@ -389,6 +394,26 @@ func (u *InfluxUnifi) collect(r report, ch chan *metric) { } } +// mergeGlobalTags returns a tag map containing the per-metric tags layered on +// top of the configured global tags. Per-metric tags win on key collision so +// device/site identifiers can never be overwritten by a misconfigured global. +func (u *InfluxUnifi) mergeGlobalTags(tags map[string]string) map[string]string { + if len(u.Tags) == 0 { + return tags + } + + merged := make(map[string]string, len(u.Tags)+len(tags)) + for k, v := range u.Tags { + merged[k] = v + } + + for k, v := range tags { + merged[k] = v + } + + return merged +} + // loopPoints kicks off 3 or 7 go routines to process metrics and send them // to the collect routine through the metric channel. func (u *InfluxUnifi) loopPoints(r report) {