#!/usr/bin/perl -w
#######################################################################################
# monitorLoad.pl
# Written by: Brandon Zehm <caspian@dotconf.net>
# 
# Purpose:
#   To monitor the 15 minute system load average and print 
#   information to the system log.
#
# Technical Description:
#   Basically does a `cat /proc/loadavg` and parses the output.  If the system load
#   is higher than the "thresholdWARNING" then it writes the current system load
#   to a file.  If it is higher than "thresholdWARNING" on more than two consecutive 
#   runs then it changes the output message to one of the warning messages.
#
# Changelog:
#     01/20/2004 - v1.20
#         - Require a -r option to run.
#         - Added a -l option for logging to a file
#         - Textual changes to most error messages.
#         - It now will try opening /proc/loadavg up to 10 times before giving up.
#
#     07/25/2002 - v1.03
#       Jared Cheney jared_cheney@nonhp-am.exch.hp.com
#       modified it to write the system load to a temporary file the first time
#       the load is checked - then the next time it will compare against the first 
#       value and only log an error if the value is rising after two consecutive
#       readings. 07/29 - update: some system calls were replaced with their perl 
#       counterparts by Brandon.
# 
#######################################################################################
use strict;

## Global Variable(s)
my %conf = (
    "programName"          => 'monitorLoad.pl',
    "version"              => '1.20',
    "authorName"           => 'Brandon Zehm',                    ## Information about the author or owner of this script.
    "authorEmail"          => 'caspian@dotconf.net',
    "hostname"             => $ENV{'HOSTNAME'},                  ## Used in printmsg() for all output.
    
    "syslogProgram"        => '/usr/local/bin/syslog.pl',
    
    "run"                  => 0,                                 ## Whether we should run script or not
    "debug"                => 0,
    "stdout"               => 0,
    "syslog"               => 1,                                 ## Syslog messages by default (--stdout disables this)
    "facility"             => "USER",
    "priority"             => "INFO",
    "load"                 => "",
    "previousLoad"         => "",
    "comparisonFile"       => "/tmp/monitorLoad.tmp",
    "logging"              => '',                                ## If this is true the printmsg function prints to the log file
    "logFile"              => '',                                ## If this is specified (form the command line via -l) this file will be used for logging.

    ## Set some thresholds, this just changes the message printed and the syslog priority.
    "thresholdWARNING"     => '0.5',
    "thresholdERROR"       => 1,
    "thresholdCRIT"        => 2,
    "thresholdEMERG"       => 5,
);







#############################
##
##      MAIN PROGRAM
##
#############################

## Initialize
initialize();

## Process Command Line
processCommandLine();

## Open the log file if we need to
if ($conf{'logFile'}) {
    if ($conf{'logging'}) {
        $conf{'logging'} = 0;
        close LOGFILE;
    }
    if (openLogFile($conf{'logFile'})) { quit("OS-ERROR => Opening the log file [$conf{'logFile'}] returned the error: $!", 1); }
}

## Get the load average
for (my $counter = 0; $counter <= 10; $counter++, sleep(10)) {
    quit("OS-ERROR => 10 consecutive errors while trying to read /proc/loadavg", 1) if ($counter >= 10);
    open(FILE, "/proc/loadavg") or next;
    $conf{'input'} = <FILE>;
    close FILE;
    last if $conf{'input'};
}

## Extract the value we need (take the average of all 3 system load values)
$conf{'load'} = sprintf("%.02f", (( (split(/\s+/, $conf{'input'}))[0] + (split(/\s+/, $conf{'input'}))[1] + (split(/\s+/, $conf{'input'}))[2]) / 3));
printmsg("OS-DEBUG => The current load is: $conf{'load'}", 1);


## Compare the extracted value to what was previously measured and determine if the value is rising or falling

   my $directionOfFlow = "";

   if (-f $conf{'comparisonFile'}) {
       ## Get the previous load average
       open(FILE, "$conf{'comparisonFile'}");
       $conf{'previousLoad'} = <FILE>;
       close FILE;
       chomp $conf{'previousLoad'};
       printmsg("OS-DEBUG => The previous load was: $conf{'previousLoad'}", 1);
   }
   
   if ($conf{'previousLoad'}) {
       if ($conf{'load'} >= $conf{'previousLoad'} ) { $directionOfFlow = "rising"; }
       elsif ($conf{'load'} < $conf{'previousLoad'} ) { $directionOfFlow = "falling"; }
       else { $directionOfFlow = "unable to determine"; }
   }
   else { $directionOfFlow = "unable to determine"; }


## Overwrite the old measurement with the new one into the compareLoad file.
    open(FILE, ">$conf{'comparisonFile'}");
    print FILE $conf{'load'};
    close FILE;

