Browse Source

expose metrics in Prometheus format at /metrics endpoint

* metrics can be enabled by setting `METRICS_ENABLED` env to `true`
* HTTP Basic authorization is supported
   - username is set using METRICS_USER env variable
   - password is set using METRICS_PASSWORD env variable
pull/82/head
Vojta Drbohlav 5 years ago
parent
commit
016c9b9dd8
  1. 8
      .vscode/settings.json
  2. 38
      README.md
  3. 7
      docker-compose.dev.yml
  4. 11
      docker-compose.yml
  5. 3
      src/config.js
  6. 71
      src/lib/Metrics.js
  7. 34
      src/lib/Server.js
  8. 1
      src/lib/WireGuard.js
  9. 31
      src/package-lock.json
  10. 4
      src/package.json
  11. 5
      src/services/Metrics.js

8
.vscode/settings.json

@ -0,0 +1,8 @@
{
"[markdown]": {
"editor.formatOnSave": false
},
"[yaml]": {
"editor.formatOnSave": false
}
}

38
README.md

@ -22,6 +22,7 @@ You have found the easiest way to install & manage WireGuard on any Linux host!
* Statistics for which clients are connected.
* Tx/Rx charts for each connected client.
* Gravatar support.
* Metrics in Prometheus format.
## Requirements
@ -87,6 +88,9 @@ These options can be configured by setting environment variables using `-e KEY="
| `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. |
| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. |
| `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. |
| `METRICS_ENABLED` | `false` | `true` | When set, metrics in Prometheus format will be exposed. |
| `METRICS_USER` | - | `prometheus` | When set, HTTP Basic authorization with this user will be required when accessing metrics. |
| `METRICS_PASSWORD` | - | `password` | When set, HTTP Basic authorization will with this password be required when accessing metrics. |
> If you change `WG_PORT`, make sure to also change the exposed port.
@ -100,4 +104,36 @@ docker rm wg-easy
docker pull weejewel/wg-easy
```
And then run the `docker run -d \ ...` command above again.
And then run the `docker run -d \ ...` command above again.
# Exposed metrics
When metrics are enabled `wg-easy` will expose metrics in Prometheus format under `/metrics` path. HTTP Basic autorization is supported for metrics endpoint.
Node process metrics specific to `wg-easy` are exported with `wg_easy_` prefix. WireGuard metrics are exported with `wireguard_` prefix.
WireGuard metrics are inspired and compatible with metrics collected by [prometheus_wireguard_exporter](https://github.com/MindFlavor/prometheus_wireguard_exporter). Grafana dashboards created for [prometheus_wireguard_exporter](https://github.com/MindFlavor/prometheus_wireguard_exporter) works with metrics exposed by `wg-easy`.
## Example WireGuard metrics
```
# HELP wireguard_sent_bytes_total Bytes sent to the peer
# TYPE wireguard_sent_bytes_total counter
wireguard_sent_bytes_total{interface="wg0",public_key="QpPNe62/SuCUSEkBTu3r2U0ihe2UrDspxUUgk195zmc=",allowed_ips="10.112.112.2/32",friendly_name="Test User 1",enabled="true"} 0
wireguard_sent_bytes_total{interface="wg0",public_key="2AyHc7bRYJUJdx9UG87QmZDolj8xh6CORgP0PA28JT4=",allowed_ips="10.112.112.3/32",friendly_name="Test User 2",enabled="true"} 95788240
# HELP wireguard_received_bytes_total Bytes received from the peer
# TYPE wireguard_received_bytes_total counter
wireguard_received_bytes_total{interface="wg0",public_key="QpPNe62/SuCUSEkBTu3r2U0ihe2UrDspxUUgk195zmc=",allowed_ips="10.112.112.2/32",friendly_name="Test User 1",enabled="true"} 0
wireguard_received_bytes_total{interface="wg0",public_key="2AyHc7bRYJUJdx9UG87QmZDolj8xh6CORgP0PA28JT4=",allowed_ips="10.112.112.3/32",friendly_name="Test User 2",enabled="true"} 54389700
# HELP wireguard_latest_handshake_seconds Seconds from the last handshake
# TYPE wireguard_latest_handshake_seconds gauge
wireguard_latest_handshake_seconds{interface="wg0",public_key="QpPNe62/SuCUSEkBTu3r2U0ihe2UrDspxUUgk195zmc=",allowed_ips="10.112.112.2/32",friendly_name="Test User 1",enabled="true"} 0
wireguard_latest_handshake_seconds{interface="wg0",public_key="2AyHc7bRYJUJdx9UG87QmZDolj8xh6CORgP0PA28JT4=",allowed_ips="10.112.112.3/32",friendly_name="Test User 2",enabled="true"} 1633967910
# HELP wireguard_persistent_keepalive_seconds Seconds between each persistent keepalive packet
# TYPE wireguard_persistent_keepalive_seconds gauge
wireguard_persistent_keepalive_seconds{interface="wg0",public_key="QpPNe62/SuCUSEkBTu3r2U0ihe2UrDspxUUgk195zmc=",allowed_ips="10.112.112.2/32",friendly_name="Test User 1",enabled="true"} 0
wireguard_persistent_keepalive_seconds{interface="wg0",public_key="2AyHc7bRYJUJdx9UG87QmZDolj8xh6CORgP0PA28JT4=",allowed_ips="10.112.112.3/32",friendly_name="Test User 2",enabled="true"} 0
```

7
docker-compose.dev.yml

@ -1,9 +1,12 @@
version: "3.8"
version: '3.8'
services:
wg-easy:
image: wg-easy
command: npm run serve
volumes:
- ./src/:/app/
# environment:
# environment:
# - PASSWORD=p
# - METRICS_ENABLED=true
# - METRICS_USER=u
# - METRICS_PASSWORD=p

11
docker-compose.yml

@ -1,4 +1,4 @@
version: "3.8"
version: '3.8'
services:
wg-easy:
environment:
@ -12,14 +12,17 @@ services:
# - WG_DEFAULT_ADDRESS=10.8.0.x
# - WG_DEFAULT_DNS=1.1.1.1
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
# - METRICS_ENABLED=true
# - METRICS_USER=prometheus
# - METRICS_PASSWORD=password
image: weejewel/wg-easy
container_name: wg-easy
volumes:
- .:/etc/wireguard
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
- '51820:51820/udp'
- '51821:51821/tcp'
restart: unless-stopped
cap_add:
- NET_ADMIN

3
src/config.js

@ -14,3 +14,6 @@ module.exports.WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string'
? process.env.WG_DEFAULT_DNS
: '1.1.1.1';
module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0';
module.exports.METRICS_ENABLED = process.env.METRICS_ENABLED === 'true' || false;
module.exports.METRICS_USER = process.env.METRICS_USER;
module.exports.METRICS_PASSWORD = process.env.METRICS_PASSWORD;

71
src/lib/Metrics.js

@ -0,0 +1,71 @@
'use strict';
const client = require('prom-client');
const { collectDefaultMetrics } = client;
collectDefaultMetrics({ prefix: 'wg_easy_' });
const sentBytesTotal = new client.Counter({
name: 'wireguard_sent_bytes_total',
help: 'Bytes sent to the peer',
labelNames: ['interface', 'public_key', 'allowed_ips', 'friendly_name', 'enabled'],
});
const reveivedBytesTotal = new client.Counter({
name: 'wireguard_received_bytes_total',
help: 'Bytes received from the peer',
labelNames: ['interface', 'public_key', 'allowed_ips', 'friendly_name', 'enabled'],
});
const latestHandshakeSeconds = new client.Gauge({
name: 'wireguard_latest_handshake_seconds',
help: 'Seconds from the last handshake',
labelNames: ['interface', 'public_key', 'allowed_ips', 'friendly_name', 'enabled'],
});
const persistentKeepaliveSeconds = new client.Gauge({
name: 'wireguard_persistent_keepalive_seconds',
help: 'Seconds between each persistent keepalive packet',
labelNames: ['interface', 'public_key', 'allowed_ips', 'friendly_name', 'enabled'],
});
module.exports = class Metrics {
async getMetrics(wgClients) {
if (!wgClients) {
return client.register.metrics();
}
for (const wgClient of wgClients) {
const labels = {
interface: wgClient.interface,
public_key: wgClient.publicKey,
allowed_ips: wgClient.allowedIPs,
friendly_name: wgClient.name,
enabled: wgClient.enabled,
};
sentBytesTotal.remove(labels);
sentBytesTotal.labels(labels).inc(wgClient.transferTx || 0);
reveivedBytesTotal.remove(labels);
reveivedBytesTotal.labels(labels).inc(wgClient.transferRx || 0);
if (!wgClient.latestHandshakeAt) {
latestHandshakeSeconds.labels(labels).set(0);
} else {
const seconds = Math.round(Date.parse(wgClient.latestHandshakeAt) / 1000.0);
latestHandshakeSeconds.labels(labels).set(seconds);
}
if (!wgClient.persistentKeepalive || wgClient.persistentKeepalive === 'off') {
persistentKeepaliveSeconds.labels(labels).set(0);
} else {
persistentKeepaliveSeconds.labels(labels).set(wgClient.persistentKeepalive);
}
}
return client.register.metrics();
}
};

34
src/lib/Server.js

@ -9,11 +9,15 @@ const debug = require('debug')('Server');
const Util = require('./Util');
const ServerError = require('./ServerError');
const WireGuard = require('../services/WireGuard');
const Metrics = require('../services/Metrics');
const {
PORT,
RELEASE,
PASSWORD,
METRICS_ENABLED,
METRICS_USER,
METRICS_PASSWORD,
} = require('../config');
module.exports = class Server {
@ -23,6 +27,36 @@ module.exports = class Server {
this.app = express()
.disable('etag')
.use('/', express.static(path.join(__dirname, '..', 'www')))
// Metrics
.get('/metrics', (Util.promisify(async (req, res) => {
if (!METRICS_ENABLED) {
throw new ServerError('Metrics Disabled', 400);
}
res.set('WWW-Authenticate', 'Basic realm="WireGuard Metrics"');
if (METRICS_USER || METRICS_PASSWORD) {
if (!req.headers['authorization']) {
throw new ServerError('Unauthorized', 401);
}
const [authScheme, authCredentials] = req.headers['authorization'].split(' ');
if (authScheme !== 'Basic' || !authCredentials) {
throw new ServerError('Unauthorized', 401);
}
const credentials = Buffer.from(`${METRICS_USER || ''}:${METRICS_PASSWORD || ''}`).toString('base64');
if (authCredentials !== credentials) {
throw new ServerError('Unauthorized', 401);
}
}
const metrics = await Metrics.getMetrics(await WireGuard.getClients());
res.header('Content-Type', 'text/plain');
res.send(metrics);
})))
.use(express.json())
.use(expressSession({
secret: String(Math.random()),

1
src/lib/WireGuard.js

@ -114,6 +114,7 @@ AllowedIPs = ${client.address}/32`;
const config = await this.getConfig();
const clients = Object.entries(config.clients).map(([clientId, client]) => ({
id: clientId,
interface: 'wg0',
name: client.name,
enabled: client.enabled,
address: client.address,

31
src/package-lock.json

@ -342,6 +342,16 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
},
"bintrees": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
"integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
},
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@ -1955,6 +1965,14 @@
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
"prom-client": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.0.0.tgz",
"integrity": "sha512-etPa4SMO4j6qTn2uaSZy7+uahGK0kXUZwO7WhoDpTf3yZ837I3jqUDYmG6N0caxuU6cyqrg0xmOxh+yneczvyA==",
"requires": {
"tdigest": "^0.1.1"
}
},
"proxy-addr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
@ -2362,6 +2380,19 @@
}
}
},
"tdigest": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz",
"integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=",
"requires": {
"bintrees": "1.0.1"
}
},
"term-size": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
"integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="
},
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",

4
src/package.json

@ -15,6 +15,8 @@
"debug": "^4.3.1",
"express": "^4.17.1",
"express-session": "^1.17.1",
"nodemon": "^2.0.12",
"prom-client": "^14.0.0",
"qrcode": "^1.4.4",
"uuid": "^8.3.2"
},
@ -30,4 +32,4 @@
"engines": {
"node": "14"
}
}
}

5
src/services/Metrics.js

@ -0,0 +1,5 @@
'use strict';
const Metrics = require('../lib/Metrics');
module.exports = new Metrics();
Loading…
Cancel
Save