#!/usr/bin/perl

# check_haproxy Nagios Plugin
# Nagios Plugin to check the state of HAProxy.
#  (c) 2014-2015 Jonathan Wright <github@jon.than.io>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

# Configure the Perl Environment
use warnings;
use Carp;
use Switch;

# Import Modules
use Getopt::Long qw(:config no_ignore_case);
use Pod::Usage;
use IO::Socket::UNIX qw( SOCK_STREAM );
use Data::Dumper;

# Configure Program details
our ($_program, $_version, $_author);
$_program = 'check_haproxy';
$_version = '1.0.3';
$_author  = 'Jonathan Wright <github@jon.than.io>';

use constant {
  # Field names to locations for all the CSV data provided by HAProxy
  STAT_PROXY_NAME               => 0,
  STAT_SERVICE_NAME             => 1,
  STAT_QUEUED_REQUESTS          => 2,
  STAT_QUEUED_MAX               => 3,
  STAT_SESSIONS_CURRENT         => 4,
  STAT_SESSIONS_MAX             => 5,
  STAT_SESSIONS_LIMIT           => 6,
  STAT_CONNECTIONS_TOTAL        => 7,
  STAT_BYTES_IN                 => 8,
  STAT_BYTES_OUT                => 9,
  STAT_REQUESTS_DENIED          => 10,
  STAT_RESPONSES_DENIED         => 11,
  STAT_REQUESTS_ERROR           => 12,
  STAT_CONNECTIONS_ERROR        => 13,
  STAT_RESPONSES_ERROR          => 14,
  STAT_CONNECTIONS_RETRIED      => 15,
  STAT_CONNECTIONS_REDISPATCHED => 16,
  STAT_STATUS                   => 17,
  STAT_WEIGHT                   => 18,
  STAT_SERVER_ACTIVE            => 19, STAT_SERVERS_ACTIVE => 19,
  STAT_SERVER_BACKUP            => 20, STAT_SERVERS_BACKUP => 20,
  STAT_CHECKS_FAIL              => 21,
  STAT_CHECKS_GO_DOWN           => 22,
  STAT_CHECKS_LAST_CHANGE       => 23,
  STAT_CHECKS_DOWNTIME          => 24,
  STAT_QUEUED_LIMIT             => 25,
  STAT_PID                      => 26,
  STAT_UID                      => 27,
  STAT_SID                      => 28,
  STAT_THROTTLE                 => 29,
  STAT_SESSIONS_TOTAL           => 30,
  STAT_TRACKED                  => 31,
  STAT_SERVICE_TYPE             => 32,
  STAT_SESSIONS_RATE_CURRENT    => 33,
  STAT_SESSIONS_RATE_LIMIT      => 34,
  STAT_SESSIONS_RATE_MAX        => 35,
  STAT_CHECK_STATUS             => 36,
  STAT_CHECK_CODE               => 37,
  STAT_CHECK_DURATION           => 38,
  STAT_RESPONSES_HTTP_1XX       => 39,
  STAT_RESPONSES_HTTP_2XX       => 40,
  STAT_RESPONSES_HTTP_3XX       => 41,
  STAT_RESPONSES_HTTP_4XX       => 42,
  STAT_RESPONSES_HTTP_5XX       => 43,
  STAT_RESPONSES_HTTP_XXX       => 44,
  STAT_CHECK_FAILED_DETAILS     => 45,
  STAT_REQUESTS_RATE_CURRENT    => 46,
  STAT_REQUESTS_RATE_MAX        => 47,
  STAT_REQUESTS_TOTAL           => 48,
  STAT_ABORTS_CLIENT            => 49,
  STAT_ABORTS_SERVER            => 50,
  STAT_COMPRESSOR_IN            => 51,
  STAT_COMPRESSOR_OUT           => 52,
  STAT_COMPRESSOR_BYPASSED      => 53,
  STAT_COMPRESSOR_REQUESTS      => 54,
  STAT_SESSIONS_LAST            => 55,
  STAT_CHECK_HEALTH_LAST        => 56,
  STAT_CHECK_AGENT_LAST         => 57,
  STAT_TIME_QUEUE               => 58,
  STAT_TIME_CONNECT             => 59,
  STAT_TIME_RESPONSE            => 60,
  STAT_TIME_TOTAL               => 61,
  # Types used by HAProxy for some fields
  TYPE_FRONTEND                 => 0,
  TYPE_BACKEND                  => 1,
  TYPE_SERVER                   => 2,
  TYPE_LISTENER                 => 3,
};

use constant {
  LIMIT_BACKEND_CHECK           => 'u',
  LIMIT_WARN_BACKEND_UP         => 5,
  LIMIT_CRIT_BACKEND_UP         => 2,
  LIMIT_WARN_BACKEND_DOWN       => 2,
  LIMIT_CRIT_BACKEND_DOWN       => 5,
  LIMIT_WARN_LIMITS             => 0.75,  # Percentages
  LIMIT_CRIT_LIMITS             => 0.90,
};

use constant EXIT_STATUS => qw(OK WARNING CRITICAL UNKNOWN);

use constant {
  EXIT_OK                       => 0,
  EXIT_WARNING                  => 1,
  EXIT_CRITICAL                 => 2,
  EXIT_UNKNOWN                  => 3,
};

# Alias _ to sprintf
sub _ {return sprintf(shift, @_)}

# Create hash array for holding all configuration variables
our (%opt, %data, %checks);
%opt = (
  'check_frontends' => 1,
  'check_backends'  => 1,
  'check_servers'   => 1,
  'socket'          => '/var/lib/haproxy/stats',
  'debug'           => 0,
  'defaults'        => undef,
  'help'            => 0,
);

# Somewhere to store messages
our (@critical, @warning);

sub main {
  my (@overrides);

  GetOptions(
    'f'       => \$opt{'check_frontends'}, 'frontends!'       => \$opt{'check_frontends'},
    'b'       => \$opt{'check_backends'},  'backends!'        => \$opt{'check_backends'},
    's'       => \$opt{'check_servers'},   'servers!'         => \$opt{'check_servers'},
    'S=s'     => \$opt{'socket'},          'socket=s'         => \$opt{'socket'},
    'd'       => \$opt{'debug'},           'debug!'           => \$opt{'debug'},
    'O=s{1,}' => \@overrides,              'overrides=s{1,}'  => \@overrides,
    'D=s'     => \$opt{'defaults'},        'defaults=s'       => \$opt{'defaults'},
    '-h'      => \$opt{'help'},            '--help'           => \$opt{'help'},
  ) or pod2usage(
    -message => sprintf("%s %s by %s", $_program, $_version, $_author),
    -exitval => EXIT_UNKNOWN,
    -verbose => 0);

  pod2usage(
    -message => sprintf("%s %s by %s", $_program, $_version, $_author),
    -exitval => EXIT_UNKNOWN,
    -verbose => 1) if $opt{'help'};

  my $socket = IO::Socket::UNIX->new(
    Peer    => $opt{'socket'},
    Type    => SOCK_STREAM,
    Timeout => 1,
  ) or do_unknown("Cannot connect to socket ".$opt{'socket'}, 1);

  %data   = get_data($socket);
  %checks = build_checks(\@overrides);

  check_frontends() if $opt{'check_frontends'};
  check_backends()  if $opt{'check_backends'};
  check_servers()   if $opt{'check_servers'};

  my ($frontends, $backends, $servers, $services) = (0,0,0,0);
  my (%servers);

  foreach my $service (sort keys %data) {
    switch ($data{$service}{'type'}) {
      case TYPE_FRONTEND {
        ++$frontends;
      }
      case TYPE_BACKEND {
        ++$backends;
        foreach my $server (sort keys %{$data{$service}{'servers'}}) {
          next if $data{$service}{'servers'}{$server}{'backup'};
          ++$services;
          unless (exists $servers{$server}) {
            ++$servers;
            $servers{$server} = 1;
          }
        }
      }
    }
  }

  do_success(
    _('HAProxy is functioning within established parameters. '.
      '(%d frontends, %d backends, %d servers, %d services)',
        $frontends, $backends, $servers, $services)
  );
}

sub _debug {
  return unless $opt{'debug'};
  printf "%s{} %s\n", shift, shift;
}

sub _report {
  my ($status, $message) = (shift, shift);
  croak('No message given') unless $message;
  _debug('report', "with status ${status} and message '${message}'");

  printf "HAPROXY %s: %s (%s %s)\n", (EXIT_STATUS)[$status], $message, $_program, $_version;
  exit $status;
}

sub do_success {
  _report(EXIT_OK, shift);
}

sub do_warning {
  my ($message) = shift;
  push(@warning, $message);
  _report(EXIT_WARNING, $message);
}

sub do_critical {
  my ($message, $_croak) = (shift, shift);
  croak($message) if $opt{'debug'} and $_croak;
  push(@critical, $message);
  _report(EXIT_CRITICAL, $message);
}

sub do_unknown {
  my ($message) = shift;
  confess($message) if $opt{'debug'};
  _report(EXIT_UNKNOWN, $message);
}

sub check_pass {
  my ($state, $threshold, $value, $max) = (shift, shift, shift, shift);

  # Bypass any checks
  return 0 if $state eq 'x';

  # Sanity checking
  do_unknown(
    _('Negative check value for state %s: %d', $state, $value)
  ) if $value < 0;

  do_unknown(
    _('Negative check threshold for state %s: %d', $state, $threshold)
  ) if $threshold < 0;

  do_unknown(
    _('Negative check maximum for state %s: %d', $state, $max)
  ) if $max < 0;

  do_unknown(
    _('No max value provided for percentage threshold on state %d: %s',
      $state, $max || 'undef')
  ) if ($threshold < 1 and $threshold > 0)
    and (not $max or not $max =~ /^[0-9]+(\.[0-9]+)?$/);

  switch ($state) {
    case 'u' {
      if ($threshold < 1 and $threshold > 0) {
        _debug('check_pass', _("UP: %d >= (%.1f%% of %d)", $value, ($threshold*100), $max));
        return $value >= ($max * $threshold);
      } else {
        _debug('check_pass', _("UP: %d >= %d", $value, $threshold));
        return $value >= $threshold;
      }
    }
    case 'd' {
      if ($threshold < 1 and $threshold > 0) {
        _debug('check_pass', _("DOWN: %d >= (%.1f%% of %d)", $value, ($threshold*100), $max));
        return $value < ($max * $threshold);
      } else {
        _debug('check_pass', _("DOWN: %d < %d", $value, $threshold));
        return $value < $threshold;
      }
    }
    case 't' {
      _debug('check_pass', _("Threshold: %d < (%.1f%% of %d)", $value, ($threshold*100), $max));
      return $value < ($max * $threshold);
    }
    default {
      # More sanity checking
      do_unknown("Invalid state check: ${state}");
    }
  }
}

sub get_data {
  my ($s, %v); $s = shift;

  _debug('get_data', 'is reading data from HAProxy');

  print $s "show stat\n";
  while (<$s>) {
    chomp; next unless length;
    my @stat = split (',');

    switch ($stat[STAT_SERVICE_TYPE]) {
      # Process all FRONTEND type entries; these are singular and are totals
      # only for each of the frontends configured
      case TYPE_FRONTEND {
        $v{$stat[STAT_PROXY_NAME]} = {
          type        => TYPE_FRONTEND,
          connections => $stat[STAT_CONNECTIONS_TOTAL],
          sessions    => {
            current   => $stat[STAT_SESSIONS_CURRENT],
            limit     => ($stat[STAT_SESSIONS_LIMIT] ? $stat[STAT_SESSIONS_LIMIT] : 0),
          },
          status      => $stat[STAT_STATUS],
          queued      => 0,
        };
      }
      # Process all BACKEND type entries; these are the totals for each backend
      # and don't have the same amount of information as SERVERs
      case TYPE_BACKEND {
        # We can't 'set' the hash here as the backend totals are normally after
        # the backend's servers, so would override anything previously set
        # in TYPE_SERVER
        $v{$stat[STAT_PROXY_NAME]}{'type'} = TYPE_BACKEND;
        $v{$stat[STAT_PROXY_NAME]}{'connections'} = $stat[STAT_CONNECTIONS_TOTAL];
        $v{$stat[STAT_PROXY_NAME]}{'sessions'} = {
          current     => $stat[STAT_SESSIONS_CURRENT],
          limit       => ($stat[STAT_SESSIONS_LIMIT] ? $stat[STAT_SESSIONS_LIMIT] : 0),
        };
        $v{$stat[STAT_PROXY_NAME]}{'queued'} = $stat[STAT_QUEUED_REQUESTS];
        $v{$stat[STAT_PROXY_NAME]}{'active'} = $stat[STAT_SERVERS_ACTIVE];
        $v{$stat[STAT_PROXY_NAME]}{'backup'} = $stat[STAT_SERVERS_BACKUP];
      }
      # Process all SERVER type entries, which are the most details and give
      # information about how each server is responding
      case TYPE_SERVER {
        # Only set the server itself directly, otherwise we may override
        # anything previously set
        $v{$stat[STAT_PROXY_NAME]}{'servers'}{$stat[STAT_SERVICE_NAME]} = {
          active      => ($stat[STAT_SERVER_ACTIVE] ? 1 : 0),
          backup      => ($stat[STAT_SERVER_BACKUP] ? 1 : 0),
          up          => ($stat[STAT_STATUS] eq 'UP' ? 1 : 0),
          down        => ($stat[STAT_STATUS] eq 'DOWN' ? 1 : 0),
          disabled    => ($stat[STAT_STATUS] =~ /^(MAINT|DRAIN|NOLB)/i ? 1 : 0),
          connections => $stat[STAT_CONNECTIONS_TOTAL],
          sessions    => {
            current   => $stat[STAT_SESSIONS_CURRENT],
            limit     => ($stat[STAT_SESSIONS_LIMIT] ? $stat[STAT_SESSIONS_LIMIT] : 0),
          },
          queued      => {
            current   => $stat[STAT_QUEUED_REQUESTS],
            limit     => ($stat[STAT_QUEUED_LIMIT] ? $stat[STAT_QUEUED_LIMIT] : 0),
          },
          status      => $stat[STAT_STATUS],
          timing      => {
            queue     => $stat[STAT_TIME_QUEUE],
            connect   => $stat[STAT_TIME_CONNECT],
            response  => $stat[STAT_TIME_RESPONSE],
            total     => $stat[STAT_TIME_TOTAL]
          },
        };
      }
    }
  }

  return %v;
}

