Skip to content

Instantly share code, notes, and snippets.

@rcdailey
Created January 14, 2026 04:26
Show Gist options
  • Select an option

  • Save rcdailey/c85113304b9c3f8bf2cee5df226f8f43 to your computer and use it in GitHub Desktop.

Select an option

Save rcdailey/c85113304b9c3f8bf2cee5df226f8f43 to your computer and use it in GitHub Desktop.

UniFi Link Speed Monitoring Implementation Plan

Overview

Deploy unpoller to scrape UniFi controller metrics and create VMRule alerts that detect link speed degradation. Configuration is self-contained via port naming convention in UniFi - no manual mappings required cluster-side.


1. Prerequisites (Manual Steps)

1.1 Create UniFi Read-Only User

In UniFi Network UI (https://192.168.1.1):

  1. Navigate to Settings → Admins & Users → Add Admin
  2. Create a local user:
    • Username: unpoller
    • Password: (generate secure password)
    • Role: Viewer (read-only access)
  3. Save the credentials for step 1.2

1.2 Store Credentials in Infisical

Create secrets at path /observability/unpoller/:

  • username = unpoller
  • password = (the password from step 1.1)

2. Deploy Unpoller

2.1 Directory Structure

kubernetes/apps/observability/unpoller/
├── ks.yaml              # Flux Kustomization
├── kustomization.yaml   # Kustomize resources
├── helmrelease.yaml     # HelmRelease using official unpoller chart
└── externalsecret.yaml  # Infisical credentials

2.2 Files to Create

ks.yaml - Flux Kustomization

  • spec.targetNamespace: observability
  • dependsOn: victoria-metrics-k8s-stack (ensures scraping infrastructure exists)

externalsecret.yaml - Credentials from Infisical

  • Pull username and password from /observability/unpoller/
  • Target secret: unpoller-secret

helmrelease.yaml - Official unpoller Helm chart

  • Source: oci://ghcr.io/unpoller/helm-chart/unpoller
  • Key configuration:
    • UniFi controller URL: https://192.168.1.1
    • Prometheus enabled on port 9130
    • InfluxDB disabled
    • Credentials via environment variables from secret
    • Security context: non-root, read-only filesystem
    • Resources: minimal (50m CPU, 64Mi memory)

kustomization.yaml - Resource list

  • Include helmrelease.yaml and externalsecret.yaml

2.3 Update Parent Kustomization

Add to kubernetes/apps/observability/kustomization.yaml:

- ./unpoller/ks.yaml

3. Create VMRule for Link Speed Alerts

3.1 File Location

kubernetes/apps/observability/vmrules/unifi-alerts.yaml

3.2 Alert Definitions

apiVersion: operator.victoriametrics.com/v1beta1
kind: VMRule
metadata:
  name: unifi-alerts
spec:
  groups:
  - name: unifi-link-speed
    interval: 30s
    rules:
    # Compliance: Catch unconfigured ports
    - alert: UnifiPortMissingSpeedLabel
      expr: unifi_port_speed_bps{port_name!~".+-(100M|1G|2\\.5G|10G)"} > 0
      for: 10m
      labels:
        severity: warning
      annotations:
        summary: "Port {{ $labels.port_num }} on {{ $labels.name }} missing speed label"
        description: "Port has active link but name '{{ $labels.port_name }}' doesn't follow naming convention"

    # 10G ports degraded
    - alert: UnifiLinkSpeedDegraded10G
      expr: unifi_port_speed_bps{port_name=~".+-10G"} < 10000000000
      for: 2m
      labels:
        severity: warning
      annotations:
        summary: "10G port {{ $labels.port_name }} on {{ $labels.name }} degraded"
        description: "Expected 10Gbps, negotiated {{ $value | humanize }}bps"

    # 2.5G ports degraded
    - alert: UnifiLinkSpeedDegraded2_5G
      expr: unifi_port_speed_bps{port_name=~".+-2\\.5G"} < 2500000000
      for: 2m
      labels:
        severity: warning
      annotations:
        summary: "2.5G port {{ $labels.port_name }} on {{ $labels.name }} degraded"
        description: "Expected 2.5Gbps, negotiated {{ $value | humanize }}bps"

    # 1G ports degraded (explicit -1G suffix)
    - alert: UnifiLinkSpeedDegraded1G
      expr: unifi_port_speed_bps{port_name=~".+-1G"} < 1000000000
      for: 2m
      labels:
        severity: warning
      annotations:
        summary: "1G port {{ $labels.port_name }} on {{ $labels.name }} degraded"
        description: "Expected 1Gbps, negotiated {{ $value | humanize }}bps"

3.3 Update VMRules Kustomization

Add to kubernetes/apps/observability/vmrules/kustomization.yaml:

- ./unifi-alerts.yaml

4. Port Naming Convention

4.1 Format

<description>-<expected-speed>

4.2 Speed Suffixes

Suffix Expected Link Speed Alert Threshold
-10G 10 Gbps < 10,000,000,000 bps
-2.5G 2.5 Gbps < 2,500,000,000 bps
-1G 1 Gbps < 1,000,000,000 bps
-100M 100 Mbps No alert (intentionally slow)

4.3 Examples

  • NAS-10G - NAS on 10GbE SFP+
  • Proxmox-2.5G - Server on 2.5GbE
  • OfficePC-1G - Workstation on gigabit
  • SmartPlug-100M - IoT device, intentionally slow

4.4 Behavior

  • Unnamed ports with active links trigger UnifiPortMissingSpeedLabel
  • -100M ports excluded from all degradation alerts
  • Adding new devices: just name the port in UniFi UI

5. Scraping Configuration

No changes required. The unpoller Helm chart creates a PodMonitor which the VictoriaMetrics operator auto-converts to VMPodScrape. The observability namespace is already in the allowed list for podScrapeNamespaceSelector.


6. Verification Steps (Post-Deploy)

  1. Check unpoller pod is running: kubectl get pods -n observability -l app.kubernetes.io/name=unpoller
  2. Verify metrics are being scraped: curl -s http://unpoller.observability:9130/metrics | grep unifi_port_speed_bps
  3. Query VictoriaMetrics: unifi_port_speed_bps should return data for all switch ports
  4. Check alerts in vmalert UI: https://vmalert.${SECRET_DOMAIN}

Files Summary

Action File
Create kubernetes/apps/observability/unpoller/ks.yaml
Create kubernetes/apps/observability/unpoller/kustomization.yaml
Create kubernetes/apps/observability/unpoller/helmrelease.yaml
Create kubernetes/apps/observability/unpoller/externalsecret.yaml
Create kubernetes/apps/observability/vmrules/unifi-alerts.yaml
Edit kubernetes/apps/observability/kustomization.yaml (add unpoller)
Edit kubernetes/apps/observability/vmrules/kustomization.yaml (add unifi-alerts)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment