#!/usr/bin/perl -w # -*- perl -*- =head1 NAME postfix_mailstats - Plugin to monitor the number of mails delivered and rejected by postfix, with support for journald logs =head1 CONFIGURATION 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 env.use_journald - Set to 1 to use journald instead of a logfile env.journalctlargs =head2 DEFAULT CONFIGURATION [postfix_mailstats] env.logdir /var/log env.logfile mail.log env.use_journald 0 env.journalctlargs _SYSTEMD_UNIT=postfix@-.service =head1 AUTHOR Original plugin contributed by Nicolai Langfeldt, extended for journald support by Stephan Kleber with some help by ChatGPT. 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 resemblance. =head1 LICENSE GPLv2 =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 use strict; use Munin::Plugin; 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 $USE_JOURNALD = $ENV{'use_journald'} || 0; my $journalctlargs = $ENV{'journalctlargs'} // '_SYSTEMD_UNIT=postfix@-.service'; my $logfile = "$LOGDIR/$LOGFILE"; if ( $ARGV[0] and $ARGV[0] eq "autoconf" ) { if ($USE_JOURNALD) { # Check if journalctl command is available if (system("which journalctl > /dev/null 2>&1") == 0) { print "yes\n"; exit 0; } else { print "no (journalctl not found)\n"; exit 0; } } else { # Logfile handling 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 ($USE_JOURNALD) { if (!defined $pos) { # Initial run. $pos = 0; } # Parse logs from journald parseJournald(); } else { # Load statefile if it exists 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; } # Logfile handling if (! -f $logfile) { print "delivered.value U\n"; foreach my $reason (sort keys %rejects) { my $fieldname = clean_fieldname("r$reason"); print "$fieldname.value U\n"; } exit 0; } my $startsize = (stat $logfile)[7]; if (!defined $pos) { # Initial run. $pos = $startsize; } parseLogfile($logfile, $pos, $startsize); $pos = $startsize; # Save statefile 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; } if ( $ARGV[0] and $ARGV[0] eq "config" ) { print "graph_title Postfix message throughput\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 mail\n"; print "graph_period minute\n"; print "delivered.label delivered\n"; print "delivered.type DERIVE\n"; print "delivered.draw AREA\n"; print "delivered.min 0\n"; foreach my $reason (sort keys %rejects) { my $fieldname = clean_fieldname("r$reason"); print "$fieldname.label reject $reason\n"; print "$fieldname.type DERIVE\n"; print "$fieldname.draw STACK\n"; print "$fieldname.min 0\n"; } exit 0; } print "delivered.value $delivered\n"; foreach my $reason (sort keys %rejects) { my $fieldname = clean_fieldname("r$reason"); print "$fieldname.value ", $rejects{$reason}, "\n"; } # Function to parse logs from a regular logfile 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 =~ / to=.*, status=sent /) { $delivered++; } elsif ($line =~ /postfix\/smtpd.*proxy-reject: \S+ (\S+)/ || $line =~ /postfix\/smtpd.*reject: \S+ \S+ \S+ (\S+)/ || $line =~ /postfix\/cleanup.* reject: (\S+)/ || $line =~ /postfix\/cleanup.* milter-reject: \S+ \S+ \S+ (\S+)/) { $rejects{$1}++; } } close(LOGFILE) or warn "Error closing $fname: $!\n"; } # Function to parse logs from journald sub parseJournald { my $cmd; $cmd = "journalctl --no-pager --quiet --since=" . `date -dlast-sunday +%Y-%m-%d` . " $journalctlargs"; open(my $journal, '-|', $cmd) or die "Unable to read journald logs: $!\n"; while (my $line = <$journal>) { chomp($line); if ($line =~ / to=.*, status=sent /) { $delivered++; } elsif ($line =~ /postfix\/smtpd.*proxy-reject: \S+ (\S+)/ || $line =~ /postfix\/smtpd.*reject: \S+ \S+ \S+ (\S+)/ || $line =~ /postfix\/postscreen.*reject: \S+ \S+ \S+ (\S+)/ || $line =~ /postfix\/cleanup.* reject: (\S+)/ || $line =~ /postfix\/cleanup.* milter-reject: \S+ \S+ \S+ (\S+)/) { $rejects{$1}++; } } close($journal) or warn "Error closing journald stream: $!\n"; } # vim:syntax=perl