sub build_checks {
  my $overrides = shift;
  my @overrides = @{$overrides};

  my (%checks, $state, $be_warn, $be_crit, $limit_warn, $limit_crit);

  # Setup the defaults for all checks
  switch (LIMIT_BACKEND_CHECK) {
    case 'u' {
      $state      = 'u';
      $be_warn    = LIMIT_WARN_BACKEND_UP;
      $be_crit    = LIMIT_CRIT_BACKEND_UP;
    }
    default {
      $state      = 'd';
      $be_warn    = LIMIT_WARN_BACKEND_DOWN;
      $be_crit    = LIMIT_CRIT_BACKEND_DOWN;
    }
  }

  $limit_warn = LIMIT_WARN_LIMITS;
  $limit_crit = LIMIT_CRIT_LIMITS;

  # See if there is an override for the defaults, then use those
  if (defined $opt{'defaults'}
      and $opt{'defaults'} =~ /([ud]?),(?:((?:0?\.)?[0-9]*)(?:,((?:0?\.)?[0-9]*)(?:,((?:0?\.)?[0-9]*)%?(?:,((?:0?\.)?[0-9]*)%?)?)?)?)?/) {
    $state      =  $1       if $1;
    $be_warn    =  $2       if $2;
    $be_crit    =  $3       if $3;
    $limit_warn = ($4/100)  if $4;
    $limit_crit = ($5/100)  if $5;
    _debug('build_checks',
           _('setting defaults to %s,%.2f,%.2f,%.2f,%.2f',
             $state, $be_warn, $be_crit, $limit_warn, $limit_crit));
  }

  # Build the defaults for all checks
  foreach my $service (sort keys %data) {
    my $type;

    switch ($data{$service}{'type'}) {
      case TYPE_FRONTEND {
        $type = 'FRONTEND';
        %{$checks{$service}} = (
          limit_warn  => $limit_warn,
          limit_crit  => $limit_crit,
        );
      }
      case TYPE_BACKEND {
        $type = 'BACKEND';
        %{$checks{$service}} = (
          state       => $state,
          be_warn     => $be_warn,
          be_crit     => $be_crit,
          limit_warn  => $limit_warn,
          limit_crit  => $limit_crit,
        );
      }
    }

    _debug('build_checks', _('building %s check for %s', $type, $service));
  }

  # Merge the two arrays
  foreach my $override (sort @overrides) {
    my ($service, $state, $be_warn, $be_crit, $limit_warn, $limit_crit);

    _debug('build_checks', _('processing override %s', $override));

    next unless ($override =~ /([^:]+):([udx]?)(?:,([0-9]*)(?:,([0-9]*)(?:,([0-9]*)%?(?:,([0-9]*)%?)?)?)?)?/);

    next unless exists $data{$1};

    if ($data{$1}{'type'} eq TYPE_BACKEND) {
      $checks{$1}{'state'}    =  $2      if $2 and ($2 eq 'u' or $2 eq 'd' or $2 eq 'x');
      $checks{$1}{'be_warn'}  =  $3      if $3 and $3 >= 0;
      $checks{$1}{'be_crit'}  =  $4      if $4 and $4 >= 0;
    }

    $checks{$1}{'limit_warn'} = ($5/100) if $5 and $5 > 0 and $5 <= 100;
    $checks{$1}{'limit_crit'} = ($6/100) if $6 and $6 > 0 and $6 <= 100;

    _debug('build_checks',
           _('setting override for %s to %s,%d,%d,%.2f,%.2f',
             $1,
             $checks{$1}{'state'},
             $checks{$1}{'be_warn'},
             $checks{$1}{'be_crit'},
             $checks{$1}{'limit_warn'},
             $checks{$1}{'limit_crit'}));
  }

  return %checks;
}

sub check_frontends {
  _debug('check_frontends', 'is starting check on frontends');
  foreach my $service (sort keys %data) {
    next unless $data{$service}{'type'} eq TYPE_FRONTEND;
    _debug('check_frontends', _('checking FRONTEND %s', $service));

    _debug('check_frontends', 'running OPEN check');

    do_critical(_('FRONTEND %s is %s', $service, $data{$service}{'status'}))
      unless $data{$service}{'status'} eq 'OPEN';

    # Theses no supported Queueing on the Frontends, so no checks for that
    # need to be done here, just for Sessions.

    next unless $data{$service}{'sessions'}{'limit'} > 0;
    _debug('check_frontends', 'running Sessions check');
    _debug('check_frontends', _('found %d (limit %d); thresholds are %.1f%%, %.1f%%',
                                $data{$service}{'sessions'}{'current'},
                                $data{$service}{'sessions'}{'limit'},
                                ($checks{$service}{'limit_warn'}*100),
                                ($checks{$service}{'limit_crit'}*100)));

    do_critical(_('Sessions for FRONTEND %s has passed %.2f%% of Limit (%d active, %d limit)',
                  $service, ($checks{$service}{'limit_crit'}*100),
                  $data{$service}{'sessions'}{'current'},
                  $data{$service}{'sessions'}{'limit'}))
      unless check_pass('t', $checks{$service}{'limit_crit'},
                             $data{$service}{'sessions'}{'current'},
                             $data{$service}{'sessions'}{'limit'});

    do_warning(_('Sessions for FRONTEND %s has passed %.2f%% of Limit (%d active, %d limit)',
                  $service, ($checks{$service}{'limit_warn'}*100),
                  $data{$service}{'sessions'}{'current'},
                  $data{$service}{'sessions'}{'limit'}))
      unless check_pass('t', $checks{$service}{'limit_warn'},
                             $data{$service}{'sessions'}{'current'},
                             $data{$service}{'sessions'}{'limit'});

  }

}

sub check_backends {
  _debug('check_backends', 'is starting check on backends');
  # Run checks for each of the backends
  foreach my $service (sort keys %data) {
    next unless $data{$service}{'type'} eq TYPE_BACKEND;
    next if $checks{$service}{'state'} eq 'x';
    _debug('check_backends', _('checking BACKEND %s', $service));

    my ($msg, $total, $up, $down, $disabled) = ('',0,0,0,0);

    foreach my $server (sort keys %{$data{$service}{'servers'}}) {
      ++$total unless $data{$service}{'servers'}{$server}{'backend'};
      ++$up        if $data{$service}{'servers'}{$server}{'up'};
      ++$down      if $data{$service}{'servers'}{$server}{'down'};
      ++$disabled  if $data{$service}{'servers'}{$server}{'disabled'};
    }

    _debug('check_backends', _('found %d up, %d down, %d disabled (%d total)',
                               $up, $down, $disabled, $total));

    switch ($checks{$service}{'state'}) {
      case 'u' {
        $msg = 'BACKEND %s has fallen below %d available server(s) (%d up, %d down, %d disabled)';
      }
      case 'd' {
        $msg = 'BACKEND %s has passed %d down/disabled servers (%d up, %d down, %d disabled)',
      }
    }

    do_critical(_($msg, $service, $checks{$service}{'be_crit'}, $up, $down, $disabled))
      unless check_pass($checks{$service}{'state'}, $checks{$service}{'be_crit'},
        ($checks{$service}{'state'} eq 'u' ? $up : ($down+$disabled)), $total);
    do_warning(_($msg, $service, $checks{$service}{'be_warn'}, $up, $down, $disabled))
      unless check_pass($checks{$service}{'state'}, $checks{$service}{'be_warn'},
        ($checks{$service}{'state'} eq 'u' ? $up : ($down+$disabled)), $total);

    next unless $data{$service}{'sessions'}{'limit'} > 0;
    _debug('check_frontends', 'running Sessions check');
    _debug('check_backends', _('found %d (limit %d); threshold is %.1f%% (warn) or %.1f%% (crit)',
                               $data{$service}{'sessions'}{'current'},
                               $data{$service}{'sessions'}{'limit'},
                               ($checks{$service}{'limit_warn'}*100),
                               ($checks{$service}{'limit_crit'}*100)));

    do_critical(_('Sessions for BACKEND %s has passed %d%% of Limit (%d active, %d limit)',
                  $service, ($checks{$service}{'limit_crit'}*100),
                  $data{$service}{'sessions'}{'current'},
                  $data{$service}{'sessions'}{'limit'}))
      unless check_pass('t', $checks{$service}{'limit_crit'},
                             $data{$service}{'sessions'}{'current'},
                             $data{$service}{'sessions'}{'limit'});

    do_warning(_('Sessions for BACKEND %s has passed %d%% of Limit (%d active, %d limit)',
                  $service, ($checks{$service}{'limit_warn'}*100),
                  $data{$service}{'sessions'}{'current'},
                  $data{$service}{'sessions'}{'limit'}))
      unless check_pass('t', $checks{$service}{'limit_warn'},
                             $data{$service}{'sessions'}{'current'},
                             $data{$service}{'sessions'}{'limit'});

  }
}

