#!/bin/perl5
#
#  ++Copyright Released Product++
# 
#  Copyright (c) 1994, 1995 Sources of Supply Corporation ("SOS").
#  All rights reserved.
# 
#  The SOS Released Product License Agreement specifies the terms and
#  conditions for redistribution.  You may find the License Agreement
#  in the file LICENSE.
# 
#  SOS Corporation
#  461 5th Ave.; 16th floor
#  New York, NY 10017
# 
#  +1 800 SOS UNIX
#  <sos-info@soscorp.com>
# 
#  --Copyright Released Product--
#

#
# A generic rules engine for monitoring log files
#
$version = '1.11';

##############################################################################
### CONFIGURATION 
##############################################################################
# what is the regexp to parse dates?
$date_regexp = '^[^:]+:\d+:\d+';
$host_regexp = '\S+';

$conf_file = '/etc/swatch++.conf';	# name of event configuration file


##############################################################################
### INITIALIZATION
##############################################################################
require 'dateconv2.pl';		# part of the mirror.pl package.
require 'getopts.pl';	# for option processing

if ($] < 5) 			# make sure we use perl 5
{
  die "This script requires perl version 5.\n";
}
# defaults
$debug = 0;			# no debugging by default
$only_show = 0;			# execute trigger actions, not just show them
$verbose = 0;			# don't be verbose by default


sub defineall
{
  package state;
  $num_events = 0;		# number of events recorded

  # event arrays and queues.
  @EVE_regexp = ();		# regular expression to match log instance
  @EVE_period = ();		# window size
  @EVE_count = ();		# number of matches required to trigger event
  @EVE_ignore = ();		# seconds to ignore such events after trigger
  @EVE_action = ();		# action to perform
  @EVE_queue = ();		# array of queues containing log instances
  @EVE_instances = ();		# count of instances in queue
  @EVE_ign_time = ();		# time when started ignoring events (-1 when
}				# not ignoring events)

# force flushing
select(STDOUT); $| = 1;
select(STDERR); $| = 1;

