banshee is under active development: collectors will keep changing, and a version you publish will likely need adjustments. For stable use, wait for version 1.0.0.

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.

  • services
  • haproxy
  • load balancer
  • proxy
  • stats
  • csv
  • http
  • https

by banshee1 installation1 version

v1Latest

Published on October 27, 2026. First version.

Keen 1.0.0 · 103 lines
// 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);
    }
}