sub check_servers {
  _debug('check_servers', 'is starting check on servers');
  # Run checks for each of servers in each of the backends
  foreach my $service (sort keys %data) {
    next unless $data{$service}{'type'} eq TYPE_BACKEND;
    next if $checks{$service}{'state'} eq 'x';
    foreach my $server (sort keys %{$data{$service}{'servers'}}) {
      next if $data{$service}{'servers'}{$server}{'backend'};
      _debug('check_servers', _('checking SERVER %s on BACKEND %s', $server, $service));

      if ($data{$service}{'servers'}{$server}{'sessions'}{'limit'} > 0) {
        _debug('check_servers', 'running Sessions check');
        _debug('check_servers', _('found %d (limit %d); thresholds are %.1f%%, %.1f%%',
                                  $data{$service}{'servers'}{$server}{'sessions'}{'current'},
                                  $data{$service}{'servers'}{$server}{'sessions'}{'limit'},
                                  ($checks{$service}{'limit_warn'}*100),
                                  ($checks{$service}{'limit_crit'}*100)));

        do_critical(_('Sessions for SERVER %s on BACKEND %s has passed %d%% of Limit (%d active, %d limit)',
                      $server, $service, ($checks{$service}{'limit_crit'}*100),
                      $data{$service}{'servers'}{$server}{'sessions'}{'current'},
                      $data{$service}{'servers'}{$server}{'sessions'}{'limit'}))
          unless check_pass('t', $checks{$service}{'limit_crit'},
                                 $data{$service}{'servers'}{$server}{'sessions'}{'current'},
                                 $data{$service}{'servers'}{$server}{'sessions'}{'limit'});

        do_warning(_('Sessions for SERVER %s on BACKEND %s has passed %d%% of Limit (%d active, %d limit)',
                      $server, $service, ($checks{$service}{'limit_warn'}*100),
                      $data{$service}{'servers'}{$server}{'sessions'}{'current'},
                      $data{$service}{'servers'}{$server}{'sessions'}{'limit'}))
          unless check_pass('t', $checks{$service}{'limit_warn'},
                                 $data{$service}{'servers'}{$server}{'sessions'}{'current'},
                                 $data{$service}{'servers'}{$server}{'sessions'}{'limit'});

      }

      if ($data{$service}{'servers'}{$server}{'queued'}{'limit'} > 0) {
        _debug('check_servers', 'running Queue check');
        _debug('check_servers', _('found %d (limit %d); thresholds are %.1f%%, %.1f%%',
                                  $data{$service}{'servers'}{$server}{'sessions'}{'current'},
                                  $data{$service}{'servers'}{$server}{'sessions'}{'limit'},
                                  ($checks{$service}{'limit_warn'}*100),
                                  ($checks{$service}{'limit_crit'}*100)));

        do_critical(_('Queue for SERVER %s on BACKEND has passed %d%% of Limit (%d active, %d limit)',
                      $server, $service, ($checks{$service}{'limit_crit'}*100),
                      $data{$service}{'servers'}{$server}{'queued'}{'current'},
                      $data{$service}{'servers'}{$server}{'queued'}{'limit'}))
          unless check_pass('t', $checks{$service}{'limit_crit'},
                                 $data{$service}{'servers'}{$server}{'queued'}{'current'},
                                 $data{$service}{'servers'}{$server}{'queued'}{'limit'});

        do_warning(_('Queue for SERVER %s on BACKEND %s has passed %d%% of Limit (%d active, %d limit)',
                      $server, $service, ($checks{$service}{'limit_warn'}*100),
                      $data{$service}{'servers'}{$server}{'queued'}{'current'},
                      $data{$service}{'servers'}{$server}{'queued'}{'limit'}))
          unless check_pass('t', $checks{$service}{'limit_warn'},
                                 $data{$service}{'servers'}{$server}{'queued'}{'current'},
                                 $data{$service}{'servers'}{$server}{'queued'}{'limit'});
      }
    }
  }
}

# Run the program
&main();

__END__

=head1 NAME

check_haproxy: This is a Nagios plugin to check the status of HAProxy over a
socket connection (by default, /var/run/haproxy.sock).

=head1 SYNOPSIS

check_haproxy [--defaults (defaults)] [--overrides S<(1 2 ... n)>]
  [--[no]frontends] [--[no]backends] [--[no]servers]
  [--socket (path)] [--help]

=head1 OPTIONS

=over

=item B<-f>, B<--[no]frontends>

