#!/usr/bin/perl -w # # Copyright (c) 2010 Stéphane Urbanovski <stephane.urbanovski@ac-nancy-metz.fr> # Copyright (c) 2019 Claudio Kuenzler <ck@claudiokuenzler.com> # # 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 (or with Nagios); if not, write to the # Free Software Foundation, Inc., 59 Temple Place - Suite 330, # Boston, MA 02111-1307, USA # # $Id: $ use strict; # should never be differently :-) use warnings; use Locale::gettext; use File::Basename; # get basename() use POSIX qw(setlocale); use Time::HiRes qw(time); # get microtime use POSIX qw(mktime); sub load_module { my @names = @_; my $module; for my $name (@names) { my $file = $name; # requires need either a bare word or a file name $file =~ s{::}{/}gsxm; $file .= '.pm'; eval { require $file; $name->import(); $module = $name; }; last if $module; } return $module; } my $plugin_module; BEGIN { $plugin_module = load_module( 'Monitoring::Plugin', 'Nagios::Plugin' ); } use LWP::UserAgent; # http client use HTTP::Request; # used by LWP::UserAgent use HTTP::Status; # to get http err msg use IO::Socket; # To use unix Sockets use Data::Dumper; my $PROGNAME = basename($0); '$Revision: 1.1 $' =~ /^.*(\d+\.\d+) \$$/; # Use The Revision from RCS/CVS/SVN my $VERSION = $1; my $DEBUG = 0; my $TIMEOUT = 9; # i18n : setlocale(LC_MESSAGES, ''); textdomain('nagios-plugins-perl'); my $np = $plugin_module->new( version => $VERSION, blurb => _gt('Plugin to check HAProxy stats url'), usage => "Usage: %s [ -v|--verbose ] -u <url> [-t <timeout>] [-U <username>] [-P <password>] [ -c|--critical=<threshold> ] [ -w|--warning=<threshold> ] [ -b|--critical-backends=<comma separated list> ] [ -i|--ignore-backends=<comma separated list> ]", timeout => $TIMEOUT+1 ); $np->add_arg ( spec => 'debug|d', help => _gt('Debug level'), default => 0, ); $np->add_arg ( spec => 'username|U=s', help => _gt('Username for HTTP Auth'), required => 0, ); $np->add_arg ( spec => 'password|P=s', help => _gt('Password for HTTP Auth'), required => 0, ); $np->add_arg ( spec => 'w=f', help => _gt('Warning request time threshold (in seconds)'), default => 2, label => 'FLOAT' ); $np->add_arg ( spec => 'c=f', help => _gt('Critical request time threshold (in seconds)'), default => 10, label => 'FLOAT' ); $np->add_arg ( spec => 'url|u=s', help => _gt('URL of the HAProxy csv statistics page HTTP or unix Socket.'), required => 1, ); $np->add_arg ( spec => 'critical-backends|b=s', help => _gt('List of critical backend, if set other backend are only warning backend'), required => 0, ); $np->add_arg ( spec => 'ignore-backends|i=s', help => _gt('Comma-separated list of backends to ignore'), required => 0, ); $np->getopts; $DEBUG = $np->opts->get('debug'); my $verbose = $np->opts->get('verbose'); my $username = $np->opts->get('username'); my $password = $np->opts->get('password'); my $crit_backends = $np->opts->get('critical-backends'); my @crit_backends_list; if ( defined ( $crit_backends ) ) { @crit_backends_list = split(',',$crit_backends); } my $ignore_backends = $np->opts->get('ignore-backends'); my @ignore_backends_list; if ( defined ( $ignore_backends ) ) { @ignore_backends_list = split(',',$ignore_backends); } # Thresholds : # time my $warn_t = $np->opts->get('w'); my $crit_t = $np->opts->get('c'); my $url = $np->opts->get('url'); # Create a LWP user agent object: my $ua = new LWP::UserAgent( 'env_proxy' => 0, 'timeout' => $TIMEOUT, ); $ua->agent(basename($0)); # Workaround for LWP bug : $ua->parse_head(0); # For csv data my $stats=""; my $timer = time(); if ( $url =~ /^http/ ) { if ( defined($ENV{'http_proxy'}) ) { # Normal http proxy : $ua->proxy(['http'], $ENV{'http_proxy'}); # Https must use Crypt::SSLeay https proxy (to use CONNECT method instead of GET) $ENV{'HTTPS_PROXY'} = $ENV{'http_proxy'}; } # Build and submit an http request : my $request = HTTP::Request->new('GET', $url); # Authenticate if username and password are supplied if ( defined($username) && defined($password) ) { $request->authorization_basic($username, $password); } my $http_response = $ua->request( $request ); if ( $http_response->is_error() ) { my $err = $http_response->code." ".status_message($http_response->code)." (".$http_response->message.")"; $np->add_message(CRITICAL, _gt("HTTP error: ").$err ); } elsif ( ! $http_response->is_success() ) { my $err = $http_response->code." ".status_message($http_response->code)." (".$http_response->message.")"; $np->add_message(CRITICAL, _gt("Internal error: ").$err ); } if ( $http_response->is_success() ) { $stats = $http_response->content; } }elsif ( $url =~ /^\// ) { my $sock = new IO::Socket::UNIX ( Peer => "$url", Type => SOCK_STREAM, Timeout => 1); if ( !$sock ) { my $err = "Can't connect to unix socket"; $np->add_message(CRITICAL, _gt("Internal error: ").$err ); }else{ print $sock "show stat\n"; while(my $line = <$sock>){ $stats.=$line; } } }else { my $err = "Can't detect socket type"; $np->add_message(CRITICAL, _gt("Internal error: ").$err ); } $timer = time()-$timer; my $status = $np->check_threshold( 'check' => $timer, 'warning' => $warn_t, 'critical' => $crit_t, ); $np->add_perfdata( 'label' => 't', 'value' => sprintf('%.6f',$timer), 'min' => 0, 'uom' => 's', 'threshold' => $np->threshold() ); if ( $status > OK ) { $np->add_message($status, sprintf(_gt("Response time degraded: %.6fs !"),$timer) ); } my $message = 'msg'; ($status, $message) = $np->check_messages(); if ( $status == OK && $stats ne "") { if ($DEBUG) { print "------------------===csv output===------------------\n$stats\n-----------------------------------------------------\n"; print "t=".$timer."s\n"; }; my @fields = (); my @rows = split(/\n/,$stats); if ( $rows[0] =~ /#\ \w+/ ) { $rows[0] =~ s/#\ //; @fields = split(/\,/,$rows[0]); } else { $np->nagios_exit(UNKNOWN, _gt("Can't find csv header !") ); } my %stats = (); for ( my $y = 1; $y < $#rows; $y++ ) { my @values = split(/\,/,$rows[$y]); if ( !defined($stats{$values[0]}) ) { $stats{$values[0]} = {}; } if ( !defined($stats{$values[0]}{$values[1]}) ) { $stats{$values[0]}{$values[1]} = {}; } for ( my $x = 2,; $x <= $#values; $x++ ) { # $stats{pxname}{svname}{valuename} $stats{$values[0]}{$values[1]}{$fields[$x]} = $values[$x]; } } #print Dumper(\%stats); my %stats2 = (); my $okMsg = ''; foreach my $pxname ( keys(%stats) ) { if ( defined($ignore_backends) ) { if ( grep(/^$pxname$/,@ignore_backends_list) ) { logD( sprintf(_gt("Skipping %s because it is defined in ignore list."),$pxname) ); next; } } $stats2{$pxname} = { 'act' => 0, 'acttot' => 0, 'bck' => 0, 'bcktot' => 0, 'scur' => 0, 'slim' => 0, 'bin' => 0, 'bout' => 0, }; foreach my $svname ( keys(%{$stats{$pxname}}) ) { if ( $stats{$pxname}{$svname}{'type'} eq '2' ) { my $svstatus = $stats{$pxname}{$svname}{'status'} eq 'UP'; my $active = $stats{$pxname}{$svname}{'act'} eq '1'; my $activeDescr = $active ? _gt("Active service") :_gt("Backup service") ; if ( $stats{$pxname}{$svname}{'status'} eq 'UP' ) { logD( sprintf(_gt("%s '%s' is up on '%s' proxy."),$activeDescr,$svname,$pxname) ); } elsif ( $stats{$pxname}{$svname}{'status'} eq 'DOWN' ) { if ( defined($crit_backends) ) { if ( grep(/^$pxname$/,@crit_backends_list) ) { $np->add_message(CRITICAL, sprintf(_gt("%s '%s' is DOWN on '%s' proxy !"),$activeDescr,$svname,$pxname) ); }else{ $np->add_message(WARNING, sprintf(_gt("%s '%s' is DOWN on '%s' proxy !"),$activeDescr,$svname,$pxname) ); } }else{ $np->add_message(CRITICAL, sprintf(_gt("%s '%s' is DOWN on '%s' proxy !"),$activeDescr,$svname,$pxname) ); } } if ( $stats{$pxname}{$svname}{'act'} eq '1' ) { $stats2{$pxname}{'acttot'}++; $stats2{$pxname}{'act'} += $svstatus; } elsif ($stats{$pxname}{$svname}{'bck'} eq '1') { $stats2{$pxname}{'bcktot'}++; $stats2{$pxname}{'bck'} += $svstatus; } $stats2{$pxname}{'scur'} += $stats{$pxname}{$svname}{'scur'}; logD( "Current sessions : ".$stats{$pxname}{$svname}{'scur'} ); } elsif ( $stats{$pxname}{$svname}{'type'} lt '2' ) { $stats2{$pxname}{'slim'} = $stats{$pxname}{$svname}{'slim'}; $stats2{$pxname}{'bin'} = $stats{$pxname}{$svname}{'bin'}; $stats2{$pxname}{'bout'} = $stats{$pxname}{$svname}{'bout'}; } } if ( $stats2{$pxname}{'acttot'} > 0 ) { $okMsg .= ' '.$pxname.' (Active: '.$stats2{$pxname}{'act'}.'/'.$stats2{$pxname}{'acttot'}; if ( $stats2{$pxname}{'bcktot'} > 0 ) { $okMsg .= ' , Backup: '.$stats2{$pxname}{'bck'}.'/'.$stats2{$pxname}{'bcktot'}; } $okMsg .= ')'; $np->add_perfdata( 'label' => 'sess_'.$pxname, 'value' => $stats2{$pxname}{'scur'}, 'min' => 0, 'uom' => '', 'max' => $stats2{$pxname}{'slim'}, ); $np->add_perfdata( 'label' => 'bytes_in_'.$pxname, 'value' => $stats2{$pxname}{'bin'}, 'min' => '0', 'uom' => 'B', 'max' => '', ); $np->add_perfdata( 'label' => 'bytes_out_'.$pxname, 'value' => $stats2{$pxname}{'bout'}, 'min' => '0', 'uom' => 'B', 'max' => '', ); } } #print Dumper(\%stats2); ($status, $message) = $np->check_messages('join' => ' '); if ( $status == OK ) { $message = $okMsg; } } # if ( $verbose ) { # ($status, $message) = $np->check_messages('join' => '<br/>','join_all' => '<br/>'); # } else { # ($status, $message) = $np->check_messages('join' => '<br/>'); # } $np->nagios_exit($status, $message ); sub logD { print STDERR 'DEBUG: '.$_[0]."\n" if ($DEBUG); } sub logW { print STDERR 'WARNING: '.$_[0]."\n" if ($DEBUG); } # Gettext wrapper sub _gt { return gettext($_[0]); } __END__ =head1 NAME This Nagios plugins check the statistics url provided by HAProxy (http://haproxy.1wt.eu/). =head1 NAGIOS CONGIGURATIONS In F<checkcommands.cfg> you have to add : define command { command_name check_haproxy command_line $USER1$/check_haproxy.pl -u $ARG1$ } In F<services.cfg> you just have to add something like : define service { host_name haproxy.exemple.org normal_check_interval 10 retry_check_interval 5 contact_groups linux-admins service_description HAProxy check_command check_haproxy!http://haproxy.exemple.org/haproxy?stats;csv } Or: define service { host_name haproxy.exemple.org normal_check_interval 10 retry_check_interval 5 contact_groups linux-admins service_description HAProxy check_command check_haproxy!/var/run/my_haproxy.sock } =head1 AUTHOR Stéphane Urbanovski <stephane.urbanovski@ac-nancy-metz.fr> David BERARD <david@nfrance.com> Claudio Kuenzler <ck@claudiokuenzler.com> =cut