#!/usr/bin/perl -w # Originally written 15 February 1999 by Jim Lippard as short hack script. # Rewritten 25 February 1999 by Jim Lippard to use config file and be # a bit fancier. Early a.m. 26 February: changed some error messages, # fixed uninitialized variable problem in read_size_file. # Modified 11 August 1999 by Jim Lippard to support cyclog-format logs. # A cyclog is a directory containing files named with timestamps. # For cyclogs, we store the time logs were last examined as well as # the size of the last log file examined. We can only detect a # reduction in size (editing) on the last log file examined. # cyclog is part of Daniel Bernstein's daemontools package, and may # be found at ftp://koobera.math.uic.edu/www/daemontools.html # This script now requires Time::HiRes from CPAN. # Modified 26 August 1999 by Jim Lippard to support multilog-format # logs. This is Bernstein's new replacement for cyclog, the format # is very similar. The only real change is when converting the # timestamps. Also modified to sort files within cyclogs/multilogs. # Modified 23 December 1999 by Jim Lippard to change name on email to # "Reporter" and use hostname minus domain name in subject line. # Modified 23 April 2002 by Jim Lippard to correctly parse regexps which # contain colons. # Modified 12 February 2003 by Jim Lippard to allow the use of a single # reportnew.conf file for multiple hosts in a backwards-compatible # way by adding optional begin-host: hostname and end-host: hostname # fields. If master_notify appears outside of any begin-host/end-host # blocks, it is the master_notify for all hosts. # Modified 28 June 2003 by Jim Lippard--there appears to be a bug where # sometimes notifications are sent when the value in the notify hash # is undefined. A workaround has been put in place to use the master_notify # address when an undefined value is sent to the send_notify sub. # Suggested enhancements: # * Allow to run continuously (like swatch) monitoring logs with # select. That will perhaps be more efficient than starting up # a perl process every N minutes, and will catch log changes more # rapidly. It should wait a little bit for additional matching # messages, though, so that it doesn't send a separate message for # each log line. (Easiest way might be to make it go into an # infinite loop, sleeping every N minutes at the end. Though # it would be more efficient to use select.) # * Allow it to execute a program for matching lines, which can # do parsing on the log lines, perform some action, etc. ### Required packages. use strict; use Time::HiRes qw( gettimeofday ); ### Constants. my $HOSTNAME = `hostname`; chop ($HOSTNAME); my $DOMAINNAME = 'discord.org'; my $SECURITY_ADMIN = 'lippard@discord.org'; my $SENDMAIL = '/usr/sbin/sendmail'; my $VERSION = 'reportnew 1.6a of 28 June 2003'; ### Variables. # Filenames. use vars qw($config_file $size_file); # Global variables from config file. use vars qw( $master_notify @logfiles %match_hash %exclude_hash %notify_hash ); # Global variables from size file. use vars qw( %log_size %log_mtime %log_checktime ); # Other global variables. use vars qw( $debug_mode @notify_lines ); # Local variables in main program. my ($logfile, $size, $mtime, @cyclog_files, $cyclog_file, $old_log_checktime, $got_first_cyclog_file); ### Main program. $debug_mode = 0; if ($#ARGV != 0) { die "Usage: reportnew config-file\n"; } $config_file = $ARGV[0]; $size_file = $config_file; # Config file name is file.conf; size file name is file.size. if (substr ($config_file, length ($config_file) - 5, 5) eq '.conf') { $size_file =~ s/\.conf$/.size/; } else { $config_file .= '.conf'; $size_file .= '.size'; } &parse_config ($config_file); &read_size_file ($size_file); foreach $logfile (@logfiles) { undef @notify_lines; if (!-d $logfile) { # Standard syslog file. ($log_size{$logfile}, $log_mtime{$logfile}, $log_checktime{$logfile}) = &check_logfile ($logfile, $log_size{$logfile}, $log_mtime{$logfile}, $log_checktime{$logfile}, $match_hash{$logfile}, $exclude_hash{$logfile}, 0); } else { # cyclog or multilog if (opendir (CYCLOG, $logfile)) { @cyclog_files = grep (!/^\./, readdir (CYCLOG)); closedir (CYCLOG); $got_first_cyclog_file = 0; $old_log_checktime = $log_checktime{$logfile}; foreach $cyclog_file (sort (@cyclog_files)) { next if ($cyclog_file eq 'lock'); # multilog format next if ($cyclog_file eq 'state'); # multilog format $cyclog_file = $logfile . '/' . $cyclog_file; ($size, $mtime) = &read_size ($cyclog_file); next if ($mtime < $old_log_checktime); # If we get here, then we've found the oldest changed file # (since we last checked). $got_first_cyclog_file++; if ($got_first_cyclog_file == 1) { ($log_size{$logfile}, $log_mtime{$logfile}, $log_checktime{$logfile}) = &check_logfile ($cyclog_file, $log_size{$logfile}, $log_mtime{$logfile}, $old_log_checktime, $match_hash{$logfile}, $exclude_hash{$logfile}, 1); } # For all the new files, we don't care about size or mtime. else { ($log_size{$logfile}, $log_mtime{$logfile}, $log_checktime{$logfile}) = &check_logfile ($cyclog_file, 0, 0, 0, $match_hash{$logfile}, $exclude_hash{$logfile}, 1); } } # foreach } # if opendir successful else { push (@notify_lines, "Could not open cyclog/multilog $logfile. $!"); } } if (defined (@notify_lines)) { &send_notify ($HOSTNAME, $logfile, $notify_hash{$logfile}, @notify_lines); } } &write_size_file ($size_file); ### Subroutines. # Subroutine to parse configuration file. sub parse_config { my ($config_file) = @_; my ($directive, $value, $line_num, $current_logfile, $specified_host, $current_host, $all_host_master_notify); $line_num = 0; $specified_host = 0; if (open (CONFIG, $config_file)) { while () { $line_num++; if (!/^#|^$/) { chop; ($directive, $value) = split (/:\s*/, $_, 2); if ($directive eq 'begin-host') { if (defined ($master_notify) && !defined ($current_host)) { $all_host_master_notify = $master_notify; } $current_host = $value; } elsif ($directive eq 'end-host') { if ($current_host ne $value) { &send_error ($config_file, "end-host directive does not match begin-host directive (which uses \"$current_host\") on line $line_num. $value\n"); exit; } if ($current_host eq $HOSTNAME) { return; } $current_host = ""; undef $master_notify; undef @logfiles; undef %match_hash; undef %exclude_hash; undef %notify_hash; } elsif ($directive eq 'master_notify') { if (defined ($master_notify)) { &send_error ($config_file, "Second master_notify directive on line $line_num. $value"); exit; } $master_notify = $value; } elsif ($directive eq 'log') { if ((!defined ($current_host) || $current_host eq $HOSTNAME) && !-e $value) { &send_error ($config_file, "Logfile does not exist in log directive on line $line_num. $value"); exit; } if (grep ($_ eq $value, @logfiles)) { &send_error ($config_file, "Previously defined logfile on line $line_num. $value"); exit; } push (@logfiles, $value); $current_logfile = $value; } elsif ($directive eq 'match') { if (!defined ($current_logfile)) { &send_error ($config_file, "No log directive corresponding to match directive on line $line_num. $_"); exit; } if (defined ($match_hash{$current_logfile})) { &send_error ($config_file, "Too many match directives at line $line_num. $_"); exit; } if (($value ne 'all') && !&valid_regexp ($value)) { &send_error ($config_file, "Invalid match directive on line $line_num. $_"); exit; } $match_hash{$current_logfile} = $value; } elsif ($directive eq 'exclude') { if (!defined ($current_logfile)) { &send_error ($config_file, "No log directive corresponding to exclude directive on line $line_num. $_"); exit; } if (defined ($exclude_hash{$current_logfile})) { &send_error ($config_file, "Too many exclude directives at line $line_num. $_"); exit; } if (($value ne 'none') && !&valid_regexp ($value)) { &send_error ($config_file, "Invalid exclude directive on line $line_num. $_"); } $exclude_hash{$current_logfile} = $value; } elsif ($directive eq 'notify') { if (!defined ($current_logfile)) { &send_error ($config_file, "No log directive corresponding to notify directive on line $line_num. $_"); exit; } if (defined ($notify_hash{$current_logfile})) { &send_error ($config_file, "Too many notify directives at line $line_num."); exit; } $notify_hash{$current_logfile} = $value; } else { &send_error ($config_file, "Unknown directive on line $line_num. $_"); exit; } } } close (CONFIG); } else { &send_error ($config_file, "Cannot open config file. $!"); exit; } if (!defined ($master_notify)) { if (defined ($all_host_master_notify)) { $master_notify = $all_host_master_notify; } else { $master_notify = $SECURITY_ADMIN; } } foreach $current_logfile (@logfiles) { if (!defined ($match_hash{$current_logfile})) { $match_hash{$current_logfile} = 'all'; } if (!defined ($exclude_hash{$current_logfile})) { $exclude_hash{$current_logfile} = 'none'; } if (!defined ($notify_hash{$current_logfile})) { $notify_hash{$current_logfile} = $master_notify; } } } # Look through a log file for any changed lines; add them to # the global variable @notify_lines. sub check_logfile { my ($logfile, $old_size, $old_mtime, $old_checktime, $match, $exclude, $cyclog) = @_; my ($size, $mtime, $checktime, $date, $line); ($size, $mtime) = &read_size ($logfile); # If the log has changed, gotten smaller, but isn't a new log file, # then that's something we need to complain about. Unfortunately, # we don't have a way to look at file creation time. Do we? # If the shrinkage is down to 1024 bytes or less, we assume it was # a log rotation. if (($mtime > $old_mtime) && ($size < $old_size) && ($size > 1024)) { $old_size = 0; push (@notify_lines, "Logfile has shrunk in size, but is larger than 1024 bytes."); $checktime = gettimeofday(); return ($size, $mtime, $checktime); } # Logfile has grown. if ($size > $old_size) { if (open (LOG, $logfile)) { seek (LOG, $old_size, 0); while () { chop; if ((($match eq 'all') || (&match_line ($match, $_))) && (($exclude eq 'none') || (!&match_line ($exclude, $_)))) { if ($cyclog) { # cyclog or multilog ($date, $line) = split (/\s+/, $_, 2); if ((substr ($date, 0, 1) eq '@') && (length ($date) == 15)) { # cyclog $date = localtime ($date); push (@notify_lines, $date . ' ' . $line); } elsif ((substr ($date, 0, 1) eq '@') && (length ($date) == 25) && (-e"/usr/local/bin/tai64nlocal")) { # multilog open (TAICONVERT, "/bin/echo \"$date\" | /usr/local/bin/tai64nlocal|"); $date = ; close (TAICONVERT); chop ($date); push (@notify_lines, $date . ' ' . $line); } else { # unknown, leave it alone push (@notify_lines, $_); } } else { # syslog push (@notify_lines, $_); } } } close (LOG); $checktime = gettimeofday(); return ($size, $mtime, $checktime); } else { push (@notify_lines, "Cannot open log file. $!"); } } $checktime = gettimeofday(); return ($size, $mtime, $checktime); } # Return 1 if a regexp is valid, 0 if not. sub valid_regexp { my ($regexp) = @_; if (($regexp =~ /\/.*\//) || ($regexp =~ /!\/.*\//)) { return 1; } else { return 0; } } # Read the contents of the size file. sub read_size_file { my ($size_file) = @_; my ($logfile, $size, $mtime, $checktime); if (!-e $size_file) { foreach $logfile (@logfiles) { $log_size{$logfile} = 0; $log_mtime{$logfile} = 0; $log_checktime{$logfile} = 0; } } else { if (open (SIZEFILE, $size_file)) { while () { chop; ($logfile, $size, $mtime, $checktime) = split (/,/); if (!grep ($_ eq $logfile, @logfiles)) { &send_notify ($HOSTNAME, $config_file, $master_notify, "Logfile $logfile in size file is not in config file."); } $log_size{$logfile} = $size; $log_mtime{$logfile} = $mtime; $log_checktime{$logfile} = $checktime; } close (SIZEFILE); } else { &send_error ($size_file, "Cannot open size file. $!"); exit; } foreach $logfile (@logfiles) { if (!defined ($log_size{$logfile})) { &send_notify ($HOSTNAME, $size_file, $master_notify, "Logfile $logfile in config file is not in size file."); $log_size{$logfile} = 0; $log_mtime{$logfile} = 0; $log_checktime{$logfile} = 0; } } } } # Read current log file size and mtime. sub read_size { my ($logfile) = @_; my ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks); ($dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size, $atime, $mtime, $ctime, $blksize, $blocks) = stat $logfile; $size = 0 if (!defined ($size)); $mtime = 0 if (!defined ($mtime)); return ($size, $mtime); } # Return 1 if line matches regexp, 0 if not. sub match_line { my ($regexp, $line) = @_; my ($negative); if (substr ($regexp, 0, 1) eq '!') { $negative = 1; $regexp =~ s/^!//; } else { $negative = 0; } $regexp =~ s/^\///; $regexp =~ s/\/$//; if (($negative && ($line =~ !/$regexp/)) || (!$negative && ($line =~ /$regexp/))) { return 1; } else { return 0; } } # Send a mail notification. sub send_notify { my ($hostname, $logfile, $notify_list, @lines) = @_; my ($line); $hostname =~ s/\.$DOMAINNAME$//; if (!defined ($notify_list)) { $notify_list = $master_notify; } if ($debug_mode) { print "$hostname: $logfile ($notify_list)\n"; foreach $line (@lines) { print "$line\n"; } } else { open (MAIL, "|$SENDMAIL -t"); print MAIL "From: Reporter \n"; print MAIL "To: $notify_list\n"; print MAIL "Subject: $hostname $logfile\n\n"; foreach $line (@lines) { print MAIL "$line\n"; } close (MAIL); } } # Send error notification. sub send_error { my ($filename, $error) = @_; if (defined ($master_notify)) { &send_notify ($HOSTNAME, $filename, $master_notify, $error); } else { &send_notify ($HOSTNAME, $filename, $SECURITY_ADMIN, $error); } } # Write out size file. sub write_size_file { my ($size_file) = @_; my ($logfile); if (open (SIZEFILE, ">$size_file")) { foreach $logfile (@logfiles) { print SIZEFILE "$logfile,$log_size{$logfile},$log_mtime{$logfile},$log_checktime{$logfile}\n"; } close (SIZEFILE); } else { &send_notify ($HOSTNAME, $size_file, $master_notify, "Cannot open size file for writing. $!"); } }