##############################################################################
### FUNCTIONS
##############################################################################
# usage
sub usage
{
  printf(STDERR "Usage: %s [-c <config>] [-d] [-h] [-n] [-v] [-V] <file(s)>\n", $0);
  printf(STDERR "\t-c cfile: use cfile as configuration file (default swatch++.conf)\n");
  printf(STDERR "\t-d: debug\n");
  printf(STDERR "\t-f: follow one file\n");
  printf(STDERR "\t-h: print help/usage info\n");
  printf(STDERR "\t-n: only show what happens if event occured\n");
  printf(STDERR "\t-v: vebose output\n");
  printf(STDERR "\t-V: Version\n");
  printf(STDERR "\t<file(s)>: zero or more input files (stdin if zero)\n");
  exit(1);
}
Getopts("fdhnvVc:") || usage;
if ($opt_c)
{
  $conf_file = $opt_c;
  $opt_c = "";
}
if ($opt_d)
{
  $debug++;
  $opt_d = "";
}
if ($opt_f && $#ARGV != 0)
{
  usage();
}
if ($opt_f)
{
  open(PID,">/tmp/.swatch++pid") || warn "Cannot store PID away";
  print PID "$$\n";
  close(PID);
}
if ($opt_h)
{
  usage();
  $opt_h = "";
}
if ($opt_n)
{
  $only_show++;
  $opt_n = "";
}
if ($opt_v)
{
  $verbose++;
  $opt_v = "";
}
if ($opt_V)
{
  print "Swatch++ version $version\nSOS Corporation <manager\@soscorp.com>\n";
  exit;
  if ($opt_V) { print "-W is stupid\n"; }
}

######################################################################
# debugging messages
#
sub debug
{
  printf(STDERR "#DEBUG: ") if $debug;
  printf(STDERR @_) if $debug;
  printf(STDERR ".\n") if $debug;
}

######################################################################
# print queue (debugging)
#
sub print_queue
{
  my( $i );
  for ($i=0; $i<=$#_; ++$i)
    {
      printf(STDERR "\tQ[%d] = %d\n", $i, $_[$i]);
    }
}

######################################################################
# process a single event's instance
#
sub process_instance
{
  local($this_time, $event_num, $matched, $line) = @_;
  my($old_time) = 0;

  push(@{$state::EVE_queue[$event_num]}, $this_time); # insert new event on the

  # queue
  &print_queue(@{$state::EVE_queue[$event_num]}) if $debug > 1;
  $state::EVE_instances[$event_num]++;	# increment instances counter

  # check if event occured, get time of oldest entry in queue
  $old_time = $state::EVE_queue[$event_num][0];
  print "OLDTIME = $old_time\n" if $debug;

  &print_queue(@{$state::EVE_queue[$event_num]}) if $debug > 1;

  # if the diff b/t old/new time is > windowsize, purge queue
  if ($this_time < $old_time)
    {
      # this may have to die
      warn "Time-line (before queue purge) reversed on line $.! Purging entry.\n";

      shift(@{$state::EVE_queue[$event_num]}); # purge old entry
      $state::EVE_instances[$event_num]--; # decrement instances counted
      $old_time = $state::EVE_queue[$event_num][0]; # reset oldest entry
    }

  while (($this_time - $old_time) > $state::EVE_period[$event_num])
    {
      printf STDERR "diff = %d\n", $this_time - $old_time if $debug;
      printf "purged entry $old_time\n" if $debug;

      shift(@{$state::EVE_queue[$event_num]}); # purge old entry
      $state::EVE_instances[$event_num]--; # decrement instances counted
      $old_time = $state::EVE_queue[$event_num][0]; # reset oldest entry

      &print_queue(@{$state::EVE_queue[$event_num]}) if $debug > 1;
    }

  if ($this_time < $old_time)
    {
      # this may have to die
      warn "Time-line reversed on line $.!\n";
    }

  # now actually check if more than $instances_threshold events of type
  # $event happened within $windowsize seconds.
  if (($this_time - $old_time) <= $state::EVE_period[$event_num] &&
      $state::EVE_instances[$event_num] >= $state::EVE_count[$event_num]) # bingo!
    {
      if ($verbose) 		# if verbose, blah blah blah
	{
	  printf( STDOUT "Event occured %d time%s",
		 $state::EVE_instances[$event_num],
		 ($state::EVE_instances[$event_num] == 1 ? "" : "s"));
	  printf( STDOUT " between time \"%s\" and \"%s\".\n",
		 &time_to_local($old_time), &time_to_local($this_time));
	  printf( STDOUT "\tThe event matched was \"%s\"\n.\n",
		 $matched);
	}

      # non-zero time ignoring also resets event counters
      if ($state::EVE_ignore[$event_num] > 0)
	{
	  $state::EVE_instances[$event_num] = 0;
	  for (;$#{$state::EVE_queue[$event_num]} >= 0;)
	    {
	      shift(@{$state::EVE_queue[$event_num]});
	    }
	}

      # store this time if time-window ignoring is turned on.
      $state::EVE_ign_time[$event_num] = $this_time;
      print "SET IGNORE:$event_num:$this_time:\n" if $debug;

      # execute the actions, or show what will be done if needed
      if ($only_show)
	{
	  printf STDOUT "EVAL: $state::EVE_action[$event_num]\n";
	}
      else
	{
	  # Call user actions in private environment
	  $todo = $state::EVE_action[$event_num];
	  $creg = $state::EVE_regexp[$event_num];
	  package user;

	  # Complete and total environment for action
	  $realdate = $main::realdate;
	  $matched = $main::matched;
	  $secs = $main::secs;
	  $host = $main::host;
	  $line = $main::line;
	  $todo = $main::todo;
	  $creg = $main::creg;

	  # reparse line, so we can use $^, $1, $2, etc. in action field
	  if ($line =~ /$creg/)
	    {
	      eval($todo);
	      warn $@ if $@;
	    }
	  else
	    {
	      warn "Did not match $line, $realdate, $matched, $secs, $host, $todo, $creg\n";
	    }

	  package main;
	}
    }
}


##############################################################################
# reset input file
#
sub reopen
{
  close <LOG>;
  open(LOG,$ARGV[0]) || die "Cannot open file $ARGV[0]\n";
  seek(LOG,0,2) || die "Cannot seek file $ARGV[0]\n";
}


##############################################################################
# Read which handles `tail -f` mode (loops on EOF until input)
#
sub myread
{
  if ($opt_f)
    {
      # This is a stupid way to wait on a file
      sleep 1 until ($_ = <LOG>);
    }
  else
    {
      $_ = <>;
    }
}


##############################################################################
# Test to see if a line matches
#
sub testline
{
  local($secs,$realdate,$host,$line) = @_;

  for ($i=0; $i<$state::num_events; $i++)
    {
      if ($line =~ /($state::EVE_regexp[$i])/)
	{
	  # check if need to ignore events for some time
	  if ($state::EVE_ignore[$i] > 0 && # did we define an ignore period?
	      $state::EVE_ign_time[$i] >= 0 && # -1 means not ignoring now
	      $this_time >= $state::EVE_ign_time[$i])  # no pardoxes please
	    {
	      if (($this_time - $state::EVE_ign_time[$i]) > $state::EVE_ignore[$i])
		{
		  print "\tIGNORE ENDED: \"$1\"\n" if $debug;
		  $state::EVE_ign_time[$i] = -1; # turn of ignoring until next

		  # event gets triggered and
		  # process this event.
		  &process_instance($this_time, $i, $1, $line);
		}
	      else
		{
		  print "\tIGNORED: \"$1\"\n" if $debug;
		}
	      next;		# either way skip
	    }
	  else
	    {
	      print "\tEVENT: \"$1\"\n" if $debug;
	    }

	  &process_instance($this_time, $i, $1, $line);
	}
    }
}


##############################################################################
# Reload the configuration file
#
sub reloadconf
{
  print "Reloading configuration file\n" if ($verbose);
  &unloadpkg("user");
  &unloadpkg("state");
  &defineall;
  &loadconf($conf_file);
}

package unload;
##############################################################################
# Unload a package
#
sub main::unloadpkg
{
  local($package) = @_;
  local($key, $val);
  local(*stab) = eval("*${package}::");
  while (($key,$val) = each(%stab))
    {
      local(*entry) = $val;

      print "Undefining ${package}::${key}\n" if ($main::debug);

      undef $entry;
      undef @entry;
      undef &entry;
      undef %entry;
      $info = <<"END";
      package foo;
      close (\$unload::key);
      closedir (\$unload::key);
      package unload;
END
      eval $info;
    }
}
package main;


##############################################################################
# Load the swatch++ configuration file
#
sub loadconf
{
  local($file) = @_;

  open(CONFFILE, $file) || die "cannot open $file: $!\n";
  while (<CONFFILE>)
    {
      chop;
      next if /^\#/;		# ignore comments
      next if /^\s*$/;		# ignore lines with whitespace

      # Subroutine definitions and other init stuff in user package
      if (/^eval +(.+)$/)
	{
	  $line = $1;
	  package user;
	  eval($main::line);
	  warn "$@ in $main::line" if $@;
	  package main;
	  next;
	}

      $cur_event = -1;
      # Rules lines	
      if (/^\/(.+)\/:(\d+):(\d+):(\d+):(.*)$/)
	{
	  $cur_event=$state::num_events;	# index of new event to insert into
	  $state::num_events++;		# increment events counter
	  $state::EVE_regexp[$cur_event]=$1;
	  $state::EVE_period[$cur_event]=$2;
	  $state::EVE_count[$cur_event]=$3;
	  $state::EVE_ignore[$cur_event]=$4;
	  $state::EVE_ign_time[$cur_event] = -1;
	  $state::EVE_action[$cur_event]=$5;
	  print "REGEXP=\"$state::EVE_regexp[$cur_event]\"\n" if $debug;
	  print "PERIOD=\"$state::EVE_period[$cur_event]\"\n" if $debug;
	  print "COUNT=\"$state::EVE_count[$cur_event]\"\n" if $debug;
	  print "IGNORE=\"$state::EVE_ignore[$cur_event]\"\n" if $debug;
	  print "ACTION=\"$state::EVE_action[$cur_event]\"\n\n" if $debug;
	}
      else
	{
	  die "syntax error on line $. of $conf_file.\n";
	}
    }
  close(CONFFILE);
}


##############################################################################
### BEGIN MAIN CODE
##############################################################################


# test if conf file exists and read it
&defineall;
&loadconf($conf_file);
$SIG{"USR1"} = "reloadconf";

##############################################################################
# main loop

if ($opt_f)
{
  open(LOG,$ARGV[0]) || die "Cannot open file $ARGV[0]\n";
  seek(LOG,0,2) || die "Cannot seek file $ARGV[0]\n";

  # Reopen the file we are following on SIGHUP
  $SIG{"HUP"} = "reopen";
}

while (&myread)
{
  chop;
  # compute date from unix epoch in integer format
  if (/($date_regexp)\s+($host_regexp)\s+/o)
    {
      $realdate = $1;
      $host = $2;
      $this_time = &lstime_to_time($1);
      $secs = $this_time;
      $rest = $_;
      $rest =~ s/$date_regexp\s+//;
      print "\tTIME = $this_time\n" if $debug;
    }
  else
    {
      warn "cannot match a date on line $. --- ignoring!\n";
      next;
    }
  if ($rest =~ /$host\s+last message repeated (\d+) times/)
    {
      $num = $1;
      for($x=0;$x<$num;$x++)
	{
	  &testline($secs,$realdate,$host,$lastmsg{$host});
	}
    }
  else
    {
      $lastmsg{$host} = $rest;
      &testline($secs,$realdate,$host,$rest);
    }
}
