Add an ExtraLabels map[string]string field to the Loki Config struct so
users can define static key=value labels that are merged into the stream
labels of every log line sent to Loki. This allows users to distinguish
streams (e.g., by environment or datacenter) without hardcoding values.
Built-in dynamic labels (application, site_name, source, etc.) always
take precedence over extra labels to preserve existing behavior.
Example config (TOML):
[loki.extra_labels]
environment = "production"
datacenter = "us-east-1"
Closes#691
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add a per-controller `<namespace>_controller_up` Prometheus GaugeVec with
a `source` label (controller URL or configured ID). The gauge is set to 1
after each successful poll and 0 on failure, giving operators a standard
metric to alert on controller connectivity issues.
Changes:
- pkg/poller/config.go: add ControllerStatus type and ControllerStatuses
field to Metrics so any output plugin can consume per-controller health.
- pkg/poller/inputs.go: merge ControllerStatuses when AppendMetrics is
called (multiple input sources).
- pkg/inputunifi/interface.go: populate ControllerStatuses with Up=true
on success and Up=false (while still continuing) on per-controller error.
- pkg/promunifi/collector.go: declare and register a prometheus.GaugeVec
`<namespace>_controller_up`; set the gauge for each controller status
after every Collect cycle.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
save_rogue = true collected data from the controller but never wrote
any of it to the output backends. All three exporters (InfluxDB, Datadog,
Prometheus) had the same guard:
if s.Age.Val == 0 { return }
The intent was to drop stale entries, but the logic is inverted: Age==0
means brand-new or (more commonly) that the UniFi controller did not
include an "age" field in the JSON response, causing FlexInt to default
to 0. This silently discarded every rogue AP record.
Remove the guard entirely. The data was just fetched on-demand from the
controller; if the user opted in to save_rogue, they want all of it.
Fixes#405
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(inputunifi): gracefully handle 404s from remote API event endpoints
The UniFi remote API (api.ui.com) does not support legacy event endpoints
such as /stat/event, causing repeated [ERROR] log lines for users who have
save_events = true with a remote controller.
When a remote controller returns an invalid HTTP status code (e.g. 404),
log a warning and continue to the next event collector instead of
propagating the error. This keeps metrics collection working and stops
the noisy error loop.
Fixes#966
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix(inputunifi): log unsupported remote API event endpoints at Info not Error
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Wrap the GetActiveDHCPLeasesWithAssociations call in a deferred recover
so a nil-pointer panic in the unifi library (triggered when 401 errors
cause GetDevices to return nil, which was then dereferenced without a
guard in v5.18.0) can no longer crash the poller process.
Fixes#965
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Adds metrics export for UDB devices (UDB-Switch, UDB-Pro, UDB-Pro-Sector)
to all output backends. UDB-Switch is a hybrid device combining PoE switch
ports with WiFi 7 wireless bridge capability (5GHz + 6GHz radios).
- pkg/promunifi/udb.go: Prometheus metrics exporter for UDB
- pkg/influxunifi/udb.go: InfluxDB batch exporter for UDB
- pkg/datadogunifi/udb.go: Datadog batch exporter for UDB
- Wire UDB into switchExport in all three output plugins
- Add UDB to inputunifi device collection and site name override
- Update integration test expectations for InfluxDB and Datadog
- Fix addUBB() bug: was incorrectly incrementing UCI counter
Resolves#947
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* Fix panic when remote discovery fails and no controllers are configured
Call setDefaults(&u.Default) before logController(&u.Default) when
len(u.Controllers) == 0 so HashPII, DropPII, etc. are initialized
and logController does not dereference nil pointers.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: trigger CI re-run
Co-authored-by: Cursor <cursoragent@cursor.com>
* ci: use golangci-lint v2.9 for Go 1.26-compatible deps
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(influxunifi): use CelsiusSafe() for temp fields to fix InfluxDB type conflict
Write temp_* fields as float64 instead of int64 so InfluxDB does not
report 'field type conflict' when the measurement already has float.
Requires github.com/unpoller/unifi/v5 with CelsiusSafe() (unpoller/unifi#195).
Fixes#944.
Co-authored-by: Cursor <cursoragent@cursor.com>
* deps: unifi v5.17.0; nil guards and 429 retry (unpoller#943)
- Bump github.com/unpoller/unifi/v5 to v5.17.0 (CelsiusSafe, ErrNilUnifi, RateLimitError)
- inputunifi: guard pollController for nil c.Unifi; controllerID(c) in formatSites/Clients/Devices
- inputunifi: getUnifi retry with backoff on 429 (up to 5 attempts, Retry-After or exponential backoff)
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(influxunifi): expect temp_* as float after CelsiusSafe() (fix#944)
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add Sysinfo collection from stat/sysinfo endpoint
- Export controller_info, uptime, update_available, data retention, ports
- Hostname fallback: name, then site_name when API omits hostname
- Apply site name override to Sysinfo for remote/cloud
- Add Discover/Discoverer for endpoint discovery
- Require unpoller/unifi v5.15.0
Co-authored-by: Cursor <cursoragent@cursor.com>
Restores the browser-based endpoint discovery script that was
mistakenly removed in the Go --discover PR. Optional: use this for
broader discovery (XHR capture) or unpoller --discover for known endpoints.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Require github.com/unpoller/unifi/v5 v5.13.0 (DiscoverEndpoints in release)
- Remove go.mod replace and workflow steps that cloned unifi for CI
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add --discover and --discover-output to unpoller; uses first unifi
controller from config to probe known API endpoints and write a
shareable markdown report.
- Add Discoverer interface and RunDiscover(); inputunifi implements
Discoverer via unifi.DiscoverEndpoints.
- Remove tools/endpoint-discovery/ (Python/Playwright).
- Add docs/PR_936_REPLACEMENT.md. .gitignore: test config and report.
Requires unpoller/unifi with DiscoverEndpoints (replace in go.mod until
unifi release).
- Python script (Playwright) that logs in to UniFi controller and captures
XHR/fetch requests to /api and /proxy/ endpoints
- Writes API_ENDPOINTS_HEADLESS_<date>.md in tool directory (easy for users)
- Helps debug 404s (e.g. device-tags #935): users can run and share output
- Optional, read-only; not required for building or running unpoller
Add comprehensive WAN metrics support to InfluxDB and Datadog exporters:
InfluxDB Metrics (measurement: wan):
- Configuration: failover_priority, load_balance_weight, provider_download_kbps,
provider_upload_kbps, smartq_enabled, magic_enabled, vlan_enabled
- Statistics: uptime_percentage, peak_download_percent, peak_upload_percent,
max_rx_bytes_rate, max_tx_bytes_rate
- Service Provider: service_provider_asn
- Metadata: creation_timestamp
Tags: wan_id, wan_name, wan_networkgroup, wan_type, wan_load_balance_type,
isp_name, isp_city
Datadog Metrics (namespace: unpoller.wan.*):
- Same metrics as InfluxDB with gauge type
- All metrics tagged with WAN and ISP information
Changes:
- pkg/influxunifi/wan.go: New WAN exporter for InfluxDB
- pkg/influxunifi/influxdb.go: Add WAN to loopPoints and switchExport
- pkg/datadogunifi/wan.go: New WAN exporter for Datadog
- pkg/datadogunifi/datadog.go: Add WAN to loopPoints and switchExport
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add 'tag' label to all device metric descriptors
- Update exportWithTags helper to create separate metric series per tag
- Update all device export functions (UAP, USW, UDM, USG, UXG, PDU, UBB, UCI) to include tags
- Update all label arrays (VAP, Radio, Port, etc.) to include tag label
- Devices with multiple tags create multiple metric series (one per tag)
- Devices without tags export with tag=""
Requires unpoller/unifi#92
- Add .cursorrules for Cursor AI
- Add CLAUDE.md for Claude Code
- Add AGENTS.md for universal AI agent context
- Add .github/copilot-instructions.md for GitHub Copilot
These files provide comprehensive context about the UnPoller project
architecture, coding standards, and development patterns to help AI
coding assistants understand and work with the codebase effectively.
Updated the install section in the brews configuration to use the
new Homebrew formula syntax for directory creation.
Changes:
- Changed `etc.mkdir "unpoller"` to `(etc/"unpoller").mkpath`
The old syntax was causing errors with newer Homebrew versions:
TypeError: no implicit conversion of String into Integer
This fix ensures compatibility with Homebrew 4.3+ while maintaining
backward compatibility with older versions.
Fixes#742
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>