From 4dc3e5cae2afc102d1633ce7598538881c097e16 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Mon, 16 Mar 2026 15:52:45 -0700 Subject: [PATCH] Add UnPoller for UniFi network metrics (#298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Deploy UnPoller as a k8s service on indri to export UniFi controller metrics to Prometheus - Custom-built container from forge mirror (`containers/unpoller/Dockerfile`) - Credentials pulled from 1Password via external-secrets - Prometheus scrape job added, docs and service-versions updated ## Test plan - [ ] Build container: `mise run container-release unpoller v2.34.0` - [ ] Update kustomization tag with built image tag - [ ] Deploy from branch: `argocd app set unpoller --revision feature/unpoller && argocd app sync unpoller` - [ ] Verify pod connects to UX7 controller (check logs) - [ ] Confirm `unpoller` target appears in Prometheus - [ ] Query `unifi_` metrics in Grafana 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.eblu.me/eblume/blumeops/pulls/298 --- argocd/apps/unpoller.yaml | 17 ++++++++ argocd/manifests/grafana/deployment.yaml | 36 ++++++++++++++++ argocd/manifests/prometheus/prometheus.yml | 8 ++++ argocd/manifests/unpoller/deployment.yaml | 42 +++++++++++++++++++ .../manifests/unpoller/external-secret.yaml | 18 ++++++++ argocd/manifests/unpoller/kustomization.yaml | 18 ++++++++ argocd/manifests/unpoller/service.yaml | 13 ++++++ argocd/manifests/unpoller/up.conf | 16 +++++++ containers/unpoller/Dockerfile | 40 ++++++++++++++++++ docs/changelog.d/feature-unpoller.feature.md | 1 + docs/reference/infrastructure/unifi.md | 10 ++++- service-versions.yaml | 7 ++++ 12 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 argocd/apps/unpoller.yaml create mode 100644 argocd/manifests/unpoller/deployment.yaml create mode 100644 argocd/manifests/unpoller/external-secret.yaml create mode 100644 argocd/manifests/unpoller/kustomization.yaml create mode 100644 argocd/manifests/unpoller/service.yaml create mode 100644 argocd/manifests/unpoller/up.conf create mode 100644 containers/unpoller/Dockerfile create mode 100644 docs/changelog.d/feature-unpoller.feature.md diff --git a/argocd/apps/unpoller.yaml b/argocd/apps/unpoller.yaml new file mode 100644 index 0000000..5eaadfb --- /dev/null +++ b/argocd/apps/unpoller.yaml @@ -0,0 +1,17 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: unpoller + namespace: argocd +spec: + project: default + source: + repoURL: ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git + targetRevision: main + path: argocd/manifests/unpoller + destination: + server: https://kubernetes.default.svc + namespace: monitoring + syncPolicy: + syncOptions: + - CreateNamespace=true diff --git a/argocd/manifests/grafana/deployment.yaml b/argocd/manifests/grafana/deployment.yaml index bdf4a6f..130618c 100644 --- a/argocd/manifests/grafana/deployment.yaml +++ b/argocd/manifests/grafana/deployment.yaml @@ -90,6 +90,42 @@ spec: volumeMounts: - name: sc-dashboard-volume mountPath: /tmp/dashboards + # Fetch UnPoller (UniFi) dashboards from forge mirror. + # Source: github.com/unpoller/dashboards (v2.0.0 Prometheus set) + - name: init-unpoller-dashboards + image: docker.io/library/alpine:kustomized + imagePullPolicy: IfNotPresent + command: ["sh", "-c"] + args: + - | + set -e + BASE_URL="https://forge.ops.eblu.me/mirrors/unpoller-dashboards/raw/branch/master/v2.0.0" + DEST="/tmp/dashboards/UniFi" + mkdir -p "$DEST" + for f in \ + # DPI dashboard requires DPI enabled on both UX7 and UnPoller + # "UniFi-Poller_ Client DPI - Prometheus.json" \ + "UniFi-Poller_ Client Insights - Prometheus.json" \ + "UniFi-Poller_ Network Sites - Prometheus.json" \ + "UniFi-Poller_ UAP Insights - Prometheus.json" \ + "UniFi-Poller_ USG Insights - Prometheus.json" \ + "UniFi-Poller_ USW Insights - Prometheus.json" \ + ; do + wget -q -O "$DEST/$f" "$BASE_URL/$(echo "$f" | sed 's/ /%20/g')" + done + # Fix datasource UIDs to match our Prometheus instance + sed -i 's/"uid": *"bdkj55oguty4gd"/"uid": "prometheus"/g' "$DEST"/*.json + sed -i 's/"uid": *"\${DS_PROMETHEUS}"/"uid": "prometheus"/g' "$DEST"/*.json + echo "Fetched $(ls "$DEST" | wc -l) UnPoller dashboards" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: RuntimeDefault + volumeMounts: + - name: sc-dashboard-volume + mountPath: /tmp/dashboards containers: # Dashboard sidecar - watches ConfigMaps with grafana_dashboard=1 - name: grafana-sc-dashboard diff --git a/argocd/manifests/prometheus/prometheus.yml b/argocd/manifests/prometheus/prometheus.yml index 2fd3252..2d2dbcf 100644 --- a/argocd/manifests/prometheus/prometheus.yml +++ b/argocd/manifests/prometheus/prometheus.yml @@ -72,6 +72,14 @@ scrape_configs: - target_label: cluster replacement: indri + # UniFi network metrics (via UnPoller exporter) + - job_name: "unpoller" + static_configs: + - targets: ["unpoller.monitoring.svc.cluster.local:9130"] + metric_relabel_configs: + - target_label: cluster + replacement: indri + # Frigate NVR metrics (via Caddy on indri — Frigate runs on ringtail) - job_name: "frigate" scheme: https diff --git a/argocd/manifests/unpoller/deployment.yaml b/argocd/manifests/unpoller/deployment.yaml new file mode 100644 index 0000000..2f7d13c --- /dev/null +++ b/argocd/manifests/unpoller/deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: unpoller + namespace: monitoring + labels: + app: unpoller +spec: + replicas: 1 + selector: + matchLabels: + app: unpoller + template: + metadata: + labels: + app: unpoller + spec: + containers: + - name: unpoller + image: registry.ops.eblu.me/blumeops/unpoller:kustomized + ports: + - containerPort: 9130 + name: metrics + env: + - name: UP_UNIFI_DEFAULT_API_KEY + valueFrom: + secretKeyRef: + name: unpoller-unifi + key: api-key + volumeMounts: + - name: config + mountPath: /etc/unpoller + resources: + requests: + cpu: 10m + memory: 32Mi + limits: + memory: 128Mi + volumes: + - name: config + configMap: + name: unpoller-config diff --git a/argocd/manifests/unpoller/external-secret.yaml b/argocd/manifests/unpoller/external-secret.yaml new file mode 100644 index 0000000..c82ec0d --- /dev/null +++ b/argocd/manifests/unpoller/external-secret.yaml @@ -0,0 +1,18 @@ +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: unpoller-unifi + namespace: monitoring +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: onepassword-blumeops + target: + name: unpoller-unifi + creationPolicy: Owner + data: + - secretKey: api-key + remoteRef: + key: unpoller + property: credential diff --git a/argocd/manifests/unpoller/kustomization.yaml b/argocd/manifests/unpoller/kustomization.yaml new file mode 100644 index 0000000..da7524d --- /dev/null +++ b/argocd/manifests/unpoller/kustomization.yaml @@ -0,0 +1,18 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: monitoring + +resources: + - deployment.yaml + - service.yaml + - external-secret.yaml + +images: + - name: registry.ops.eblu.me/blumeops/unpoller + newTag: v2.34.0-6b0005d + +configMapGenerator: + - name: unpoller-config + files: + - up.conf diff --git a/argocd/manifests/unpoller/service.yaml b/argocd/manifests/unpoller/service.yaml new file mode 100644 index 0000000..1ce870b --- /dev/null +++ b/argocd/manifests/unpoller/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: unpoller + namespace: monitoring +spec: + selector: + app: unpoller + ports: + - port: 9130 + targetPort: metrics + protocol: TCP + name: metrics diff --git a/argocd/manifests/unpoller/up.conf b/argocd/manifests/unpoller/up.conf new file mode 100644 index 0000000..0430067 --- /dev/null +++ b/argocd/manifests/unpoller/up.conf @@ -0,0 +1,16 @@ +[prometheus] + http_listen = "0.0.0.0:9130" + report_errors = true + +[influxdb] + disable = true + +[unifi] + dynamic = false + +[unifi.defaults] + # API key comes from environment variable: UP_UNIFI_DEFAULT_API_KEY + url = "https://192.168.1.1" + verify_ssl = false + save_sites = true + save_dpi = false diff --git a/containers/unpoller/Dockerfile b/containers/unpoller/Dockerfile new file mode 100644 index 0000000..0391f6d --- /dev/null +++ b/containers/unpoller/Dockerfile @@ -0,0 +1,40 @@ +# UnPoller — UniFi metrics exporter for Prometheus +# Two-stage build: Go compilation, then minimal Alpine runtime + +ARG CONTAINER_APP_VERSION=v2.34.0 + +FROM golang:alpine3.22 AS build + +ARG CONTAINER_APP_VERSION +RUN apk add --no-cache git + +RUN git clone --depth 1 --branch ${CONTAINER_APP_VERSION} \ + https://forge.ops.eblu.me/mirrors/unpoller.git /app + +WORKDIR /app + +ENV CGO_ENABLED=0 + +RUN go build -ldflags="-s -w \ + -X main.version=${CONTAINER_APP_VERSION} \ + -X main.builtBy=blumeops \ + -X golift.io/version.Version=${CONTAINER_APP_VERSION} \ + -X golift.io/version.Branch=HEAD \ + -X golift.io/version.BuildUser=blumeops \ + -X golift.io/version.Revision=blumeops-build" \ + -o /bin/unpoller . + +FROM alpine:3.22 + +LABEL org.opencontainers.image.title="UnPoller" +LABEL org.opencontainers.image.description="UniFi metrics exporter for Prometheus" +LABEL org.opencontainers.image.source="https://github.com/unpoller/unpoller" + +RUN apk add --no-cache ca-certificates tzdata + +COPY --from=build /bin/unpoller /usr/bin/unpoller + +EXPOSE 9130 +USER 65534:65534 +ENTRYPOINT ["/usr/bin/unpoller"] +CMD ["--config", "/etc/unpoller/up.conf"] diff --git a/docs/changelog.d/feature-unpoller.feature.md b/docs/changelog.d/feature-unpoller.feature.md new file mode 100644 index 0000000..848cbbc --- /dev/null +++ b/docs/changelog.d/feature-unpoller.feature.md @@ -0,0 +1 @@ +Add UnPoller deployment to monitor UniFi network metrics via Prometheus diff --git a/docs/reference/infrastructure/unifi.md b/docs/reference/infrastructure/unifi.md index 71ca744..6182880 100644 --- a/docs/reference/infrastructure/unifi.md +++ b/docs/reference/infrastructure/unifi.md @@ -1,6 +1,6 @@ --- title: UniFi -modified: 2026-02-24 +modified: 2026-03-16 tags: - infrastructure - networking @@ -69,6 +69,14 @@ Local admin account on the UX7. Credentials stored in 1Password (vault `blumeops Attempted Feb 2026 with the `ubiquiti-community/unifi` Terraform provider via Pulumi. A "no-op" update on the default LAN network reset undeclared properties, bricking the network and requiring a factory reset. The provider ecosystem is too immature for single-device infrastructure. +## Monitoring + +UniFi metrics are exported to Prometheus via [UnPoller](https://github.com/unpoller/unpoller), running as a k8s deployment in the `monitoring` namespace on indri. UnPoller polls the UX7 controller API using an API key and exposes metrics on port 9130. + +- **Prometheus job:** `unpoller` +- **Metrics prefix:** `unifi_` +- **Credentials:** 1Password item `unpoller` (vault `blumeops`, API key) + ## Related - [[hosts]] — Device inventory diff --git a/service-versions.yaml b/service-versions.yaml index 0ad1733..686a529 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -253,6 +253,13 @@ services: upstream-source: https://code.forgejo.org/forgejo/runner/releases notes: Forgejo runner on ringtail via nixpkgs; version tracks flake.lock + - name: unpoller + type: argocd + last-reviewed: 2026-03-16 + current-version: "v2.34.0" + upstream-source: https://github.com/unpoller/unpoller/releases + notes: UniFi metrics exporter for Prometheus + - name: forgejo type: ansible last-reviewed: 2026-02-22