From a64e85b2b7de99d32e53ef9ae5c362ce7e9af917 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 25 Dec 2013 12:30:51 +0900 Subject: [PATCH] Add multiple postfix monitoring plugin --- plugins/postfix/postfix_mailqueue_ | 138 +++++++++++++ plugins/postfix/postfix_mailqueuelog_ | 239 +++++++++++++++++++++++ plugins/postfix/postfix_mailstats_ | 208 ++++++++++++++++++++ plugins/postfix/postfix_mailvolume_multi | 220 +++++++++++++++++++++ 4 files changed, 805 insertions(+) create mode 100755 plugins/postfix/postfix_mailqueue_ create mode 100755 plugins/postfix/postfix_mailqueuelog_ create mode 100755 plugins/postfix/postfix_mailstats_ create mode 100755 plugins/postfix/postfix_mailvolume_multi diff --git a/plugins/postfix/postfix_mailqueue_ b/plugins/postfix/postfix_mailqueue_ new file mode 100755 index 00000000..5ee84556 --- /dev/null +++ b/plugins/postfix/postfix_mailqueue_ @@ -0,0 +1,138 @@ +#!/bin/sh +# -*- sh -*- + +: << =cut + +=head1 NAME + +postfix_mailqueue_ - Plugin to monitor postfix mail spools per running postfix + +=head1 ABOUT + +A guide to postfix mail queue manageent can be found at +L + +A summary: + +=over 4 + +=item maildrop + +Messages that have been submitted via the Postfix sendmail(1) command, +but not yet brought into the main Postfix queue by the pickup(8) +service. + +=item hold + +Messages placed in the "hold" queue stay there until the administrator +intervenes + +=item incoming + +Inbound mail from the network, or mail picked up by the local +pickup(8) daemon from the maildrop directory. + +=item active + +Messages that the queue manager has opened for delivery. Only a limited number +of messages is allowed to enter the active queue (leaky bucket strategy, for a +fixed delivery rate). + +=item deferred + +Mail that could not be delivered upon the first attempt. The queue manager +implements exponential backoff by doubling the time between delivery attempts. + +=item corrupt + +Unreadable or damaged queue files are moved here for inspection. + +=back + +=head1 CONFIGURATION + +Uses the last part of the symlink name to get the postfix queue directory for the config file. +It then extract the queue path from the configuration file and uses it as a spooldir. +A environment spooldir can be set as a fallback. + + [postfix_mailqueue] + env.spooldir /var/spool/postfix + +=head1 AUTHOR + +Unknown. + +Extended to multiple queue use by Clemens Schwaighofer (gullevek@gullevek.org) in 2010. + +=head1 LICENSE + +Unknown. + +=head1 MAGIC MARKERS + +=begin comment + +These magic markers are used by munin-node-configure when installing +munin-node. + +=end comment + + #%# family=auto + #%# capabilities=autoconf + +=cut + +# atempt to get spooldir via postconf, but environment overrides. + +# Remember that postconf is not available unless postfix is. +CONFIG=${0##*postfix_mailqueue_} +CONFIG="/etc/"$CONFIG"/" +POSTCONFSPOOL="$(postconf -c $CONFIG -h queue_directory 2>/dev/null || echo /var/spool/postfix)" +SPOOLDIR=${spooldir:-$POSTCONFSPOOL} + +. $MUNIN_LIBDIR/plugins/plugin.sh + +case $1 in + autoconf|detect) + if [ -d $SPOOLDIR ] ; then + echo yes + exit 0 + else + echo "no (spooldir not found)" + exit 0 + fi + ;; + config) + echo "graph_title Postfix Mailqueue $CONFIG"; + cat <<'EOF' +graph_vlabel Mails in queue +graph_category postfix +graph_total Total +active.label active +deferred.label deferred +maildrop.label maildrop +incoming.label incoming +corrupt.label corrupt +hold.label held +EOF + for field in active deferred maildrop incoming corrupt hold; do + print_warning $field + print_critical $field + done + exit 0 + ;; +esac + +cd $SPOOLDIR >/dev/null 2>/dev/null || { + echo "# Cannot cd to $SPOOLDIR" + exit 1 +} + +cat < =~ /^sum:(\d+)/) +# { +# $sum = $1; +# } +# while () +# { +# if (/^([0-9a-z.\-]+):(\d+)$/) +# { +# $status->{$1} = $2; +# } +# } +# close IN; +#} + +if (! -d $configdir) +{ + print "sum.value U\n"; + foreach my $i (@status_list) + { + print "r$i.value U\n"; + } + exit 0; +} + + +parseLogfile($configdir); + +if ($ARGV[0] and $ARGV[0] eq "config") +{ + # descriptions for the rrd file + my %descriptions = ( + 'crefused' => 'Connection refused', + 'ctimeout' => 'Connection timed out', + 'rtimeout' => 'Lost connection', + 'refusedtalk' => 'Host refused connection', + 'nohost' => 'Host not found', + 'msrefused' => 'Mail service refused', + 'noroute' => 'Route not found', + 'usernotfound' => 'User not found', + 'err450' => '450 mailbox not okay (REJECT)', + 'err452' => '452 mailbox is full', + 'err421' => '421 service not okay (REJECT)', + 'err421a' => '421 service not okay (REJECT, SB)', + 'err4' => 'General 4xx error', + 'lostc' => 'Lost connection', + 'active' => 'Active running', + 'other' => 'Other error' + ); + + + print "graph_title Postfix mailqueue log for $postfix\n"; + print "graph_args --base 1000 -l 0\n"; # numbers not bytes + print "graph_vlabel Mails in Queue log\n"; + print "graph_scale no\n"; # so we do not print "micro, milli, kilo, etc" +# print "graph_total Total\n"; + print "graph_category postfix\n"; + foreach my $i (@status_list) + { + if ($descriptions{$i}) + { + print "r$i.label ".$descriptions{$i}."\n"; + print "r$i.type GAUGE\n"; + print "r$i.draw ".(!$field ? 'AREA' : 'STACK')."\n"; + print "r$i.min 0\n"; + $field = 'AREA'; + } + } + print "sum.label Sum\n"; + print "sum.type GAUGE\n"; + print "sum.draw LINE2\n"; + print "sum.min 0\n"; + exit 0; +} + +print "sum.value $sum\n"; +foreach my $i (@status_list) +{ + print "r$i.value ".($status->{$i} ? $status->{$i} : 0)."\n"; +} + +#if(-l $statefile) { +# die("$statefile is a symbolic link, refusing to touch it."); +#} +#open (OUT, '>', $statefile) or die "Unable to open statefile: $!\n"; +#print OUT "sum:$sum\n"; +#foreach my $i (@status_list) +#{ +# print OUT "$i:".($status->{$i} ? $status->{$i} : 0)."\n"; +#} +#close OUT; + +sub parseLogfile +{ + my ($fname) = @_; + + # the search parts + %search = ( + 'crefused' => 'Connection refused', + 'ctimeout' => 'Connection timed out', + 'rtimeout' => 'read timeout', + 'refusedtalk' => 'refused to talk to me: 554', + 'nohost' => 'Host not found', + 'msrefused' => 'server refused mail service"', + 'noroute' => 'No route to host', + 'usernotfound' => 'address rejected', + 'err450' => ': 450 ', + 'err452' => ': 452 ', + 'err421' => ': 421 ', + 'err421a' => ': 421)', + 'err4' => 'said: 4', + 'lostc' => 'lost connection with', + ); + + my $command = "mailq -C $fname"; + + open(FILE, "$command|") || die ("Cannot open $command: $!"); + while () + { + if (/^\w{10,}\*\s/o) + { + $status->{'active'} ++; + } + elsif (/^\s*\(/o) + { + $set = 0; + foreach $i (@status_list) + { + if ($search{$i} && index($_, $search{$i}) >= 0 && !$set) + { + $status->{$i} ++; + $set = 1; + } + } + if (!$set) + { + $status->{'other'} ++; + } + } + } + close(FILE) || die ("Cannot close $command: $!"); + + foreach $i (keys %{$status}) + { + $sum += $status->{$i}; + } +} + +# vim:syntax=perl diff --git a/plugins/postfix/postfix_mailstats_ b/plugins/postfix/postfix_mailstats_ new file mode 100755 index 00000000..352fcdcf --- /dev/null +++ b/plugins/postfix/postfix_mailstats_ @@ -0,0 +1,208 @@ +#!/usr/bin/perl -w +# -*- perl -*- + +=head1 NAME + +postfix_mailstats_ - Plugin to monitor the number of mails delivered and +rejected by postfix + +=head1 CONFIGURATION + +Uses the last part of the symlink name for grepping the correct data from the +postfix log file. The name must be syslog_name from the postfix config. +The environment settings still applay to this plugin. + +Configuration parameters for /etc/munin/postfix_mailstats_, +if you need to override the defaults below: + + [postfix_mailstats] + env.logdir - Which logfile to use + env.logfile - What file to read in logdir + +=head2 DEFAULT CONFIGURATION + + [postfix_mailstats] + env.logdir /var/log + env.logfile mail.log + +=head1 AUTHOR + +Records show that the plugin was contributed by Nicolai Langfeldt in +2003. Nicolai can't find anything in his email about this and expects +the plugin is based on the corresponding exim plugin - to which it now +bears no resemblence. + +Extended for multiple queue use by Clemens Schwaighofer (gullevek@gullevek.org) in 2010. + +=head1 LICENSE + +GPLv2 + +=head1 MAGIC MARKERS + +=begin comment + +These magic markers are used by munin-node-configure when installing +munin-node. + +=end comment + + #%# family=manual + #%# capabilities=autoconf + +=head1 RANDOM COMMENTS + +Would be cool if someone ported this to Munin::Plugin for both state +file and log tailing. + +=cut + +# get the postfix queue number to look for +$0 =~ /postfix_mailstats_([\w\d\-]+)$/; +my $postfix = $1; +my $statefile = "$ENV{MUNIN_PLUGSTATE}/munin-plugin-".$postfix."_mailstats.state"; +my $pos; +my $delivered = 0; +my $rejects = {}; +my $LOGDIR = $ENV{'logdir'} || '/var/log'; +my $LOGFILE = $ENV{'logfile'} || 'mail.log'; + +my $logfile = "$LOGDIR/$LOGFILE"; + +if ($ARGV[0] and $ARGV[0] eq "autoconf") +{ + my $logfile; + if (-d $LOGDIR) + { + if (-f $logfile) + { + if (-r $logfile) + { + print "yes\n"; + exit 0; + } + else + { + print "no (logfile '$logfile' not readable)\n"; + } + } + else + { + print "no (logfile '$logfile' not found)\n"; + } + } + else + { + print "no (could not find logdir '$LOGDIR')\n"; + } + + exit 0; +} + + +if (-f $statefile) +{ + open (IN, '<', $statefile) or die "Unable to open state-file: $!\n"; + if ( =~ /^(\d+):(\d+)/) + { + ($pos, $delivered) = ($1, $2); + } + while () + { + if (/^([0-9a-z.\-]+):(\d+)$/) + { + $rejects->{$1} = $2; + } + } + close IN; +} + +if (! -f $logfile) +{ + print "delivered.value U\n"; + foreach my $i (sort keys %{$rejects}) + { + print "r$i.value U\n"; + } + exit 0; +} + +$startsize = (stat $logfile)[7]; + +if (!defined $pos) +{ + # Initial run. + $pos = $startsize; +} + +parseLogfile($logfile, $pos, $startsize); +$pos = $startsize; + +if ( $ARGV[0] and $ARGV[0] eq "config" ) +{ + print "graph_title Postfix message throughput for $postfix\n"; + print "graph_args --base 1000 -l 0\n"; + print "graph_vlabel mails / \${graph_period}\n"; + print "graph_scale no\n"; + print "graph_total Total\n"; + print "graph_category postfix\n"; + print "delivered.label delivered\n"; + print "delivered.type DERIVE\n"; + print "delivered.draw AREA\n"; + print "delivered.min 0\n"; + foreach my $i (sort keys %{$rejects}) + { + print "r$i.label reject $i\n"; + print "r$i.type DERIVE\n"; + print "r$i.draw STACK\n"; + print "r$i.min 0\n"; + } + exit 0; +} + +print "delivered.value $delivered\n"; +foreach my $i (sort keys %{$rejects}) +{ + print "r$i.value ", $rejects->{$i}, "\n"; +} + +if (-l $statefile) +{ + die ("$statefile is a symbolic link, refusing to touch it."); +} +open (OUT, '>', $statefile) or die "Unable to open statefile: $!\n"; +print OUT "$pos:$delivered\n"; +foreach my $i (sort keys %{$rejects}) +{ + print OUT "$i:", $rejects->{$i}, "\n"; +} +close OUT; + +sub parseLogfile +{ + my ($fname, $start, $stop) = @_; + open (LOGFILE, $fname) + or die "Unable to open logfile $fname for reading: $!\n"; + seek (LOGFILE, $start, 0) + or die "Unable to seek to $start in $fname: $!\n"; + + while (tell (LOGFILE) < $stop) + { + my $line = ; + chomp ($line); + + if ($line =~ /$postfix\/qmgr.*from=.*size=[0-9]*/ || + $line =~ /$postfix\/smtp.* status=sent /) + { + $delivered++; + } + elsif ($line =~ /$postfix\/smtpd.*reject: \S+ \S+ \S+ (\S+)/ || + $line =~ /$postfix\/cleanup.* reject: (\S+)/) + { + $rejects->{$1}++; + } + } + close(LOGFILE) or warn "Error closing $fname: $!\n"; +} + +# vim:syntax=perl diff --git a/plugins/postfix/postfix_mailvolume_multi b/plugins/postfix/postfix_mailvolume_multi new file mode 100755 index 00000000..c155e31c --- /dev/null +++ b/plugins/postfix/postfix_mailvolume_multi @@ -0,0 +1,220 @@ +#!/usr/bin/perl -w +# -*- perl -*- + +=head1 NAME + +postfix_mailvolume - Plugin to monitor the volume of mails delivered + by multiple postfix and stores per postfix delivered data. + +=head1 APPLICABLE SYSTEMS + +Any postfix. + +=head1 CONFIGURATION + +The following shows the default configuration. + + [postfix*] + env.logdir /var/log + env.logfile syslog + +=head2 Needed additional configuration + +To correctly get all the postfix log data, the postfix system_log prefix names need to be defined with the env.postfix config setting. +If this is not set, the script tries to find all the postfix config folders in /etc/postfix* and get the syslog names from there + + env.postfix postfix10 postfix11 postfix12 + +=head1 INTERPRETATION + +The plugin shows the number of bytes of mail that has passed through +the postfix installation per postfix mailer running. + +=head1 MAGIC MARKERS + + #%# family=auto + #%# capabilities=autoconf + +=head1 BUGS + +None known + +=head1 VERSION + + $Id: postfix_mailvolume.in 2314 2009-08-03 11:28:34Z ssm $ + +=head1 AUTHOR + +Copyright (C) 2011. + +Clemens Schwaighofer (gullevek@gullevek.org) + +=head1 LICENSE + +GPLv2 + +=cut + +use strict; +use Munin::Plugin; + +my $pos = undef; +my $syslog_name = ''; +my @postfix_syslog_name = (); +my %volume = (); +my @restore_state = (); +my $i = 1; +my $LOGDIR = $ENV{'logdir'} || '/var/log'; +my $LOGFILE = $ENV{'logfile'} || 'syslog'; +my $POSTFIX = $ENV{'postfix'} || ''; +# get the postfix syslog_name from the POSTFIX env var, if not set, find them in the /etc/postfix* type +if (!$POSTFIX) +{ + foreach my $dir (grep -d, glob "/etc/postfix*") + { + # remove the leading etc + $dir =~ s/\/etc\///g; + # add data to the postfix string + $POSTFIX .= ' ' if ($POSTFIX); + $POSTFIX .= $dir; + } +} +if ($POSTFIX) +{ + foreach my $config (split(/ /, $POSTFIX)) + { + # find the syslog name + $syslog_name = `postconf -c /etc/$config | grep "syslog_name"`; + # remove any pending whitespace or line breaks + chomp($syslog_name); + $syslog_name =~ s/syslog_name = //g; + # add this to the postfix syslog name array + push(@postfix_syslog_name, $syslog_name); + # also init set the syslog name 0 + $volume{$syslog_name} = 0; + } +} +else +{ + print "Cannot get any postfix syslog_name data\n"; + exit 1; +} + +sub parseLogfile +{ + my ($fname, $start) = @_; + + my ($LOGFILE, $rotated) = tail_open($fname, $start); + + my $line; + + while ($line =<$LOGFILE>) + { + chomp ($line); + # get the postfix syslog name and the size + if ($line =~ /\ ([\d\w\-]+)\/qmgr.*from=.*size=([0-9]+)/) + { + $volume{$1} += $2; + } + } + return tail_close($LOGFILE); +} + +if ($ARGV[0] and $ARGV[0] eq "autoconf") +{ + my $logfile; + `which postconf >/dev/null 2>/dev/null`; + if (!$?) + { + $logfile = "$LOGDIR/$LOGFILE"; + + if (-f $logfile) + { + if (-r "$logfile") + { + print "yes\n"; + exit 0; + } + else + { + print "no (logfile '$logfile' not readable)\n"; + } + } + else + { + print "no (logfile '$logfile' not found)\n"; + } + } + else + { + print "no (postfix not found)\n"; + } + exit 0; +} + +if ($ARGV[0] and $ARGV[0] eq "config") +{ + print "graph_title Postfix bytes throughput per postfix\n"; + print "graph_args --base 1000 -l 0\n"; + print "graph_vlabel bytes / \${graph_period}\n"; + print "graph_scale yes\n"; + print "graph_category postfix\n"; + print "graph_total Throughput sum\n"; + # loop through the postfix names and create per config an entry + foreach $syslog_name (@postfix_syslog_name) + { + print $syslog_name."_volume.label ".$syslog_name." throughput\n"; + print $syslog_name."_volume.type DERIVE\n"; + print $syslog_name."_volume.min 0\n"; + } + exit 0; +} + + +my $logfile = "$LOGDIR/$LOGFILE"; + +if (! -f $logfile) { + print "delivered.value U\n"; + exit 1; +} + +@restore_state = restore_state(); +# first is pos, rest is postfix entries +$pos = $restore_state[0]; +# per postfix values are store: postfix config,value +for ($i = 1; $i < @restore_state; $i ++) +{ + my ($key, $value) = split(/,/, $restore_state[$i]); + $volume{$key} = $value; +} + +if (!$pos) +{ + # No state file present. Avoid startup spike: Do not read log + # file up to now, but remember how large it is now, and next + # time read from there. + + $pos = (stat $logfile)[7]; # File size + foreach $syslog_name (@postfix_syslog_name) + { + $volume{$syslog_name} = 0; + } +} +else +{ + $pos = parseLogfile($logfile, $pos); +} + +@restore_state = ($pos); +foreach $syslog_name (sort keys %volume) +{ + print $syslog_name."_volume.value ".$volume{$syslog_name}."\n"; + push(@restore_state, $syslog_name.','.$volume{$syslog_name}); +} + +# save the current state +save_state(@restore_state); + +# vim:syntax=perl + +__END__