Enable/disable checks for the frontends in HAProxy (that they're marked as OPEN
and the session limits haven't been reached).

=item B<-b>, B<--[no]backends>

Enable/disable checks for the backends in HAProxy (that they have the required
quorum of servers, and that the session limits haven't been reached).

=item B<-s>, B<--[no]servers>

Enable/disable checks for the servers in HAProxy (that they haven't reached the
limits for the sessions or for queues).

=item B<-D>, B<--defaults> I<defaults>

Set/Override the defaults which will be applied to all checks (unless
specifically set by --overrides). Takes the form:

  C<{u,d,x},{svr_warn},{svr_crit},{sess_warn},{sess_crit}>

Each of these is optional, but the positioning is important. To fully override,
set (for example):

  C<u,10,5,.25,0.5>

which means:

=over

=item C<u>

Check for servers up (C<u>) or servers down (C<d>), or disable all checks on
that particular frontend/backend (C<x>).

=item C<10>

WARNING if less than 10 servers are up, or if at least that many servers are
down.

=item C<5>

CRITICAL if less than 5 servers are available, or if at least that many have
gone away.

=item C<.25>

WARNING if more any frontend, backend, or individual server has gone over 25% of
it's maximum allowed sessions (or any queue for any server on the backend is at
least 25% full).

=item C<0.5>

CRITICAL for the same reasons as previous, but we've reached 50% of these
levels.

=back

To override only some of these values from the pre-set defaults
(C<u,5,2,.75..9>), simply leave the others as empty, for example: C<,10,7> will
leave checks as up, but increase the server WARN/CRIT to 10/7. or to switch to
use down, use C<d,>, or off with C<x>.

Each number has two meanings:

=over

=item C<less than 1>

Where any number is less than (but not equal to) 1, then the value is understood
as a percentage of the available servers or session limits. If you have 20
servers and set a threshold of .25 then that would mean 5 servers either up or
down.

=item C<greater than, or equal to 1>

Where any number is greater than, or equal to, 1, then this is a fixed value.
By setting 5 you are hard-coding the number of servers or sessions, regardless
of the limit or number available to HAProxy.

The code B<does not> alter this limit if it is greater than the available
servers or sessions so there are situations where the alert may not trigger.
For example, in the event of C<down> being the trigger and the number of servers
is less than the Warning or Critical thresholds, the error may never trigger.
It is generally better to use C<up> and percentage values where you are going
to have a flexible number of backends.

=back

=item B<-O>, B<--overrides> I<override override ... override>

Override the defaults for a particular frontend or backend, in the form
{name}:{override}, where {override} is the same format as --defaults above. For
example, to override the frontend called "api" and allow that to increase to
limits of 15/10 for WARN/CRIT, use C<api:,15,10>. Add as many as you like as
space-delimited options:

  C<--overrides api:,15,10 assets:d,2,5 webmail:u,3,2>

=item B<-S>, B<--socket> I</path/to/socket>

Path to the socket check_haproxy should connect to (requires read/write
permissions and must be at least user level; no operator or admin privileges
needed).

=back

=head1 DESCRIPTION

B<This program> will connect to the HAProxy socket and parse the statistics to
ensure that it is operating within accepted limits, as per the defaults, or as
set for each frontend/backend.

=head1 HISTORY

=over

=item v1.0.0rc1

=over

=item *

Testing Release.

=back

=item v1.0.0rc2

=over

=item *

Fix bug in calling do_unknown().

=back

=item v1.0.0

=over

=item *

Added support for disabling some checks with an x state.

=back

=item v1.0.1

=over

=item *

Added prefix to response message.

=back

=item v1.0.2

=over

=item *

Removed old constants no longer needed.

=item *

Added help POD messages.

=back

=item v1.0.3

=over

=item *

Improved error messages around check_pass.

=item *

Fixed critical messaeg for sessions levels: Missing substitution in the report
string.

=item *

Remove sanity check forcing a maximum value to be blow any threshold as it seams
that in some cases this is possible in HAProxy.

=back

=back

=end text

=head1 AUTHOR

Jonathan Wright <github@jon.than.io>

=head1 COPYRIGHT AND LICENCE

(c) 2014-2015, Jonathan Wright <github@jon.than.io>

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

=cut
