Next.js+React web interface for controlling HAProxy clusters (groups of servers), in conjunction with with https://gitgud.io/fatchan/haproxy-protection.
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
158 lines
4.5 KiB
158 lines
4.5 KiB
'use strict';
|
|
|
|
process
|
|
.on('uncaughtException', console.error)
|
|
.on('unhandledRejection', console.error);
|
|
|
|
import dotenv from 'dotenv';
|
|
dotenv.config({ path: '.env' });
|
|
|
|
import Queue from 'bull';
|
|
const haproxyStatsQueue = new Queue('stats', { redis: {
|
|
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
port: process.env.REDIS_PORT || 6379,
|
|
password: process.env.REDIS_PASS || '',
|
|
db: 1,
|
|
}});
|
|
|
|
if (!process.env.INFLUX_HOST) {
|
|
console.error('INFLUX_HOST not set, statistics will not be recorded');
|
|
process.exit(1);
|
|
}
|
|
|
|
import { InfluxDB, Point } from '@influxdata/influxdb-client';
|
|
import agent from '../agent.js';
|
|
|
|
import fetch, { AbortError } from 'node-fetch';
|
|
const writeApi = new InfluxDB({ url: process.env.INFLUX_HOST, token: (process.env.INFLUX_TOKEN || null) }).getWriteApi('proxmox', 'proxmoxdb')
|
|
, base64Auth = Buffer.from(`${process.env.DATAPLANE_USER}:${process.env.DATAPLANE_PASS}`).toString('base64');
|
|
|
|
async function fetchStats(host, parameters) {
|
|
const controller = new AbortController();
|
|
const signal = controller.signal;
|
|
setTimeout(() => {
|
|
controller.abort();
|
|
}, 10000);
|
|
const clusterUrl = new URL(host);
|
|
const statsRes = await fetch(`https://${clusterUrl.host}/v3/services/haproxy/stats/native?${new URLSearchParams(parameters).toString()}`, {
|
|
agent,
|
|
headers: { 'authorization': `Basic ${base64Auth}` },
|
|
signal,
|
|
})
|
|
.then(res => res.json())
|
|
.catch(err => console.error(err));
|
|
return statsRes;
|
|
|
|
};
|
|
|
|
async function getFormattedStats(host) {
|
|
const [serverStats, frontendStats] = await Promise.all([
|
|
fetchStats(host, { type: 'server', parent: 'servers' }),
|
|
fetchStats(host, { type: 'frontend', name: 'www-http-https' })
|
|
]);
|
|
|
|
frontendStats[0].stats = frontendStats[0].stats
|
|
.filter(t => t.name === 'www-http-https')
|
|
.map(t => ({
|
|
'name': t.name,
|
|
'stats': {
|
|
'Bytes in': t.stats.bin,
|
|
'Bytes out': t.stats.bout,
|
|
'Conn rate': t.stats.conn_rate,
|
|
'Cr (max)': t.stats.conn_rate_max,
|
|
'Request rate': t.stats.req_rate,
|
|
'Rr (max)': t.stats.req_rate_max,
|
|
'1xx': t.stats.hrsp_1xx,
|
|
'2xx': t.stats.hrsp_2xx,
|
|
'3xx': t.stats.hrsp_3xx,
|
|
'4xx': t.stats.hrsp_4xx,
|
|
'5xx': t.stats.hrsp_5xx,
|
|
'Total': t.stats.req_tot,
|
|
}
|
|
}));
|
|
|
|
serverStats.forEach(server => {
|
|
server.stats = server.stats
|
|
.filter(t => t.backend_name === 'servers')
|
|
.map(t => ({
|
|
'name': t.name,
|
|
'backend_name': t.backend_name,
|
|
'stats': {
|
|
'Address': t.stats.addr,
|
|
'Bytes in': t.stats.bin,
|
|
'Bytes out': t.stats.bout,
|
|
'Sess rate': t.stats.rate,
|
|
'Sr (max)': t.stats.rate_max,
|
|
'Queue': t.stats.qcur,
|
|
'Q (max)': t.stats.qmax,
|
|
'Q (time)': t.stats.qtime,
|
|
'1xx': t.stats.hrsp_1xx,
|
|
'2xx': t.stats.hrsp_2xx,
|
|
'3xx': t.stats.hrsp_3xx,
|
|
'4xx': t.stats.hrsp_4xx,
|
|
'5xx': t.stats.hrsp_5xx,
|
|
'Total': t.stats.req_tot,
|
|
}
|
|
}));
|
|
});
|
|
|
|
return {
|
|
frontendStats,
|
|
serverStats,
|
|
};
|
|
};
|
|
|
|
async function processHost(host) {
|
|
try {
|
|
const hostname = new URL(host).hostname;
|
|
console.time(`Fetched stats from ${hostname}`);
|
|
const { frontendStats, serverStats } = await getFormattedStats(host);
|
|
console.timeEnd(`Fetched stats from ${hostname}`);
|
|
let points = [];
|
|
const now = new Date();
|
|
frontendStats.forEach((s, i) => {
|
|
const statPoints = Object.entries(s.stats[0].stats)
|
|
.map(e => {
|
|
return new Point(e[0])
|
|
.tag('type', 'frontend')
|
|
.tag('hostname', hostname)
|
|
.floatField('value', e[1])
|
|
.timestamp(now);
|
|
});
|
|
points = points.concat(statPoints);
|
|
});
|
|
serverStats.forEach(server => {
|
|
server.stats.forEach(ss => {
|
|
const statPoints = Object.entries(ss.stats)
|
|
.map(e => {
|
|
return new Point(e[0])
|
|
.tag('type', 'backend')
|
|
.tag('hostname', hostname)
|
|
.tag('server_name', ss.name)
|
|
.tag('server_address', ss.stats['Address'])
|
|
.floatField('value', e[1])
|
|
.timestamp(now);
|
|
});
|
|
points = points.concat(statPoints);
|
|
});
|
|
});
|
|
console.time(`Flushed ${points.length} points for ${hostname} to influx`);
|
|
await writeApi.writePoints(points);
|
|
await writeApi.flush();
|
|
console.timeEnd(`Flushed ${points.length} points for ${hostname} to influx`);
|
|
} catch (e) {
|
|
if (e && e.cause && e.cause.code && e.cause.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
|
|
console.error('Error writing stats', new URL(host).hostname, e.cause.code);
|
|
} else {
|
|
console.error('Error writing stats:', e);
|
|
}
|
|
}
|
|
};
|
|
|
|
async function handleJob(job, done) {
|
|
const { hosts } = job.data;
|
|
hosts.forEach(processHost);
|
|
done();
|
|
}
|
|
|
|
haproxyStatsQueue.process(handleJob);
|
|
|