Prefer a known stats channel and fall back to airQuality for temperature
and humidity. Omit channels whose status is unknown, and add the
particulate and gas series only when the controller includes them.
Co-authored-by: Cursor <cursoragent@cursor.com>
Debian 11 (bullseye) has reached end of life, so the
gcr.io/distroless/static-debian11 base image no longer receives
security updates. Move to static-debian13 (trixie), which is
published for every platform the release targets (linux/amd64,
linux/arm64, linux/arm/v7).
interval=0 now turns off the Prometheus scrape cache as PR #1014
documented, and sub-15s intervals warn instead of clamping (#1083).
Adopted devices stay in Prometheus, Influx, OTel, and Datadog exports
while locate/identify is on (#1075).
Co-authored-by: Cursor <cursoragent@cursor.com>
default_site_name_override is applied in augmentMetrics, which only the
metrics path goes through. Log entries leave by collectControllerEvents,
which reads its sites straight from getFilteredSites — where the override
is deliberately not applied — so events, alarms, IDS records and system-log
entries ship with the controller's stock site name.
Four of the five site-scoped log collectors were affected; collectAnomalies
already did this inline, which is what makes the omission visible.
The consequence is worst for a poller watching several UniFi OS consoles:
each one calls its only site "default", so their log entries are
indistinguishable downstream. In Loki every stream from those consoles lands
under site_name="Default (default)" no matter which console it came from,
and no relabeling downstream can separate them again — the information is
gone by then. That is precisely the case the option was added for, and it
works for the metrics from those same consoles.
Extract the check collectAnomalies was doing into overrideSiteName and call
it from all five, so the two paths agree.
Note that only Site.Name reaches an API path; Site.SiteName is a display
name throughout the library. The override is therefore safe here, which is
what applySiteNameOverride's own comment already says ("keeping default for
API calls").
unpoller_wan_interface_state ships with site_name and nothing else to
identify where it came from. That is not enough: every UniFi OS console
names its only site "default", so site_name is "Default (default)" on all
of them. An instance polling several controllers emits WAN status that is
indistinguishable, and downstream attribution has to be guessed.
Observed in production before the fix: a scrape config guessing from
site_name filed a UDM's WAN state under the wrong customer. No error, no
missing series — just wrong data under someone else's name.
unifi/v6.1.0 (#244) added SourceName to WANStatus, stamped from the
controller URL in all three read paths. go.mod is already on v6.1.0, so
this only has to read the field.
Two lines, same shape as #1071 which did this for unpoller_wan_*.
Tests use the fakeReport already present in the package: every emitted
metric carries both site_name and source, the raw state is kept as a
label, and a nil status still produces nothing rather than panicking a
poll. TestExportWANStatusIsAttributed fails on master and passes here.
Remote discovery stored Integration display names, so extra sites were dropped when checkSites compared them to legacy Site.Name. Use unifi v6.1.0 InternalReference instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
Found by running this against a real UNVR4 rather than only the fake one.
setDefaults fills an unset user with the placeholder "unifipoller". For a
Protect-only console that placeholder was reaching Login(), the console answered
403, and the controller died during initialisation -- the same symptom #1066 set
out to fix, one layer further in. A Protect API key and no local account is the
config an operator actually writes for a UNVR, so this was the common case, not
an edge one.
Nothing on such a console uses a session unless Protect logs are wanted: the
Integration API authenticates with the key alone. So withhold the credentials
entirely in that case, and say so in the startup summary rather than naming a
username that is never sent.
Also pins the go.mod bump to unifi v6.0.1, which is the release that carries
NewProtectClient (unpoller/unifi#240).
Verified end to end against a UNVR4 (UniFi OS 5.1.31, Protect 7.2.105): the
controller comes up, logs "Auth: Protect API key only (no session needed)",
makes no login request at all, and exports 37 unpoller_protect_* series across
9 cameras and 2 bridges with unpoller_controller_up = 1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVreutpEATmBjm6PBw9RjQ
A UNVR or UNVR Pro runs UniFi Protect with no Network application installed.
UnPoller could not poll one at all: NewUnifi ends with GetServerData(), a GET of
/proxy/network/status, which such a console answers with its UniFi OS SPA HTML.
The controller entry died during initialisation and re-failed every interval,
never even printing a config summary -- while the Protect Integration API on the
same host answered every endpoint with the same key.
Set disable_network = true on that controller. It defaults to false, so nothing
about an existing config changes.
The Protect collectors were already complete and already not site-scoped; three
things stood between them and a Protect-only console:
- getUnifi now calls unifi.NewProtectClient, which skips the Network probe and
validates the Protect Integration API instead (unpoller/unifi#240).
- pollController aborted on getFilteredSites long before reaching
collectProtect, and collectControllerEvents did the same before
collectProtectLogs. The Network pass is extracted into pollNetwork and
skipped wholesale; the event collector list reduces to collectProtectLogs,
the only site-independent one.
- Metrics counted a poll successful only if it produced devices or clients. A
Protect-only console produces neither, so a filtered scrape of one -- the
Prometheus per-target path -- fell through to the dynamic-controller branch
and reported ErrDynamicLookupsDisabled despite a successful collection.
ProtectDevices now counts too.
Two smaller things worth calling out for reviewers:
- extractDevices dereferenced metrics.Devices unguarded. That was already a
latent panic; skipping the Network pass makes it reachable, so it is fixed
here rather than left for the first person to hit it.
- RawMetrics answers the raw-path kind for these consoles and rejects the
site-scoped kinds with ErrNetworkDisabled. Returning an empty result would
read as "this console has no devices" rather than "wrong question".
warnProtectOnly logs an error, without failing the controller, for the two
configurations that can never collect anything: disable_network with neither
Protect save flag, and save_protect_devices with no key to authenticate with.
Silently collecting nothing is the failure mode hardest to spot in a log.
pkg/inputunifi had no tests before this. input_test.go follows inputunas'
input_test.go: an httptest fake UNVR serving the console's SPA HTML for
everything but the Protect paths and the login, covering initialisation,
metrics, events, the filtered scrape, RawMetrics, both warnings, config binding
across toml/json/yaml/env, and that the shipped examples leave Network enabled.
TestProtectOnlyControllerFailsWithoutFlag pins the original bug against that
same console, so the flag is demonstrably what makes the difference.
Requires github.com/unpoller/unifi/v6 with NewProtectClient (unpoller/unifi#240).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVreutpEATmBjm6PBw9RjQ
Every unpoller_wan_* series shipped with site_name="" and source="". The
exporter said so itself:
cfg.WANLoadBalanceType,
"", // site_name - will be set by caller if available
"", // source - will be set by caller if available
The caller had nothing to set them from: WANEnrichedConfiguration carried
no identity. unifi/v6.0.3 fixes that upstream — GetWANEnrichedConfiguration
now stamps SiteName and SourceName from the site it fetched, the same way
GetSiteDPI does.
This bumps to v6.0.3 and fills the labels in. Two slices needed it, not
one: the base label set and the provider label set built further down for
the isp_name/isp_city descriptors. The test caught the second, which I had
missed.
Why it matters: an instance polling several controllers emitted WAN
metrics that were indistinguishable from one another, since wan_id is the
only other distinguishing label. Attributing them downstream meant
hardcoding a mapping in the scrape config and hoping no second controller
ever gained a gateway — when one does, its metrics are silently filed
under the wrong customer. No error, no missing series, just wrong data.
Tests use the fakeReport already present in the package. They assert every
emitted metric carries both labels, and that a nil configuration still
produces nothing rather than panicking a poll.
TestExportWANIsAttributed fails on master and passes with this change.
Release tags now fail closed without signing secrets, ship a signed Windows PE via house signerd, and replace per-arch Darwin tarballs with one stapled universal archive.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remote API discovery was unconditionally overwriting default_site_name_override
with the console name for Cloud Gateways. Only apply the console name fallback
when the user has not already configured an override.
Fixes#1057
Co-authored-by: Cursor <cursoragent@cursor.com>