Add UnPoller for UniFi network metrics (#298)
All checks were successful
Build Container (Nix) / detect (push) Successful in 2s
Build Container / detect (push) Successful in 2s
Build Container (Nix) / build (unpoller) (push) Successful in 2s
Build Container / build (unpoller) (push) Successful in 7s

## 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: #298
This commit is contained in:
Erich Blume 2026-03-16 15:52:45 -07:00
commit 4dc3e5cae2
12 changed files with 225 additions and 1 deletions

17
argocd/apps/unpoller.yaml Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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