#!/usr/bin/perl -w
# vim: set cindent expandtab ts=4 sw=4:

# qmonitor -- an rrdtool enabled postfix queue monitor
# copyright (c) 1998-2006 He zhiqiang <hzqbbc@hzqbbc.com>
# released under the GPL v2

use RRDs;
use strict;
no strict qw(subs refs);
use Getopt::Long;
use POSIX 'setsid';

my $VERSION = "1.02_ext";

my $rrdstep = 60;
my $xpoints = 540;
my $points_per_sample = 3;

my $daemon_logfile = '/var/log/qmonitor.log';
my $daemon_pidfile = '/var/run/qmonitor.pid';
my $daemon_rrd_dir = '/var/lib';
my $rrd_queue = "$daemon_rrd_dir/mailgraph_queue.rrd";

my $this_minute;
my $rrd_inited=0;
my %opt = ();

use vars qw(%q %qm @queue $spool);
@queue = qw(incoming maildrop active deferred hold);
$spool = '/var/spool/postfix';

sub usage
{
    print "usage: qmonitor [*options*]\n\n";
    print "  -h, --help            display this help and exit\n";
    print "  -v, --verbose         be verbose about what you do\n";
    print "  -V, --version         output version information and exit\n";
    print "  -c, --cat             qmonitor only output queue stat and not monitored\n";
    print "  -d, --daemon          start in the background\n";
    print "  --daemon-pid=FILE     write PID to FILE instead of /var/run/qmonitor.pid\n";
    print "  --daemon-rrd=DIR      write RRDs to DIR instead of /var/log\n";
    print "  --daemon-log=FILE     write log to FILE instead of /var/log/qmonitor.log\n";
    print "  --disable-queue-rrd   don't update the queue rrd\n";
    print "  --rrd-name=NAME       use NAME_queue.rrd for the rrd files\n";
    print "  -f, --freq            frequency to check queue (default 0.05 s)\n";

    exit;
}

sub main
{
    Getopt::Long::Configure('no_ignore_case');
    GetOptions(\%opt, 'help|h', 'cat|c', 'version|V',
        'verbose|v+', 'daemon|d!', 'daemon_pid|daemon-pid=s',
        'daemon_rrd|daemon-rrd=s', 'daemon_log|daemon-log=s',
        'disable-queue-rrd','rrd_name|rrd-name=s','freq|f=i',
        ) or exit(1);
    usage if $opt{help};

    if($opt{version}) {
        print "qmonitor $VERSION by hzqbbc\@hzqbbc.com\n";
        exit;
    }

    $daemon_pidfile = $opt{daemon_pid} if defined $opt{daemon_pid};
    $daemon_logfile = $opt{daemon_log} if defined $opt{daemon_log};
    $daemon_rrd_dir = $opt{daemon_rrd} if defined $opt{daemon_rrd};
    $rrd_queue      = $opt{rrd_name}."_queue.rrd" if defined $opt{rrd_name};
    my $sleep       = $opt{freq} || $opt{f} || 0.05;

    if($opt{daemon} or $opt{daemon_rrd}) {
        chdir $daemon_rrd_dir or die "mailgraph: can't chdir to $daemon_rrd_dir: $!";
        -w $daemon_rrd_dir or die "mailgraph: can't write to $daemon_rrd_dir\n";
    }

    daemonize() if $opt{daemon};

    for (@queue) {
        $q{$_} = "$spool/$_"; # initialize
        my @dirs = init_dir($q{$_});
        unshift @dirs, { $q{$_} => (stat $q{$_})[9] };
        $qm{$_} = {
            LIST => \@dirs,
            QSIZE => scalar scan_dir($q{$_}),
        }
    }

    init_rrd(time);

    # the accuracy is 1 second, because the return value of (stat $dir)[9]
    # is epoch second, and can't return the micro second unit :(
    while (1) {
        my $change = 0;
        for (@queue) {
            if (dirchange($_)) {
                print "dir $_ changed!\n" if ($opt{verbose});
                $change = 1;
            }
        }
        if ($change) {
            update(time);
        }
        usleep($sleep);
    }
}

sub dirchange {
    my $type = shift;

    my $arr = $qm{$type}->{LIST};
    for (my $i=0; $i<scalar @$arr;$i++) {
        my $e = $arr->[$i];
        my $dir = (keys %$e)[0];
        my $mtime = $e->{$dir};
        my $now = (stat $dir)[9];
        next if ($now == $mtime);

        $qm{$type}->{QSIZE} = scalar scan_dir($q{$type});
        $qm{$type}->{LIST}->[$i]->{$dir} = $now;
        return 1;
    }
    0;
}

sub usleep {
    my $t = shift;
    select(undef, undef, undef, $t);
}

sub daemonize()
{
    open STDIN, '/dev/null' or die "mailgraph: can't read /dev/null: $!";
    if($opt{verbose}) {
        open STDOUT, ">>$daemon_logfile"
            or die "mailgraph: can't write to $daemon_logfile: $!";
    }
    else {
        open STDOUT, '>/dev/null'
            or die "mailgraph: can't write to /dev/null: $!";
    }
    defined(my $pid = fork) or die "mailgraph: can't fork: $!";
    if($pid) {
        # parent
        open PIDFILE, ">$daemon_pidfile"
            or die "mailgraph: can't write to $daemon_pidfile: $!\n";
        print PIDFILE "$pid\n";
        close(PIDFILE);
        exit;
    }
    # child
    setsid            or die "mailgraph: can't start a new session: $!";
    open STDERR, '>&STDOUT' or die "mailgraph: can't dup stdout: $!";
}

sub init_rrd($)
{
    my $m = shift;
    my $rows = $xpoints/$points_per_sample;
    my $realrows = int($rows*1.1); # ensure that the full range is covered
    my $day_steps = int(3600*24 / ($rrdstep*$rows));
    # use multiples, otherwise rrdtool could choose the wrong RRA
    my $week_steps = $day_steps*7;
    my $month_steps = $week_steps*5;
    my $year_steps = $month_steps*12;

    if(! -f $rrd_queue and ! $opt{'disable-queue-rrd'}) {
        RRDs::create($rrd_queue, '--start', $m, '--step', $rrdstep,
                'DS:incoming:ABSOLUTE:'.($rrdstep*2).':0:U',
                'DS:maildrop:ABSOLUTE:'.($rrdstep*2).':0:U',
                'DS:active:ABSOLUTE:'.($rrdstep*2).':0:U',
                'DS:deferred:ABSOLUTE:'.($rrdstep*2).':0:U',
                'DS:hold:ABSOLUTE:'.($rrdstep*2).':0:U',
                "RRA:AVERAGE:0.5:$day_steps:$realrows",   # day
                "RRA:AVERAGE:0.5:$week_steps:$realrows",  # week
                "RRA:AVERAGE:0.5:$month_steps:$realrows", # month
                "RRA:AVERAGE:0.5:$year_steps:$realrows",  # year
                "RRA:MAX:0.5:$day_steps:$realrows",   # day
                "RRA:MAX:0.5:$week_steps:$realrows",  # week
                "RRA:MAX:0.5:$month_steps:$realrows", # month
                "RRA:MAX:0.5:$year_steps:$realrows",  # year
                );
        my $ERR=RRDs::error;
        print "ERROR: $ERR\n" if $ERR;
        $this_minute = $m;
    }
    elsif(-f $rrd_queue) {
        $this_minute = RRDs::last($rrd_queue) + $rrdstep;
    }

    $rrd_inited=1;
}

sub init_dir {
    my $dir = shift;
    opendir DIR, $dir or die "$!\n";
    my @dirs;
    for (readdir DIR) {
        next if (/^\./);
        next if (!-d "$dir/$_");
        push @dirs, { "$dir/$_" => (stat "$dir/$_")[9] };
        push @dirs, init_dir("$dir/$_");
    }
    closedir DIR;
    @dirs;
}

sub scan_dir {
    my $dir = shift;
    opendir DIR, $dir or die "$!\n";
    my @files ;
    for (readdir DIR) {
        next if (/^\./);
        if (-d "$dir/$_") {
            push @files, scan_dir("$dir/$_");
        } else {
            # my $mode = sprintf("%04o",(stat "$dir/$_")[2] & 07777);
            push @files, $_;
        }
    }
    closedir DIR;
    return @files;
}

# returns 1 if $sum should be updated
sub update($)
{
    my $t = shift;
    my $mt = $t - $t%$rrdstep;
    init_rrd($mt) unless $rrd_inited;
    return 1 if $mt == $this_minute;
    return 0 if $mt < $this_minute;

    my $a = $qm{active}->{QSIZE} ||0;
    my $d = $qm{deferred}->{QSIZE} ||0;
    my $i = $qm{incoming}->{QSIZE} ||0;
    my $m = $qm{maildrop}->{QSIZE} ||0;
    my $h = $qm{hold}->{QSIZE} ||0;

    print "update $this_minute:$i:$m:$a:$d:$h\n" if $opt{verbose};
    RRDs::update $rrd_queue, "$this_minute:$i:$m:$a:$d:$h" unless $opt{'disable-queue-rrd'};
    if ($mt > $this_minute+$rrdstep) {
        for (my $sm=$this_minute+$rrdstep;$sm<$mt;$sm+=$rrdstep) {
            print "update $sm:$i:$m:$a:$d:$h (SKIP)\n" if $opt{verbose};
            RRDs::update $rrd_queue, "$sm:$i:$m:$a:$d:$h" unless $opt{'disable-queue-rrd'};
        }
    }
    $this_minute = $mt;
}

main;


syntax highlighted by Code2HTML, v. 0.9.1