#!/usr/bin/perl

use strict;

use Config::General qw(ParseConfig);
use Getopt::Long;
use Data::Dumper;
use IO::File;
use DBI;
use Date::Manip;
use Mail::SendEasy;

my %dateformats = (
  'kern' => \&parse_kern_log_date
);

my %actions = (
  'email' => \&email
);

my %options;
my %config;

GetOptions(
  "c|config-file=s" => \$options{"config-file"},
  "d|dry-run"       => \$options{"dry-run"},
  "n|no-actions"    => \$options{"no-actions"}
);

die "Please specify a valid configuration file" if (! defined $options{"config-file"});

%config = ParseConfig(
  -ConfigFile => $options{"config-file"},
  -LowerCaseNames => 1,
  -UseApacheInclude => 1,
  -IncludeDirectories => 1,
  -IncludeRelative => 1
);

#
# Perform some configuration sanity checking and pre-processing.
#

foreach my $filename (keys %{ $config{logfile} })
{
  my $entry = $config{logfile}->{$filename};

  #
  # Build the date extraction function for this file.
  #

  if ($entry->{dateformat})
  {
    my $fmt = $entry->{dateformat};

    $entry->{datefunc} = $dateformats{$fmt} || die "Invalid date format: $fmt";
  }
  elsif ($entry->{datepattern})
  {
    my $pat = qr/$entry->{datepattern}/;

    $entry->{datefunc} = sub { $_[0] =~ $pat; return $1 };
  }
  else
  {
    die "Trigger for $filename is missing a DatePattern or DateFormat spec.";
  }

  #
  # Now, for each of the triggers, build a match function.  Right now,
  # that means building a function which does a pattern match on an input
  # line.  Some day it might do more.
  #

  my @triggers = (ref($entry->{trigger}) eq "ARRAY") ? @{ $entry->{trigger} } : ($entry->{trigger});

  foreach my $trigger (@triggers)
  {
    my $pat = qr/$trigger->{pattern}/;

    $trigger->{matchfunc} = sub { $_[0] =~ $pat; return $1 };

    if (! $options{"dry-run"} && ! $options{"no-actions"})
    {
      $trigger->{action} = $actions{$trigger->{action}};
    }
    else
    {
      my $act = $trigger->{action};

      $trigger->{action} = sub { dryrun($act, @_) };
    }
  }

  # Reset this so it's always an array... more convenient for later.

  $entry->{trigger} = \@triggers;
}

#
# First, initialize our database if necessary.
#

my $dbname = $config{databasepath};

if (! -f $dbname)
{
  my $sqlite3 = new IO::File("|sqlite3 '$dbname'");
  my $line;

  while (defined ($line = <DATA>))
  {
    print $sqlite3 $line;
  }

  print $sqlite3 ".q\n";
  close $sqlite3;
}

#
# Now, connect to the database and let's process the logfiles. Now, connect to the database and 
#

my $dbh = DBI->connect("dbi:SQLite:dbname=$dbname","","") || die "Error opening DB: $dbname";

foreach my $filename (keys %{ $config{logfile} })
{
  my $entry = $config{logfile}->{$filename};
  my ($sth, $rv);
  my $logfileid;

  #
  # Now, try to find the log file in the database.  If it doesn't exist, create it.
  #

  $sth = $dbh->prepare("select id from logfiles where name = ?");
  $sth->execute($filename);
  ($logfileid) = $sth->fetchrow_array();

  if ((! defined $logfileid) && (! defined $options{"dry-run"}))
  {
    $sth = $dbh->prepare("insert into logfiles (name) values (?)");
    $rv = $sth->execute($filename);

    die "Couldn't add log file: " . $dbh->errstr if ($rv != 1);

    $sth = $dbh->prepare("select id from logfiles where name = ?");
    $sth->execute($filename);
    ($logfileid) = $sth->fetchrow_array();
  }

  #
  # Now open the logfile.  We can open it two ways, either by tailing the file,
  # or just opening it and processing it from top to bottom.  Which to choose
  # depends on the size of the file.
  #

  my $logfh;

  if (defined $entry->{tail})
  {
    my $count = $entry->{tail};

    $logfh = new IO::File("tail $count '$filename'|");
  }
  else
  {
    $logfh = new IO::File("<$filename");
  }

  #
  # Alright, now process the log file.  We do it in an eval block so that we
  # can catch any errors and report them.
  #

  eval 
  {
    my $linenum = 0;

    while (my $line = <$logfh>)
    {
      chomp $line;
      $linenum++;

      foreach my $trigger (@{ $entry->{trigger} })
      {
        # Alright, first things first, try matching against the line.

        my $action = $trigger->{action};
        my $severity = $trigger->{severity};
        my $match = $trigger->{matchfunc}->($line);

        next if (! defined $match);

        # We have a match, get the date for the entry.

	my $datestr = $entry->{datefunc}->($line);

	if (! defined $datestr)
	{
	  print STDERR "Unable to match date for event at $filename:$linenum\n";

	  next if (! defined $datestr);
	}

        # Now, check if this log record is already in our database.  If it
        # is, we'll just ignore it.

        my $date = UnixDate($datestr, '%Y-%m-%d %H:%M:%S');

        $sth = $dbh->prepare("select count(1) from events where logfileid = ? and date = ? and text = ?");
        $sth->execute($logfileid, $date, $match); 

        my ($count) = $sth->fetchrow_array;

        next if ($count > 0);

        # Got a new record, let's get the message text and trigger off the action.

        my $msgtext = $match;

        if ((defined $trigger->{run}) && 
            (! defined $options{"dry-run"}) && 
            (! defined $options{"no-actions"}))
        {
          $msgtext .= "\n\n---\n\n";
          $msgtext .= `$trigger->{run}`;
        }

        $action->($filename, $date, $line, $msgtext, $severity);

        # Alright, now record the new entry in our database.

        if (! $options{"dry-run"})
        {
          $sth = $dbh->prepare("insert into events (logfileid, date, text, severity) values (?, ?, ?, ?)");
          $rv = $sth->execute($logfileid, $date, $match, $severity);

          die "Couldn't insert event: " . $dbh->errstr if ($rv != 1);
        }
      }
    }
  };

  if ($@)
  {
    print STDERR $@;

    if ($config{emailonerror})
    {
      mail($filename, '', '', "pwatch - exception during analysis:\n\n$@");
    }
  } 
}

exit(0);

sub email
{
  my ($filename, $date, $line, $capture, $severity) = @_;
  my $mail = new Mail::SendEasy(smtp => $config{smtpserver});
  my $status = $mail->send(
    from => $config{emailfrom},
    from_title => $config{emailtitle},
    to => $config{emailto},
    subject => "pwatch - $severity in $filename on $date",
    msg => $capture
  );
}

sub dryrun
{
  my ($action, $filename, $date, $line, $capture, $severity) = @_;

  print "$action -> $filename, $date, $capture, $severity\n";
}

sub parse_kern_log_date
{
  my $line = shift;
  my $now = ParseDate("now");
  my @nowparts = localtime(time());
  my $year = $nowparts[5] + 1900;

  $line =~ /^(...) (\d+) (\d+:\d+:\d+)/;

  my $t = ParseDate(join(" ", $year, $1, $2, $3));

  $t = ParseDate(join(" ", $year - 1, $1, $2, $3)) if ($t > $now);

  return $t;
}

__DATA__

create table logfiles (
  id integer primary key autoincrement,
  name text
);

create table events (
  id integer primary key autoincrement,
  logfileid integer,
  date date,
  text text,
  severity text
);