## Decide how urgent this is:
    
    ## EMERGENCY ##
    if ( $conf{'load'} >= $conf{'thresholdEMERG'}) {
      ## If the system load has risen or remained the same since the last check, then set the priority to EMERG
      if ($directionOfFlow eq "rising") {
          $conf{'priority'} = "EMERG";
      }
      ## If the system load has dropped since the last check, then set the priority to WARNING, so a page is not generated.
      elsif ($directionOfFlow eq "falling") {
          $conf{'priority'} = "WARNING";
      }
      ## If neither rising nor falling, assume that the script just started up or the previous value didn't pass the WARNING threshold, so only set as level WARNING
      else {
          $conf{'priority'} = "WARNING";
      }
    } 
    
    ## CRIT ##
    elsif ( $conf{'load'} >= $conf{'thresholdCRIT'} ) {
      ## If the system load has risen or remained the same since the last check, then set the priority to CRIT
      if ($directionOfFlow eq "rising") {
          $conf{'priority'} = "CRIT";
      }
      ## If the system load has dropped since the last check, then set the priority to WARNING, so a page is not generated.
      elsif ($directionOfFlow eq "falling") {
          $conf{'priority'} = "WARNING";
      }
      ## If neither rising nor falling, assume that the script just started up or the previous value didn't pass the WARNING threshold, so only set as level WARNING
      else {
          $conf{'priority'} = "WARNING";
      }
    } 
    
    ## ERROR ##
    elsif ( $conf{'load'} >= $conf{'thresholdERROR'} ) {
      ## If the system load has risen or remained the same since the last check, then set the priority to ERROR
      if ($directionOfFlow eq "rising") {
          $conf{'priority'} = "ERROR";
      }
      ## If the system load has dropped since the last check, then set the priority to WARNING, so a page is not generated.
      elsif ($directionOfFlow eq "falling") {
          $conf{'priority'} = "WARNING";
      }
      ## If neither rising nor falling, assume that the script just started up or the previous value didn't pass the WARNING threshold, so only set as level WARNING
      else {
          $conf{'priority'} = "WARNING";
      }
    }
    
    ## WARNING ##
    elsif ( $conf{'load'} >= $conf{'thresholdWARNING'} ) {
        $conf{'priority'} = "WARNING";
        ## rm the comparisonFile so that the $directionOfFlow doesn't get set unless the load is >= the ERROR threshold at least two times
        unlink($conf{'comparisonFile'});
    }
    
    else {
        $conf{'priority'} = "INFO";
        ## rm the comparisonFile so that the $directionOfFlow doesn't get set unless the load is >= the ERROR threshold at least two times
        unlink($conf{'comparisonFile'});
    }
  

## DEBUG:
printmsg("OS-DEBUG => $directionOfFlow is the current direction of flow", 1);

## Generate an output message
if ( ($conf{'load'} >= $conf{'thresholdERROR'}) && ($directionOfFlow eq "rising") ) {
    $conf{'message'} = "OS-$conf{'priority'} => The 15 minute system load average is [$conf{'load'}] and has risen over the last 15 minutes.";
}
else {
    $conf{'message'} = "OS-$conf{'priority'} => The 15 minute system load average is [$conf{'load'}].";
}


## Syslog the message (or print it to stdout)
printmsg($conf{'message'}, 0);


## Quit
quit("",0);
































######################################################################
## Function:    help ()
##
## Description: For all those newbies ;) 
##              Prints a help message and exits the program.
## 
######################################################################
sub help {
print <<EOM;

$conf{'programName'}-$conf{'version'} by $conf{'authorName'} <$conf{'authorEmail'}>

Synopsis: 
  Checks the 15 minute load average and logs it to the syslog daemon.

Usage:  $conf{'programName'} [options]

  Required:
    -r                        run script
  
  Optional:
    --stdout                  print messages to STDOUT rather than the syslog.
    --facility=[0-11]         syslog facility (1/USER is used by default)
    -l <logfile>              enable logging to the specified file
    -v                        verbosity - use multiple times for greater effect

EOM
quit("", 1);
}










