#!/usr/pkg/bin/perl

# $Header: /home/vikas/src/nocol/perlnocol/RCS/syslogmon,v 1.3 1998/08/12 15:54:29 vikas Exp $
#
#	syslogmon - NOCOL perl monitor for syslogs
#
#	Copyright 1997. Netplex Technologies Inc.  vikas@navya.com
#
#
# syslogmon reads various log files specified in the config file and
# parses them for the regular expression specified in the config file.
# Each regular expression has a matching 'variable' name (this can be
# arbitrary). If the regular expression is matched, an event is put
# in the Nocol format datafile, with the variable name as specified
# in the config file. The 'address' field of the EVENT structure is
# set to the regular expression match. If the regular expression has
# a '()', then the 'address' field is set to the pattern matching the
# regexp within the '()' ($1 in PERL style regular expressions).
#
# To avoid too many messages being logged, if the variable name is
# matches an already existing entry in the NOCOL data file, then the
# entry is not-logged. After every 30 minutes, all old entries in
# the data file are expired.
#
# The LOG files(/var/log/messages, /var/log/maillog etc.) have to be in a 
# fixed format i.e. the first five fields have to be Month, Date, hh:mm:dd,
# host, and logger name. Whatever after these five fields is searched to
# match REGEXP(#4).
#
# Set the nocol host as the main syslog host by putting the following
# line in your /etc/syslog.conf file (or its equivalent):
#	*.notice;kern.debug;lpr,auth.info;mail.crit	@nocol.your.domain
#
###################
# PROGRAM LOGIC : #
###################
#
# 1) Reading the config file :
# ============================
#    #1        #2               #3              #4
#    Diskerr   *	        warning		diskfull\s*(/var\S*)
#
#    - Read each line and break them into four fields.
#    - create regexp[] array from the #4 field
#    - initialize isdone array.  This array is used to prevent logging
#      same type of event more than once.
#
# 2) Expiring Events :
# ====================
#    - Open syslogmon-output using two file-handles for reading and for writing
#      (IEVENTS for reading and OEVENTS for writing).
#    - Read event by event from IEVENTS
#    - for each event read, if event timestamp is older than current time +
#      expiretime then dont write the event back, else write to OEVENTS.
#      Reset $isdone to zero
#    - At end, truncate file to the length obtained from tell(OEVENTS).
#
# 3) Logging Events to syslogmon-output :
# =======================================
#    - Do Expire-Events
#    - For each log file given in the config file get the file size
#      - If the log file size is equal to the last saved offset then skip
#        this file.
#      - open the log file
#      - else if the size has shrunk(probably the file was expunged) or
#        the last saved offset is zero then
#        - position the file pointer to the end of the file
#      - else position the file pointer to the last saved offset.
# 
#      - retrieve each line from the log file and break them into 5 fields i.e.
#        Month Date time     host     rest till "\n" .............
#        Feb   11   17:43:15 freebird /kernel: diskfull  /var/tmp/junk
#
#      - go through the regexp[] array
#        - if the regexp matches the last field i.e. rest and host match then
#          - intiliaze and log the event to output file and noclogd daemon.
#          - set the isdone flag so that we dont log this event again till
#            the expire time is over.
#     NOTE THAT THIS PROGRAM ESCALATES EVENTS DIRECTLY TO THEIR LISTED
#     SEVERITY AND DOES NOT STEP THROUGH THE VARIOUS LEVELS.
#
########################
# Sample config line : #
########################
#
# Var(#1)   Site(#2)	Severity(#3)	Reg Exp(#4)
#####################################################
# Probe	    *	        info		probing
# Diskerr   *	        warning		diskfull\s*(/var\S*)
#
# When displaying EXP(#4) within parenthesis if any will be displayed
# else the whole regular exp(#4) will be displayed.
#
# Nocol event elements used:
#   sender                     "syslogmon"
#   severity                   "info", "error", "warning" or "critical"
#   site
#    name                      host
#    addr                      contains regexp
#   var                       
#    name                      "Diskerr", "ParityError" etc from config file
#    value                     1 means at Info level
#    threshold                 as read from the config file
#    units                     always "Log"
#
## 
##
#
#
############################
## Variables customization #  overrides values in the nocollib.pl library
############################
$sleepint=60;       			# Seconds to sleep between tries.
$sender="syslogmon";
$varname = "UNINIT";
$varunits = "LogMesg";
############################
$debug = 0;				# set to 1 for debugging output
$libdebug = 0;				# set to 1 for debugging output

