#!/usr/bin/perl -w

use 5.006;
use strict;
use warnings;

# $Id: check_named-stats,v 1.3 2005/06/10 12:20:49 fany Exp $

# braucht:
# * libfile-readbackwards-perl >= 1.03
# * libnetsaint-noris-perl
# * perl-base

use FindBin ();
use lib $FindBin::Bin;
use File::ReadBackwards ();
use Getopt::Long qw(GetOptions);
use noris::NetSaint;

use constant DEFAULT_STATS_COMMAND => qw(/usr/sbin/rndc stats);

GetOptions
  'critical=s'      => \our %Critical,
  'decimals=i'      => \( our $Decimals = 0 ),
  'debug+'          => \our $Debug,
  'max-wait=i'      => \( our $MaxWait = 10 ),
  'stats-command=s' => \our @StatsCommand,
  'stats-file=s'    => \( our $StatsFile = '/var/log/named/named.stats' ),
  'warnings=s'      => \our %Warning,
  'help|?'          => \our $Help;

@StatsCommand = DEFAULT_STATS_COMMAND unless @StatsCommand;

exec perldoc => -F => $0 or die "exec('perldoc -F $0'): $!\n" if $Help;

our $NetSaint = new noris::NetSaint;

my $bw = File::ReadBackwards->new($StatsFile)
  or $NetSaint->update(
    Unknown => qq(Kann Stats-Datei "$StatsFile" nicht ffnen: $!\n") ), exit;
my $pos = $bw->tell;
system @StatsCommand
  and
  $NetSaint->update( Unknown => qq("@StatsCommand" schlug fehl mit Status $?)
      . ( $! ? ": $!" : '.' ) ), exit;

my $old_ts;
while ( defined( my $line = $bw->readline ) ) {
    ($old_ts) = $line =~ /^--- Statistics Dump --- \((\d+)\)$/ and last;
}
$NetSaint->update( Unknown =>
      qq(Habe in der Stats-Datei "$StatsFile" keinen alten Timestamp gefunden.)
  ), exit
  unless defined $old_ts;

my %old_v;
while ( defined( my $line = $bw->readline ) ) {
    if ( $line =~ /^(\w+) (\d+)$/ ) { $old_v{SUM} += $old_v{$1} = $2 }
    elsif ( $line =~ /^\+\+\+ Statistics Dump \+\+\+ \($old_ts\)$/ ) { last }
    else {
        $NetSaint->update( Unknown =>
              qq(Unbekannter Eintrag in der Stats-Datei "$StatsFile": $line) );
        exit;
    }
}

my $fh = $bw->get_handle;

my $new_ts;
for ( 1 .. $MaxWait ) {
    seek $fh, $pos, 0
      or $NetSaint->update( Unknown =>
qq(Kann nicht an Position $pos der Stats-Datei "$StatsFile" springen: $!)
      ), exit;
    if ( defined( my $line = <$fh> ) ) {
        ($new_ts) = $line =~ /^\+\+\+ Statistics Dump \+\+\+ \((\d+)\)$/
          and last;
        $NetSaint->update( Unknown =>
qq(Anstelle eines neuen Timestamps fand ich in der Stats-Datei "$StatsFile" folgende Zeile vor: $line)
        );
        exit;
    }
    sleep 1;
}

$NetSaint->update( Unknown =>
      qq(Habe in der Stats-Datei "$StatsFile" keinen neuen Timestamp gefunden.)
  ), exit
  unless defined $new_ts;
( my $seconds = $new_ts - $old_ts ) > 0
  or $NetSaint->update( Unknown =>
"Der neue Timestamp $new_ts ist nicht grer als der alte Timestamp $old_ts."
  ), exit;
print 'Untersuche Zeitraum '
  . localtime($old_ts) . ' bis '
  . localtime($new_ts)
  . " ($seconds Sekunde"
  . ( $seconds != 1 && 'n' ) . "):\n"
  if $Debug;

my %new_v;
while ( defined( my $line = <$fh> ) ) {
    if ( $line =~ /^(\w+) (\d+)$/ ) { $new_v{SUM} += $new_v{$1} = $2 }
    elsif ( $line =~ /^--- Statistics Dump --- \($new_ts\)$/ ) { last }
    else {
        $NetSaint->update( Unknown =>
              qq(Unbekannter Eintrag in der Stats-Datei "$StatsFile": $line) );
        exit;
    }
}

my $f = $Decimals ? 5 + $Decimals : 4;
my %relevant = $Debug ? ( %old_v, %new_v ) : ( %Warning, %Critical );
for ( sort keys %relevant ) {
    if ( defined $old_v{$_} ) {
        if ( defined $new_v{$_} ) {
            my $rate = ( $new_v{$_} - $old_v{$_} ) / $seconds;
            printf "%-10s%10d /%10d =>%$f.${Decimals}f\n", "$_:", $old_v{$_},
              $new_v{$_}, $rate
              if $Debug;
            if ( defined $Critical{$_} && $rate >= $Critical{$_} ) {
                $NetSaint->update(
                    Critical => sprintf
qq(Die Rate fr Schlssel "%s" betrgt %.${Decimals}f pro Sekunde und berschreitet damit den kritischen Schwellenwert %.${Decimals}f.),
                    $_, $rate, $Critical{$_}
                );
            }
            elsif ( defined $Warning{$_} && $rate >= $Warning{$_} ) {
                $NetSaint->update(
                    Warning => sprintf
qq(Die Rate fr Schlssel "%s" betrgt %.${Decimals}f pro Sekunde und berschreitet damit den Warnungs-Schwellenwert %.${Decimals}f.),
                    $_, $rate, $Warning{$_}
                );
            }
        }
        else {
            $NetSaint->update( Unknown =>
                  qq(Fr Schlssel "$_" wurde kein neuer Wert gefunden.) );
        }
    }
    elsif ( defined $new_v{$_} ) {
        $NetSaint->update(
            Unknown => qq(Fr Schlssel "$_" wurde kein alter Wert gefunden.) );
    }
    else {
        $NetSaint->update(
            Unknown => qq(Fr Schlssel "$_" wurden keine Werte gefunden.) );
    }
}

$NetSaint->atleast('OK');

=head1 NAME

check_named-stats -
Nagios-Plugin, das BINDs Befindlichkeit anhand des statistics-file berprft

=head1 SYNOPSE

	check_named-stats --warning SUM=1000 \
		--critical failure=100 --warning failure=50

Erzeugt eine Warnung, wenn der lokale C<named> in letzter Zeit durchschnittlich
mindestens tausend Anfragen pro Sekunde beantworten musste oder durchschnittlich
mehr als fnfzig Anfragen pro Sekunde mit einem Fehler (insb. C<ServFail>)
beantworten musste bzw. einen kritischen Alarm, falls es mehr als hundert
Fehler-Anfragen pro Sekunde waren.

=head1 BESCHREIBUNG

Das Script bittet C<named> mittels eines C<rndc stats>-Kommandos, aktuelle
Statistikwerte in eine Stats-Datei zu loggen, vgl. "DNS and BIND", 4th Edition,
Seiten 191 ff.
Es vergleicht sodann diese Statistikwerte mit den beim vorangegangenen Aufruf
geschriebenen und errechnet anhand der ebenfalls mitprotokollierten Zeitstempel
die Durchschnittswerte pro Sekunde, um diese anschlieend mit den angegebenen
Schwellenwerten zu vergleichen und bei berschreitungen entsprechend
Alarmzustnde zu generieren.

Geht irgendetwas unerwartet schief, wird mindestens ein C<Unknown>-Zustand
erzeugt.

=head1 OPTIONEN

=over 4

=item critical=s %

zur Definition der Schwellenwerte fr kritische Alarmzustnde,
vgl. L<Synopse|/SYNOPSE>

=item warning=s %

zur Definition der Schwellenwerte fr Warnzustnde, vgl. L<Synopse|/SYNOPSE>

=item decimals=i

zur Festlegung, mit wie vielen Dezimalstellen Fliekommawerte (z. B. die
Durchschnittswerte pro Sekunde) ausgegeben werden sollen; Default: 0

=item stats-command=s @

um festzulegen, mit welchem Kommando C<named> gebeten werden soll, aktuelle
Statistikwerte auszugeben; Default: /usr/sbin/rndc stats

=item stats-file=s

legt den Namen der Stats-Datei fest; Default: /var/named/var/log/named.stats

=item max-wait=i

um festzulegen, wie viele Sekunden wir C<named> maximal Zeit geben mchten, um
die angeforderten Statistikwerte in die Stats-Datei zu schreiben; Default: 10.

=item debug+

kann verwendet werden, um zu Debugging-Zwecken auf der Standardausgabe genauere
Informationen ber die gemessenen Werte zu bekommen

=item help|?

um (nur) diese Dokumentation anzeigen zu lassen

=head1 AUTOR

 Martin H. Sluka <fany@noris.net>
 fr die noris network AG
 RT#209019

=cut

