Services
banshee/services-collection/services/haproxy/https/
stats
The state, sessions and errors of each frontend, backend and server of an HAProxy, from its CSV statistics page.
by banshee1 installation1 version
Published on October 27, 2026. First version.
// An HAProxy load balancer, by its statistics page in CSV (the stats URI with ";csv" added) over HTTP: for
// each frontend, backend and server, its state, its current sessions against their limit, and its errors per
// second. An instance is named <proxy>/<name>: "www/web1", "www/BACKEND", "fe-https/FRONTEND".
need('protocol:http');
$user = need('protocol:http:user', 'optional');
$password = need('protocol:http:password', 'optional');
$url = variable('haproxy:url', 'text', array(
'label' => 'Statistics page address',
'help' => 'The address of the statistics page, http:// or https://, for instance https://lb.example:8404/stats. The collector adds ;csv.',
));
per('proxy', 'Frontends, backends and servers');
output('status', 'text', array('label' => 'State', 'per' => 'proxy'));
output('sessions', 'number', array('label' => 'Current sessions', 'per' => 'proxy'));
output('sessions-percent', 'number', array('label' => 'Sessions against their limit', 'unit' => '%', 'per' => 'proxy'));
output('errors', 'number', array('label' => 'Errors', 'unit' => 'errors/s', 'per' => 'proxy'));
watch.state('down', 'Down', 'status', array('critical' => array('DOWN')));
watch.state('maintenance', 'In maintenance or draining', 'status', array('warning' => array('MAINT', 'DRAIN', 'NOLB'), 'enabled' => false));
watch.above('saturation', 'Sessions near their limit', 'sessions-percent', array('warning' => 80, 'critical' => 95));
watch.above('errors', 'Errors', 'errors', array('warning' => 1, 'for' => '3 runs', 'enabled' => false));
watch.missing('missing', 'Frontend, backend or server missing', 'status', array('enabled' => false));
// The CSV form of the page, unless the address already asks for it.
$address = $url;
if (!text.contains($url, ';csv')) {
$address = $url . ';csv';
}
// A load balancer that refuses the connection, or does not answer in time, is down: critical.
$answer = http.get($address, user=$user, password=$password, errors=$err);
if ($err['code'] == 'connection_refused' || $err['code'] == 'timeout') {
fail('critical', 'HAProxy down', $err['text']);
}
if ($err['code'] != '') {
fail('unreachable', 'HAProxy unreachable', $err['text']);
}
if ($answer['status'] == 401 || $answer['status'] == 403 || $answer['status'] == 404) {
fail('misconfigured', 'HAProxy refuses the statistics page, code ' . $answer['status'],
'See the stats section and its account in the documentation.');
}
if ($answer['status'] != 200) {
fail('unreachable', 'HAProxy answers the statistics page with code ' . $answer['status']);
}
// The first line names the columns: "# pxname,svname,qcur,qmax,scur,smax,slim,stot,...,status,...,type,...".
// Each column is found by its name, so that the columns HAProxy adds from one version to the next do not count.
$lines = text.lines($answer['body']);
if (list.count($lines) == 0 || list.count(text.match($lines[0], '^# *pxname,')) == 0) {
fail('misconfigured', 'The address does not serve the CSV statistics of HAProxy', $address);
}
$header = text.match($lines[0], '^# *(.*)$');
$column = array();
$i = 0;
foreach (text.split($header[1], ',') as $name) {
$column[$name] = $i;
$i = $i + 1;
}
foreach (array('pxname', 'svname', 'scur', 'slim', 'ereq', 'econ', 'eresp', 'status', 'type') as $name) {
if (!list.has($column, $name)) {
fail('misconfigured', 'The statistics of HAProxy have no column ' . $name);
}
}
// One line per frontend (type 0), backend (1), server (2) or listening socket (3, left out). An empty field
// is a figure that does not apply: the limit of a server without maxconn, the request errors of a server.
for ($n = 1; $n < list.count($lines); $n = $n + 1) {
$f = text.split($lines[$n], ',');
if (list.count($f) <= $column['type'] || $f[$column['type']] == '3') {
continue;
}
$name = $f[$column['pxname']] . '/' . $f[$column['svname']];
// "UP", "DOWN", "OPEN", "MAINT", "DRAIN", "NOLB", "no check", and during a change "UP 1/3" or "DOWN 1/2":
// the first word, in capitals.
$words = text.words($f[$column['status']]);
$status = 'NO CHECK';
if (list.count($words) > 0 && $words[0] != 'no') {
$status = $words[0];
}
value('status', $status, $name);
$sessions = $f[$column['scur']];
if (text.is_number($sessions)) {
value('sessions', $sessions, $name);
$limit = $f[$column['slim']];
if (text.is_number($limit) && $limit + 0 > 0) {
value('sessions-percent', number.round(100 * $sessions / $limit, 1), $name);
}
}
$errors = 0;
foreach (array('ereq', 'econ', 'eresp') as $kind) {
if (text.is_number($f[$column[$kind]])) {
$errors = $errors + $f[$column[$kind]];
}
}
$rate = counter.rate('errors', $errors, $name);
if ($rate != '') {
value('errors', number.round($rate, 3), $name);
}
}