require  "nocollib.pl" ;

$expiretime = (30*60); # time for which an entry remains in datafile

##
# Read the config file. Use '\t' as a separator for 'item'
sub readconf {
    local($startlogfiles) = 0;
    open(CONFIG,"<$cfile")||die("Couldn't find $cfile, exiting");
    while(<CONFIG>)
    {
	chop;
	if( /^\s*\#/ || /^\s*$/ ) {next;} # skip comments & blank lines

	if ( !$startlogfiles && /^LOGFILES/ ) { 
	    $startlogfiles = 1; next ;  # all other lines are hostnames
	}
	
	if ($startlogfiles) {
	    if ( /^\s*(\S+)\s*.*$/ ) { 
		push (@logfiles, $1); 
		$offset{$1} = 0;
	    }
	    else { print STDERR "Illegal log file line $_, skipping\n" ;}
	    next ;
	}
	# VAR HOST SEVERITY REGEXP
	if (/\s*(\S+)\s+(\S+)\s+(\S+)\s*(.*)\s*$/)
	{
	    $item = $1 ;	 # the VAR name
	    local ($re) = $2 ;	# temp variable
	    if ($2 eq '*' || $2 eq '+') { $re = '.+' } ; # convert '*' => '.+'
	    $site{$item} = $re;
	    $level{$item} = $3;  # level
	    $regexp{$item} = $4; # regexp for searching log lines
	    $isdone{$item} = 0;  # init to zero
	    push(@items,$item);
	} else { print STDERR "Ignoring illegal line: $_\n";}

    }	# end while(CONFIG)

    close(CONFIG);
    if(0>$#items){die("Nothing to monitor in $cfile, exiting")};
    if ($debug) {
	print "Items are:\n"; foreach (@items) { print "\t$_\n" } ;
	print "Logfiles :\n"; foreach (@logfiles) { print "\t$_\n"};
    }
}				# end: readconf


## dotest()
#
# Calls validate_datafile() to expire old events, logs new events to
# NOCOL datafile.
#
# Sample log message :
#
# $1  $2 <--$3--> <--$4--> <------------- $5 -------------------------->
##########################################################################
# Feb 11 17:40:22 freebird /kernel: probing for devices on the PCI bus:
# Feb 11 17:43:15 freebird /kernel: real memory  = 33554432 (32768K bytes)
# Feb 11 17:43:15 freebird /kernel: diskfull  /var/tmp/junk
#
sub dotest {
    local($i) = $sender;          # index to writeevent and readevent funcs
    
    # expire old data in NOCOL data file
    &expire_datafile_events();

    # open the data file for writing to data/syslogmon-output
    open(OEVENTS, ">> $datafile") || die("Error opening file : $datafile\n");
    foreach $logfile (@logfiles) # for all log files given in config file
    {
	local ($size) = (-s "$logfile"); # returns file size
	#print STDERR "Opening log file : $logfile,  Size : $size\n";

	# if log file size same then dont read log file.  If log file size
	# is < $offset{$logfile} that means the log file was probably expunged
	if ($size == $offset{$logfile}) { next;	}
	elsif ($size < $offset{$logfile} || $offset{$logfile} == 0) {
	    $offset{$logfile} = $size;  # point to EOF
	}
	# open the log file for reading
	open(LOGFILE, "< $logfile") || die("Invalid file : $logfile\n");

	# move the file pointer to last read + 1 or EOF
	seek(LOGFILE, $offset{$logfile}, 0) || die("dotest: Could not seek");
	while(<LOGFILE>) # read a single line
	{
	    local($line) = $_;
	    (/\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)\s*$/);
	    $Month = $1; $Date = $2; $time = $3; $host = $4; $rest = $5;
	    
	    # check if read data matches regexp
	    foreach $item (@items)	# the VAR name
	    {
		local($site) = $site{$item};
		local($regexp) = $regexp{$item}; # to store retrieved reg exp
		#sleep(1);
		if ($host =~ m#$site#i && $rest =~ m#$regexp#i)
		{ # if host and reg exp match
		    # $1 is taken from the reg exp
		    if ($1) {$regexp = $1;}

		    # if this event type already logged then skip
		    if ($isdone{$item} == 1) { 
			if ($debug > 2) {
			    print "(dotest)Event already logged, \'$item\'\n";
			}
			last; # skip foreach ($item)
		    }
		    else { $isdone{$item} = 1;} # set the flag
		    # $debug && print "Logging Line : $line, Item : $item\n";
		    
		    # init event and fill values. The parsed 'regexp' is the
		    # address field.
		    &init_event($host, $regexp , $i);
		    $varname{$i} = $item;

		    # The event gets written directly at the level instead
		    # of escalating gradually like typical nocol monitors
		    if ($level{$item} =~ /info/i)
		    { $severity{$i} = $loglevel{$i} = $E_INFO; }
		    elsif ($level{$item} =~ /error/i)
		    { $severity{$i} = $loglevel{$i} = $E_ERROR; }
		    elsif ($level{$item} =~ /warning/i)
		    { $severity{$i} = $loglevel{$i} = $E_WARNING; }
		    else { $severity{$i} = $loglevel{$i} = $E_CRITICAL;}

		    $nocop{$i} = $n_DOWN;
		    &writeevent(OEVENTS, $i); # write event to datafile
		    &eventlog(&packevent($i)); # log event to noclogd
		    last;
		}  # if (host and regexp match)
	    } # end of foreach (item)
	} # end of while(<LOGFILE>)

	$offset{$logfile} = tell(LOGFILE); 	# for next time
	close(LOGFILE);
    } # end of foreach(@logfiles)
    close(OEVENTS);
}                   # end of function dotest()


## expire_datafile_events
#
# Checks if the events in the datafile are older than $expiretime.
# If so, delete them from the datafile (this is to prevent old events
# from sitting around in the NOCOL datafile forever).
# 
sub expire_datafile_events {
    local($i) = $sender;
    if (! -e "$datafile") {return;} # if no file then return

    # for reading the NOCOL datafile
    open(IEVENTS, "< $datafile") || die("Error opening file : $datafile");
    # dont overwrite the NOCOL datafile
    open(OEVENTS, "+< $datafile") || die("Error opening file : $datafile");
    seek(OEVENTS, 0, 0) || die("Could not seek\n"); # seek to beginning of file
    local($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
	localtime(time);
    local($curtime) = ($hour*60*60) + ($min*60) + $sec; # convert to seconds

    # read each event structure from the NOCOL datafile
    while ( &readevent(IEVENTS, $i)) # readevent returns no of bytes read
    {
	local($evttime) = $hour{$i}*60*60 + $min{$i}*60; # event time in secs
	local($item) = $varname{$i};

	# if next date then add 24*60 to curtime
	if ($curtime < $evttime) { $curtime = $curtime + (24*60*60); }

	# if event expired then dont write it back.
	if ($curtime > ($evttime + $expiretime)) {
	    $isdone{$item} = 0;		# reset
	    if ($debug > 2) {
		print STDERR "(expire_ev) Expiring Variable: \'$item\'\n";
	    }
	    next;
	}

	&writeevent(OEVENTS, $i); # write the event back.
    }
    close(IEVENTS); # input stream
    truncate(OEVENTS, tell(OEVENTS)); # truncate file if some events expired
    close(OEVENTS); # output stream

} # end of function expire_datafile_events()


###
### main
###

# fork a daemon and read config file
&nocol_startup; # creates daemon
&readconf;  # done only once

# Loop forever.
#
local ($stime, $deltatime);	# outside while() to prevent memory leaks ?
while (1)			# forever...
{
    $stime = time;          # time starting tests
    &dotest;               # do the test etc
    $deltatime = time - $stime;              # time to do tests
    $debug && printf STDERR "(dbg) sleep for= %d\n", $sleepint - $deltatime;
    if ($sleepint > $deltatime) { sleep(($sleepint - $deltatime)) };
    if ($debug > 2) {
	print "Items are:\n";
	foreach (keys %isdone) { print "\t\'$_\' $isdone{$_}\n"; } 
    }

}	# end: while(forever) 

