couriergraph/0000755000000000000000000000000010370400107012236 5ustar rootrootcouriergraph/couriergraph.cgi0000755000175700017570000001320210370400100016166 0ustar hildebhildeb#!/usr/bin/perl -w # couriergraph -- a postfix statistics rrdtool frontend # copyright (c) 2002-2006 Ralf Hildebrandt # based upon mailgraph, which is # mailgraph -- a postfix statistics rrdtool frontend # copyright (c) 2000-2006 David Schweikert # released under the GNU General Public License use RRDs; use POSIX qw(uname); my $VERSION = "1.10stack"; my $host = (POSIX::uname())[1]; my $scriptname = 'couriergraph.cgi'; my $xpoints = 800; my $points_per_sample = 3; my $ypoints = 160; my $ypoints_err = 96; my $rrd = '/etc/postfix/couriergraph.rrd'; # path to where the RRD database is my $tmp_dir = '/tmp/couriergraph'; # temporary directory where to store the images my @graphs = ( { title => 'Day Graphs', seconds => 3600*24, }, { title => 'Week Graphs', seconds => 3600*24*7, }, { title => 'Month Graphs', seconds => 3600*24*31, }, { title => 'Year Graphs', seconds => 3600*24*365, }, ); sub graph($$$) { my $range = shift; my $file = shift; my $title = shift; my $step = $range*$points_per_sample/$xpoints; my $date = localtime(time); $date =~ s|:|\\:|g unless $rrdtool_1_0; my ($graphret,$xs,$ys) = RRDs::graph($file, '--imgformat', 'PNG', '--width', $xpoints, '--height', $ypoints, '--start', "-$range", '--vertical-label', 'logins/min', '--lower-limit', 0, '--units-exponent', 0, # don't show milli-messages/s '--lazy', '--color', 'SHADEA#ffffff', '--color', 'SHADEB#ffffff', '--color', 'BACK#ffffff', $RRDs::VERSION < 1.2002 ? () : ( '--slope-mode' ), "DEF:pop3d_login=$rrd:pop3d_login:AVERAGE", "DEF:mpop3d_login=$rrd:pop3d_login:MAX", "DEF:pop3d_ssl_login=$rrd:pop3d_ssl_login:AVERAGE", "DEF:mpop3d_ssl_login=$rrd:pop3d_ssl_login:MAX", "CDEF:rpop3d_login=pop3d_login,60,*", "CDEF:vpop3d_login=pop3d_login,UN,0,pop3d_login,IF,$range,*", "CDEF:rmpop3d_login=mpop3d_login,60,*", "CDEF:rpop3d_ssl_login=pop3d_ssl_login,60,*", "CDEF:vpop3d_ssl_login=pop3d_ssl_login,UN,0,pop3d_ssl_login,IF,$range,*", "CDEF:rmpop3d_ssl_login=mpop3d_ssl_login,60,*", "DEF:imapd_login=$rrd:imapd_login:AVERAGE", "DEF:mimapd_login=$rrd:imapd_login:MAX", "DEF:imapd_ssl_login=$rrd:imapd_ssl_login:AVERAGE", "DEF:mimapd_ssl_login=$rrd:imapd_ssl_login:MAX", "CDEF:rimapd_login=imapd_login,60,*", "CDEF:vimapd_login=imapd_login,UN,0,imapd_login,IF,$range,*", "CDEF:rmimapd_login=mimapd_login,60,*", "CDEF:rimapd_ssl_login=imapd_ssl_login,60,*", "CDEF:rmimapd_ssl_login=mimapd_ssl_login,60,*", "CDEF:vimapd_ssl_login=imapd_ssl_login,UN,0,imapd_ssl_login,IF,$range,*", 'LINE:rpop3d_login#DD0000:pop3', 'GPRINT:vpop3d_login:AVERAGE:total\: %.0lf logins', 'GPRINT:rmpop3d_login:MAX:max\: %.0lf logins/min\l', 'HRULE:0#000000', 'AREA:rpop3d_ssl_login#770000:pop3/ssl:STACK', 'GPRINT:vpop3d_ssl_login:AVERAGE:total\: %.0lf logins', 'GPRINT:rmpop3d_ssl_login:MAX:max\: %.0lf logins/min\l', 'HRULE:0#000000', 'LINE:rimapd_login#00DD00:imap', 'GPRINT:vimapd_login:AVERAGE:total\: %.0lf logins', 'GPRINT:rmimapd_login:MAX:max\: %.0lf logins/min\l', 'HRULE:0#000000', 'AREA:rimapd_ssl_login#007700:imap/ssl:STACK', 'GPRINT:vimapd_ssl_login:AVERAGE:total\: %.0lf logins', 'GPRINT:rmimapd_ssl_login:MAX:max\: %.0lf logins/min\l', 'COMMENT:\s', 'COMMENT:['.$date.']\r', ); my $ERR=RRDs::error; die "ERROR: $ERR\n" if $ERR; } sub print_html() { print "Content-Type: text/html\n\n"; print < Courier Login Statistics for $host HEADER print "

Courier Login Statistics for $host

\n"; for my $n (0..$#graphs) { print '
'; print "

$graphs[$n]{title}

\n"; print "
\n"; print "

\"couriergraph\"\n"; } print <
Couriergraph $VERSION by Ralf Hildebrandt, based upon Mailgraph $VERSION by David Schweikert
FOOTER } sub send_image($) { my $file = shift; -r $file or do { print "Content-type: text/plain\n\nERROR: can't find $file\n"; exit 1; }; print "Content-type: image/png\n"; print "Content-length: ".((stat($file))[7])."\n"; print "\n"; open(IMG, $file) or die; my $data; print $data while read IMG, $data, 1; } sub main() { my $uri = $ENV{REQUEST_URI} || ''; $uri =~ s/\/[^\/]+$//; $uri =~ s/\//,/g; $uri =~ s/(\~|\%7E)/tilde,/g; mkdir $tmp_dir, 0777 unless -d $tmp_dir; mkdir "$tmp_dir/$uri", 0777 unless -d "$tmp_dir/$uri"; my $img = $ENV{QUERY_STRING}; if(defined $img and $img =~ /\S/) { if($img =~ /^(\d+)-n$/) { my $file = "$tmp_dir/$uri/couriergraph_$1.png"; graph($graphs[$1]{seconds}, $file, $graphs[$1]{title}); send_image($file); } else { die "ERROR: invalid argument\n"; } } else { print_html; } } main; couriergraph/couriergraph.pl0000755000175700017570000004305610155303350016063 0ustar hildebhildeb#!/usr/bin/perl -w # couriergraph -- a postfix statistics rrdtool frontend # copyright (c) 2002-2004 Ralf Hildebrandt # based upon mailgraph, which is # mailgraph -- an rrdtool frontend for mail statistics # copyright (c) 2000-2004 David Schweikert # released under the GNU General Public License ######## Parse::Syslog 1.04 (automatically embedded) ######## package Parse::Syslog; use Carp; use Symbol; use Time::Local; use strict; use vars qw($VERSION); my %months_map = ( 'Jan' => 0, 'Feb' => 1, 'Mar' => 2, 'Apr' => 3, 'May' => 4, 'Jun' => 5, 'Jul' => 6, 'Aug' => 7, 'Sep' => 8, 'Oct' => 9, 'Nov' =>10, 'Dec' =>11, 'jan' => 0, 'feb' => 1, 'mar' => 2, 'apr' => 3, 'may' => 4, 'jun' => 5, 'jul' => 6, 'aug' => 7, 'sep' => 8, 'oct' => 9, 'nov' =>10, 'dec' =>11, ); # year-increment algorithm: if in january, if december is seen, decrement year my $enable_year_decrement = 1; # fast timelocal, cache minute's timestamp # don't cache more than minute because of daylight saving time switch my @str2time_last_minute; my $str2time_last_minute_timestamp; # 0: sec, 1: min, 2: h, 3: day, 4: month, 5: year sub str2time($$$$$$$) { my $GMT = pop @_; if(defined $str2time_last_minute[4] and $str2time_last_minute[0] == $_[1] and $str2time_last_minute[1] == $_[2] and $str2time_last_minute[2] == $_[3] and $str2time_last_minute[3] == $_[4] and $str2time_last_minute[4] == $_[5]) { return $str2time_last_minute_timestamp + $_[0]; } my $time; if($GMT) { $time = timegm(@_); } else { $time = timelocal(@_); } @str2time_last_minute = @_[1..5]; $str2time_last_minute_timestamp = $time-$_[0]; return $time; } sub _use_locale($) { use POSIX qw(locale_h strftime); my $old_locale = setlocale(LC_TIME); for my $locale (@_) { croak "new(): wrong 'locale' value: '$locale'" unless setlocale(LC_TIME, $locale); for my $month (0..11) { $months_map{strftime("%b", 0, 0, 0, 1, $month, 96)} = $month; } } setlocale(LC_TIME, $old_locale); } sub new($$;%) { my ($class, $file, %data) = @_; croak "new() requires one argument: file" unless defined $file; %data = () unless %data; if(not defined $data{year}) { $data{year} = (localtime(time))[5]+1900; } $data{type} = 'syslog' unless defined $data{type}; $data{_repeat}=0; if(ref $file eq 'File::Tail') { $data{filetail} = 1; $data{file} = $file; } else { $data{file}=gensym; open($data{file}, "<$file") or croak "can't open $file: $!"; } if(defined $data{locale}) { if(ref $data{locale} eq 'ARRAY') { _use_locale @{$data{locale}}; } elsif(ref $data{locale} eq '') { _use_locale $data{locale}; } else { croak "'locale' parameter must be scalar or array of scalars"; } } if(defined $data{locale}) { if(ref $data{locale} eq 'ARRAY') { _use_locale @{$data{locale}}; } elsif(ref $data{locale} eq '') { _use_locale $data{locale}; } else { croak "'locale' parameter must be scalar or array of scalars"; } } return bless \%data, $class; } sub _year_increment($$) { my ($self, $mon) = @_; # year change if($mon==0) { $self->{year}++ if defined $self->{_last_mon} and $self->{_last_mon} == 11; $enable_year_decrement = 1; } elsif($mon == 11) { if($enable_year_decrement) { $self->{year}-- if defined $self->{_last_mon} and $self->{_last_mon} != 11; } } else { $enable_year_decrement = 0; } $self->{_last_mon} = $mon; } sub _next_line($) { my $self = shift; my $f = $self->{file}; if(defined $self->{filetail}) { return $f->read; } else { return <$f>; } } sub _next_syslog($) { my ($self) = @_; while($self->{_repeat}>0) { $self->{_repeat}--; return $self->{_repeat_data}; } line: while(my $str = $self->_next_line()) { # date, time and host $str =~ /^ (\S{3})\s+(\d+) # date -- 1, 2 \s (\d+):(\d+):(\d+) # time -- 3, 4, 5 (?:\s<\w+\.\w+>)? # FreeBSD's verbose-mode \s ([-\w\.]+) # host -- 6 \s+ (.*) # text -- 7 $/x or do { warn "WARNING: line not in syslog format: $str"; next line; }; my $mon = $months_map{$1}; defined $mon or croak "unknown month $1\n"; $self->_year_increment($mon); # convert to unix time my $time = str2time($5,$4,$3,$2,$mon,$self->{year}-1900,$self->{GMT}); if(not $self->{allow_future}) { # accept maximum one day in the present future if($time - time > 86400) { warn "WARNING: ignoring future date in syslog line: $str"; next line; } } my ($host, $text) = ($6, $7); # last message repeated ... times if($text =~ /^(?:last message repeated|above message repeats) (\d+) time/) { next line if defined $self->{repeat} and not $self->{repeat}; next line if not defined $self->{_last_data}{$host}; $1 > 0 or do { warn "WARNING: last message repeated 0 or less times??\n"; next line; }; $self->{_repeat}=$1-1; $self->{_repeat_data}=$self->{_last_data}{$host}; return $self->{_last_data}{$host}; } # marks next if $text eq '-- MARK --'; # some systems send over the network their # hostname prefixed to the text. strip that. $text =~ s/^$host\s+//; # discard ':' in HP-UX 'su' entries like this: # Apr 24 19:09:40 remedy : su : + tty?? root-oracle $text =~ s/^:\s+//; $text =~ /^ ([^:]+?) # program -- 1 (?:\[(\d+)\])? # PID -- 2 :\s+ (?:\[ID\ (\d+)\ ([a-z0-9]+)\.([a-z]+)\]\ )? # Solaris 8 "message id" -- 3, 4, 5 (.*) # text -- 6 $/x or do { warn "WARNING: line not in syslog format: $str"; next line; }; if($self->{arrayref}) { $self->{_last_data}{$host} = [ $time, # 0: timestamp $host, # 1: host $1, # 2: program $2, # 3: pid $6, # 4: text ]; } else { $self->{_last_data}{$host} = { timestamp => $time, host => $host, program => $1, pid => $2, msgid => $3, facility => $4, level => $5, text => $6, }; } return $self->{_last_data}{$host}; } return undef; } sub _next_metalog($) { my ($self) = @_; line: while(my $str = $self->_next_line()) { # date, time and host $str =~ /^ (\S{3})\s+(\d+) # date -- 1, 2 \s (\d+):(\d+):(\d+) # time -- 3, 4, 5 # host is not logged \s+ (.*) # text -- 6 $/x or do { warn "WARNING: line not in metalog format: $str"; next line; }; my $mon = $months_map{$1}; defined $mon or croak "unknown month $1\n"; $self->_year_increment($mon); # convert to unix time my $time = str2time($5,$4,$3,$2,$mon,$self->{year}-1900,$self->{GMT}); my $text = $6; $text =~ /^ \[(.*?)\] # program -- 1 # no PID \s+ (.*) # text -- 2 $/x or do { warn "WARNING: text line not in metalog format: $text ($str)"; next line; }; if($self->{arrayref}) { return [ $time, # 0: timestamp 'localhost', # 1: host $1, # 2: program undef, # 3: (no) pid $2, # 4: text ]; } else { return { timestamp => $time, host => 'localhost', program => $1, text => $2, }; } } return undef; } sub next($) { my ($self) = @_; if($self->{type} eq 'syslog') { return $self->_next_syslog(); } elsif($self->{type} eq 'metalog') { return $self->_next_metalog(); } croak "Internal error: unknown type: $self->{type}"; } ##################################################################### ##################################################################### ##################################################################### use RRDs; use strict; use File::Tail; use Getopt::Long; use POSIX 'setsid'; my $VERSION = "1.10"; # config my $rrdstep = 60; my $xpoints = 540; my $points_per_sample = 3; my $daemon_logfile = '/var/log/couriergraph.log'; my $daemon_pidfile = '/var/run/couriergraph.pid'; my $daemon_rrd_dir = '/var/log'; # global variables my $logfile; my $rrd = "/etc/postfix/couriergraph.rrd"; my $year; my $this_minute; my %sum = ( imapd_ssl_login => 0, imapd_login => 0, pop3d_ssl_login => 0, pop3d_login => 0 ); my $rrd_inited=0; my %opt = (); # prototypes sub daemonize(); sub process_line($); sub event_imapd_ssl_login($); sub event_imapd_login($); sub event_pop3d_ssl_login($); sub event_pop3d_login($); sub init_rrd($); sub update($); sub usage { print "usage: couriergraph [*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 causes the logfile to be only read and not monitored\n"; print " -l, --logfile f monitor logfile f instead of /var/log/syslog\n"; print " -t, --logtype t set logfile's type (default: syslog)\n"; print " -y, --year starting year of the log file (default: current year)\n"; print " --host=HOST use only entries for HOST (regexp) in syslog\n"; print " -d, --daemon start in the background\n"; print " --daemon-pid=FILE write PID to FILE instead of /var/run/mailgraph.pid\n"; print " --daemon-rrd=DIR write RRDs to DIR instead of /var/log\n"; print " --daemon-log=FILE write verbose-log to FILE instead of /var/log/mailgraph.log\n"; print " --rrd-name=NAME use NAME.rrd for the rrd files\n"; exit; } sub main { Getopt::Long::Configure('no_ignore_case'); GetOptions(\%opt, 'help|h', 'cat|c', 'logfile|l=s', 'logtype|t=s', 'version|V', 'year|y=i', 'host=s', 'verbose|v+', 'daemon|d!', 'daemon_pid|daemon-pid=s', 'daemon_rrd|daemon-rrd=s', 'daemon_log|daemon-log=s', 'ignore-localhost!', 'ignore-host=s', 'rrd_name|rrd-name=s', ) or exit(1); usage if $opt{help}; if($opt{version}) { print "couriergraph $VERSION by ralf.hildebrandt\@charite.de\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 = $opt{rrd_name}.".rrd" if defined $opt{rrd_name}; daemonize if $opt{daemon}; my $logfile = defined $opt{logfile} ? $opt{logfile} : '/var/log/pop3d-imapd.log'; my $file; if($opt{cat}) { $file = $logfile; } else { $file = File::Tail->new(name=>$logfile, tail=>-1); } my $parser = new Parse::Syslog($file, year => $opt{year}, arrayref => 1, type => defined $opt{logtype} ? $opt{logtype} : 'syslog'); if(not defined $opt{host}) { while(my $sl = $parser->next) { process_line($sl); } } else { my $host = qr/^$opt{host}$/i; while(my $sl = $parser->next) { process_line($sl) if $sl->[1] =~ $host; } } } sub daemonize() { 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"; 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; # mail rrd if(! -f $rrd ) { RRDs::create($rrd, '--start', $m, '--step', $rrdstep, 'DS:imapd_ssl_login:ABSOLUTE:'.($rrdstep*2).':0:U', 'DS:imapd_login:ABSOLUTE:'.($rrdstep*2).':0:U', 'DS:pop3d_ssl_login:ABSOLUTE:'.($rrdstep*2).':0:U', 'DS:pop3d_login: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 ); $this_minute = $m; } elsif(-f $rrd) { $this_minute = RRDs::last($rrd) + $rrdstep; } $rrd_inited=1; } sub process_line($) { my $sl = shift; my $time = $sl->[0]; my $prog = $sl->[2]; my $text = $sl->[4]; if ($prog eq 'courierpop3login') { if($text =~ /LOGIN,/) { event($time, 'pop3d_login'); } } elsif ($prog eq 'imaplogin') { if($text =~ /LOGIN,/) { event($time, 'imapd_login'); } } elsif ($prog eq 'pop3d-ssl') { if($text =~ /LOGIN,/) { event($time, 'pop3d_ssl_login'); } } elsif ($prog eq 'imapd-ssl') { if($text =~ /LOGIN,/) { event($time, 'imapd_ssl_login'); } } } sub event($$) { my ($t, $type) = @_; update($t) and $sum{$type}++; } # returns 1 if $sum should be updated sub update($) { my $t = shift; my $m = $t - $t%$rrdstep; init_rrd($m) unless $rrd_inited; return 1 if $m == $this_minute; return 0 if $m < $this_minute; print "update $this_minute:$sum{imapd_ssl_login}:$sum{imapd_login}:$sum{pop3d_ssl_login}:$sum{pop3d_login}\n"; RRDs::update $rrd, "$this_minute:$sum{imapd_ssl_login}:$sum{imapd_login}:$sum{pop3d_ssl_login}:$sum{pop3d_login}"; if($m > $this_minute+$rrdstep) { for(my $sm=$this_minute+$rrdstep;$sm<$m;$sm+=$rrdstep) { print "update $sm:0:0:0:0:0:0 (SKIP)\n" if $opt{verbose}; RRDs::update $rrd, "$sm:0:0:0:0"; } } $this_minute = $m; $sum{imapd_ssl_login}=0; $sum{imapd_login}=0; $sum{pop3d_ssl_login}=0; $sum{pop3d_login}=0; return 1; } main; __END__ =head1 NAME couriergraph.pl - rrdtool frontend for mail statistics =head1 SYNOPSIS B [I...] --man show man-page and exit -h, --help display this help and exit --version output version information and exit -h, --help display this help and exit -v, --verbose be verbose about what you do -V, --version output version information and exit -c, --cat causes the logfile to be only read and not monitored -l, --logfile f monitor logfile f instead of /var/log/syslog -t, --logtype t set logfile's type (default: syslog) -y, --year starting year of the log file (default: current year) --host=HOST use only entries for HOST (regexp) in syslog -d, --daemon start in the background --daemon-pid=FILE write PID to FILE instead of /var/run/mailgraph.pid --daemon-rrd=DIR write RRDs to DIR instead of /var/log --daemon-log=FILE write verbose-log to FILE instead of /var/log/mailgraph.log --rrd-name=NAME use NAME.rrd for the rrd files =head1 DESCRIPTION This script does parse syslog and updates the RRD database (mailgraph.rrd) in the current directory. =head1 COPYRIGHT Copyright (c) 2004 by ETH Zurich. All rights reserved. =head1 LICENSE This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. =head1 AUTHOR Sdws@ee.ethz.chE> =head1 HISTORY 2002-03-19 ds Version 0.20 2004-10-11 ds Initial ISGTC version (1.10) =cut # Emacs Configuration # # Local Variables: # mode: cperl # eval: (cperl-set-style "PerlStyle") # mode: flyspell # mode: flyspell-prog # End: # # vi: sw=4 et