######################################################################
##  Function: initialize ()
##  
##  Does all the script startup jibberish.
##  
######################################################################
sub initialize {
    
    ## Set STDOUT to flush immediatly after each print  
    $| = 1;
    
    ## Intercept signals
    $SIG{'QUIT'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    $SIG{'INT'}   = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    $SIG{'KILL'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    $SIG{'TERM'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    
    ## ALARM and HUP signals are not supported in Win32
    unless ($^O =~ /win/i) {
        $SIG{'HUP'}   = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
        $SIG{'ALRM'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    }
    
    ## Make sure the path is sane
    $ENV{'PATH'} .= ":/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin";
    
    ## Fixup $conf{'hostname'}
    if ($conf{'hostname'}) {
        $conf{'hostname'} = $conf{'hostname'};
        $conf{'hostname'} =~ s/\..*$//;
    }
    else {
        $conf{'hostname'} = "unknown";
    }
    
    return(0);
}









######################################################################
##  Function: processCommandLine ()
##  
##  Processes command line storing important data in global var %conf
##  
######################################################################
sub processCommandLine {
  
    ############################
    ##  Process command line  ##
    ############################
  
    my @ARGS = @ARGV;
    my $numargv = @ARGS;
    my $counter = 0;
    for ($counter = 0; $counter < $numargv; $counter++) {
    
        if ($ARGS[$counter] =~ s/^--stdout//i) {             ## stdout ##
            $conf{'stdout'} = 1;
            $conf{'syslog'} = 0;
        }
    
        elsif ($ARGS[$counter] =~ s/^-v+//i) {               ## Verbosity ##
            $conf{'debug'} += (length($&) - 1);
        }
        
        elsif ($ARGS[$counter] =~ /^-l$/) {                  ## Log File ##
            $counter++;
            $conf{'logFile'} = $ARGS[$counter];
        }
        
        elsif ($ARGS[$counter] =~ s/^--facility=//i) {       ## Facility ##
            $conf{'facility'} = $';
        }

        elsif ($ARGS[$counter] =~ /^-r$/) {                  ## Run
            $conf{'run'} = 1;
        }
        
        elsif ($ARGS[$counter] =~ /^-h$|^--help$/i) {        ## Help ##
            help();
        }
        
        else {                                               ## The Message ##
            quit("OS-ERROR => The option [$ARGS[$counter]] is unknown. Try --help", 1);
        }

    }
    
    ## If the user didn't use a -r print the help
    if ($conf{'run'} == 0) {
        help();
    }
    
  
    return(1);
}










###############################################################################################
##  Function:    printmsg (string $message, int $level)
##
##  Description: Handles all messages - 
##               Depending on the state of the program it will log
##               messages to a log file, print them to STDOUT or both.
##               
##
##  Input:       $message          A message to be printed, logged, etc.
##               $level            The debug level of the message. If not defined 0
##                                 will be assumed.  0 is considered a normal message, 
##                                 1 and higher is considered a debug message.
##  
##  Output:      Prints to STDOUT, to LOGFILE, both, or none depending 
##               on the state of the program and the debug level specified.
##  
##  Example:     printmsg("ERROR => The file could not be opened!", 0);
###############################################################################################
sub printmsg {
    ## Assign incoming parameters to variables
    my ( $message, $level ) = @_;
    
    ## Make sure input is sane
    $level = 0 if (!defined($level));
    
    ## Continue only if the debug level of the program is >= message debug level.
    if ($conf{'debug'} >= $level) {
        
        ## Get the date in the format: Dec  3 11:14:04
        my ($sec, $min, $hour, $mday, $mon) = localtime();
        $mon = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[$mon];
        my $date = sprintf("%s %02d %02d:%02d:%02d", $mon, $mday, $hour, $min, $sec);
    
        ## Syslog the message is needed
        if ($conf{'syslog'}) {
            system( qq{$conf{'syslogProgram'} --facility=$conf{'facility'} --priority=$conf{'priority'} "$conf{'programName'}: $message" });
        }
        
        ## Print to STDOUT always if debugging is enabled, or if conf{stdout} is true.
        if ( ($conf{'debug'} >= 1) or ($conf{'stdout'} == 1) ) {
            print "$date $conf{'hostname'} $conf{'programName'}: $message\n";
        }
        
        ## Print to the log file if $conf{'logging'} is true
        if ($conf{'logging'}) {
            print LOGFILE "$date $conf{'hostname'} $conf{'programName'}: $message\n";
        }
        
    }
    
    ## Return 0 errors
    return(0);
}














###############################################################################################
## FUNCTION:    
##   openLogFile ( $filename )
## 
## 
## DESCRIPTION: 
##   Opens the file $filename and attaches it to the filehandle "LOGFILE".  Returns 0 on success
##   and non-zero on failure.  Error codes are listed below, and the error message gets set in
##   global variable $!.
##   
##   
## Example: 
##   openFile ("/var/log/scanAlert.log");
##
###############################################################################################
sub openLogFile {
    ## Get the incoming filename
    my $filename = $_[0];
    
    ## Make sure our file exists, and if the file doesn't exist then create it
    if ( ! -f $filename ) {
        printmsg("NOTICE => The file [$filename] does not exist.  Creating it now with mode [0600].", 0);
        open (LOGFILE, ">>$filename");
        close LOGFILE;
        chmod (0600, $filename);
    }
    
    ## Now open the file and attach it to a filehandle
    open (LOGFILE,">>$filename") or return (1);
    
    ## Put the file into non-buffering mode
    select LOGFILE;
    $| = 1;
    select STDOUT;
    
    ## Tell the rest of the program that we can log now
    $conf{'logging'} = "yes";
    
    ## Return success
    return(0);
}











######################################################################
##  Function:    quit (string $message, int $errorLevel)
##  
##  Description: Exits the program, optionally printing $message.  It 
##               returns an exit error level of $errorLevel to the 
##               system  (0 means no errors, and is assumed if empty.)
##
##  Example:     quit("Exiting program normally", 0);
######################################################################
sub quit {
  my %incoming = ();
  (
    $incoming{'message'},
    $incoming{'errorLevel'}
  ) = @_;
  $incoming{'errorLevel'} = 0 if (!defined($incoming{'errorLevel'}));
  
  
  ## Print exit message
  if ($incoming{'message'}) { 
    printmsg($incoming{'message'}, 0);
  }
  
  ## Exit
  exit($incoming{'errorLevel'});
}






