Unifi prometheus exporter

Hello! The other day I wanted to make graphs for all our access points, we have a lot of them, some are based on Mikrotik and there are no problems with them, it is easily polled via SNMP and gives statistics at once for all points, but with Unifi everything is more difficult, you need to poll each point access separately, and they sometimes change with us, so we need some kind of solution that will track these changes automatically. There was nothing ready-made on the Internet, so we decided to write another “bicycle”, but I wanted to make it as compatible as possible so that others could use it.

Our approach

We decided that we would write an application / daemon that can log in to the Unifi controller, get a list of access points, and then, upon request to the /metrics handle, it will access all access points via snmp and give the result out in the prometheus format

Implementation

I will not dwell on the details of the implementation of the application, I filled everything on github and documented in detail. There are also docker image.

I will describe some of the approaches that have been used:

  • development language golang

  • framework used to process cli-arguments and environment variables urfave/cli/v2

  • used as an http request router gorilla/mux

  • to limit the simultaneous polling of access points, the synchronization primitive “semaphore

  • used mutex to synchronize the list of access points between goroutines

  • to poll access points, the application refers to a third-party implementation snmp-exporter

Environment setup

We need to set up two applications:

  • snmp-exporter – the daemon that we will access from our application

  • unifi-prometheus-exporter – our app

I’ll say right away that I honestly dug into the snmp-exporter code, hoping that it would be possible to import the code and use its functions natively, but everything is not very good there, the code is not modular, it cannot be imported, so you have to use two applications.

snmp-exporter

First you need to run snmp-exporter, which we will use to poll our access points, I will show how this can be run in docker-compose and kubernetes. I will not consider installing docker-compose and kubernetes here, this is not the purpose of this post.

The exporter listens on port 9116, in order to poll the remote host via snmp, it is enough to send an http request to the exporter in the /snmp handle with the module (if_mib by default) and target (what we are polling) parameters, for example:

curl http://127.0.0.1:9116/snmp?target=10.0.0.1

docker-compose

docker-compose.yaml

version: "2"

services:
  nexus:
    image: prom/snmp-exporter
    ports:
      - "9116:9116"

kubernetes

snmp-exporter.yaml

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: snmp-exporter
  labels:
    app: snmp-exporter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: snmp-exporter
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: snmp-exporter
    spec:
      containers:
      - image: prom/snmp-exporter
        imagePullPolicy: IfNotPresent
        name: exporter
        ports:
        - containerPort: 9116
---
apiVersion: v1
kind: Service
metadata:
  name: snmp-exporter
spec:
  ports:
  - port: 9116
    protocol: TCP
    targetPort: 9116
    name: snmp-exporter
  selector:
    app: snmp-exporter

unifi-prometheus-exporter

now we launch our application, it has a number of cli-arguments duplicated by environment variables:

NAME:
   exporter - экспортер snmp-метрик от точек доступа unifi

USAGE:
   exporter [global options] command [command options] [arguments...]

COMMANDS:
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --controller-login value               логин от exporter-контроллера [$CONTROLLER_LOGIN]
   --controller-password value            пароль от exporter-контроллера [$CONTROLLER_PASSWORD]
   --controller-address value             адрес exporter-контроллера (default: "https://127.0.0.1:8443") [$CONTROLLER_ADDRESS]
   --snmp-exporter-address value          адрес snmp-экспортера (default: "http://snmp-exporter:9116") [$SNMP_EXPORTER_ADDRESS]
   --access-points-update-interval value  интервал обновления списка точек (default: 1h0m0s) [$ACCESS_POINTS_UPDATE_INTERVAL]
   --listen-port value                    порт прослушки http-сервера (default: 8080) [$LISTEN_PORT]
   --parallel value                       количество потоков для опроса точек-доступа (default: 10) [$PARALLEL]
   --poll-timeout value                   таймаут для опроса точек доступа (default: 15s) [$POLL_TIMEOUT]
   --help, -h                             show help (default: false)

in principle, here, in my opinion, everything is clear. run in docker-compose/kubernetes

docker-compose

version: "2"

services:
  nexus:
    image: maetx777/unifi-prometheus-exporter
    environment:
    - CONTROLLLER_LOGIN=admin
    - CONTROLLER_PASSWORD=123456
    ports:
      - "9116:9116"

kubernetes

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: unifi-snmp-exporter
  labels:
    app: unifi-snmp-exporter
spec:
  replicas: 1
  selector:
    matchLabels:
      app: unifi-snmp-exporter
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: unifi-snmp-exporter
      annotations:
        prometheus.io/scrape: 'true'
        prometheus.io/port: '8080'
        prometheus.io/path: '/metrics'
    spec:
      containers:
      - image: maetx777/unifi-prometheus-exporter
        name: exporter
        ports:
        - containerPort: 8080
        env:
        - name: CONTROLLER_LOGIN
          value: admin
        - name: CONTROLLER_PASSWORD
          value: 123456
      restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
  name: unifi-prometheus-exporter
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 8080
    name: unifi-prometheus-exporter
  selector:
    app: unifi-prometheus-exporter      

focus on the nuances

  • CONTROLLER_LOGIN – change if necessary

  • CONTROLLER_PASSWORD – if necessary, change 🙂

  • CONTROLLER_ADDRESS – change to the address of the unifi controller indicating the protocol and port, for example https://1.2.3.4:8443

  • SNMP_EXPORTER_ADDRESS – change to the address of the snmp-exporter that was launched earlier, you can use the dns-name

  • transfer secrets in kubernetes in the specified way poorlythere is a special resource for this secretbut this is not considered here, each myself will finish it for himself

After launch

After launch, we will see something like this in the unifi-prometheus-controller logs

INFO[0000] Daemon start
INFO[0000] Start http server
INFO[0000] Start fatals catcher
INFO[0000] Start signals catcher
INFO[0000] Start access points updater
INFO[0001] Http client authorized
INFO[0001] Update access points list
INFO[0001] Access point name Room1, ip 10.0.0.10
INFO[0001] Access point name Room2, ip 10.0.0.20

now we can refer to our controller to get the metrics of the found points

# curl -s http://127.0.0.1:8080/metrics|grep ifOutOctets
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="ath0",ifIndex="6",ifName="ath0"} 0
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="ath1",ifIndex="7",ifName="ath1"} 3.319545249e+09
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="br0",ifIndex="9",ifName="br0"} 2.7572029e+07
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="eth0",ifIndex="2",ifName="eth0"} 4.93001573e+08
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="eth1",ifIndex="3",ifName="eth1"} 0
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="lo",ifIndex="1",ifName="lo"} 3572
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="teql0",ifIndex="5",ifName="teql0"} 0
ifOutOctets{ap_name="Room1",ap_ip="10.0.0.10",ifAlias="",ifDescr="vwire2",ifIndex="8",ifName="vwire2"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="ath0",ifIndex="6",ifName="ath0"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="ath1",ifIndex="7",ifName="ath1"} 6.28150693e+08
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="br0",ifIndex="9",ifName="br0"} 2.7178302e+07
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="eth0",ifIndex="2",ifName="eth0"} 4.95262026e+08
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="eth1",ifIndex="3",ifName="eth1"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="lo",ifIndex="1",ifName="lo"} 8180
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="teql0",ifIndex="5",ifName="teql0"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="vwire2",ifIndex="8",ifName="vwire2"} 0
ifOutOctets{ap_name="Room2",ap_ip="10.0.0.20",ifAlias="",ifDescr="wifi0",ifIndex="4",ifName="wifi0"} 0

the request takes some time, the more points – the longer the request will work, but the polling occurs asynchronously in multithreaded mode, so it usually fits within an acceptable time (we have 10 points polled in 10 seconds)

metrics are also “enriched” with tags with the name (ap_name) and ip-address (ap_ip) of polled points

Prometheus

I won’t write here how to install prometheus, this is not the purpose of this article, this business works for us based on kubernetes_sd_configin the presented kubernetes config, an annotation is specified that tells the prometheus system the port and handle for polling

Grafana

Finally, I’ll show you a simple dashboard grafana to view graphs

After creating the dashboard, we immediately go to Dashboard settings, create a variable

Query: ifOutOctets{ap_name=~".+"}
Regex: /ap_name="([^"]+)"/

the meaning of this variable is that we select all unique values ​​​​of ap_name so that it can be further selected from the list:

now create a graph and write formulas there

A: irate(ifOutOctets{ap_name=~"[[ap_name]]"}[5m])*8
A.Legend: {{ap_name}} {{ap_ip}} {{ifDescr}} out

B: irate(ifInOctets{ap_name=~"[[ap_name]]"}[5m])*-8
B.Legend: {{ap_name}} {{ap_ip}} {{ifDescr}} in

multiplying by 8 is necessary because ifOutOctets is a value in bytes, and we need a graph in megabits / sec

multiply by -8 so that the incoming traffic is displayed down, yes, I know that there are transformations, but it worked somehow crookedly for us, the graph disappeared completely, and we didn’t understand what was wrong

and at the output we get the opportunity to view the chart for any of the access points:

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *