diff --git a/plugins/jchkmail_counters_/jchkmail_counters_ b/plugins/jchkmail_counters_/jchkmail_counters_ new file mode 100644 index 00000000..63c5f3e8 --- /dev/null +++ b/plugins/jchkmail_counters_/jchkmail_counters_ @@ -0,0 +1,661 @@ +#! @@PERL@@ -w +# -*- perl -*- + +=pod + +=encoding UTF-8 + +=head1 NAME + + jchkmail_counters_ + +=head1 APPLICABLE SYSTEMS + + MTAs running j-chkmail mail filter - http://www.j-chkmail.org + +=head1 CONFIGURATION + + [jchkmail_counters_*] + user root + +The following environment variables may be defined : + + # disable some instances of the plugin + env.disable no + +=head1 BUGS/GOTCHAS + + None known, but some improvements to do... + +=head1 AUTHOR + + Jose-Marcio Martins da Cruz - mailto:Jose-Marcio.Martins@mines-paristech.fr + Ecole Nationale Superieure des Mines de Paris + + This plugin was inspired by the work of Christophe Decor + Christophe Decor - mailto:decor@supagro.inra.fr + INRA - Institut National de Recherche Agronomique + +=head1 COPYRIGHT + Copyright Jose-Marcio Martins da Cruz + +=head1 VERSION + + 1.0 - Feb 2014 + +=head1 MAGIC MARKERS + + #%# family=contrib + #%# capabilities=autoconf suggest + +=cut + +use strict; +use warnings; + +use lib $ENV{'MUNIN_LIBDIR'}; +use Munin::Plugin; + +my $SMCF = "/etc/mail/jchkmail/j-chkmail.cf"; +my $FSTATS = "/var/jchkmail/files/j-stats"; +my $WORKDIR = "/var/jchkmail/wdb"; +my $CONSTDIR = "/var/jchkmail/cdb"; + +# ------------------------------------------------ +# +# +my $debug = 0; + +my $AppName = $0; +$AppName = `basename $AppName`; +chomp $AppName; + +my $AppExt = undef; +if ($AppName =~ /_([a-z0-9.-]+)$/i) { + $AppExt = $1; +} + +my $disable = defined $ENV{'disable'} && $ENV{'disable'} =~ /yes|oui/i; + +my %StaticData = (); +GetStaticData(\%StaticData); + +my $AppType = ""; +if (defined $AppExt && exists $StaticData{config}{"$AppExt+type"}) { + $AppType = $StaticData{config}{"$AppExt+type"}; +} + +# valider results from Munin::Plugin +# statefile is used only by jchkmail_counters_ratiospamham +my $StateFile = undef; +if (exists $ENV{MUNIN_PLUGSTATE} && defined $ENV{MUNIN_PLUGSTATE}) { + $StateFile = "$ENV{MUNIN_PLUGSTATE}/$AppName.state-joe"; +} +if (exists $ENV{statefile} && defined $ENV{statefile}) { + $StateFile = $ENV{statefile}; +} + +# ------------------------------------------------ +# +# +if ($#ARGV >= 0 && $ARGV[0] eq "autoconf") { + + unless (-f $SMCF) { + print "no\n"; + exit 0; + } + print "yes\n"; + exit 0; +} + +# ------------------------------------------------ +# +# +if ($#ARGV >= 0 && $ARGV[0] eq "suggest") { + + # results from /var/jchkmail/files/j-stats + if (-f $SMCF && -f $FSTATS) { + my %Data = (); + my $line = GetStatsLine(\%Data); + exit 1 unless defined $line && $line ne ""; + + foreach my $k (keys %{$StaticData{config}}) { + next unless $k =~ /(.*)[+]type$/; + my $graph = $1; + my $type = $StaticData{config}{$k}; + if ($type =~ /^(counters|ratio)$/) { + next unless exists $StaticData{config}{$graph . "+data"}; + my $data = $StaticData{config}{$graph . "+data"}; + $data =~ tr#,/# #; + my @fields = split " ", $data; + my $ok = 1; + my $bad = ""; + for my $f (@fields) { + unless (exists $Data{$f}) { + $ok = 0; + $bad .= " $f"; + } + } + if ($ok) { + print "$graph\n" if $ok; + } else { + printf "# %-12s - Lacking fields : %s\n", $graph, $bad; + } + } + } + } + + # results from /var/jchkmail/wdb + if (-f $SMCF && (-d $WORKDIR || -d $CONSTDIR)) { + foreach my $k (keys %{$StaticData{config}}) { + next unless $k =~ /(.*)[+]type$/; + my $graph = $1; + my $type = $StaticData{config}{$k}; + if ($type =~ /^(dbcounters)$/) { + next unless exists $StaticData{config}{$graph . "+data"}; + my $data = $StaticData{config}{$graph . "+data"}; + $data =~ tr#,/# #; + my @fields = split " ", $data; + my $ok = 1; + my $bad = ""; + for my $f (@fields) { + unless (-f "$WORKDIR/$f.db" || -f "$CONSTDIR/$f.db") { + $ok = 0; + $bad .= " $f"; + } + } + if ($ok) { + print "$graph\n" if $ok; + } else { + printf "# %-12s - Lacking fields : %s\n", $graph, $bad; + } + } + } + } + exit 0; +} + +# ------------------------------------------------ +# +# +if (defined $AppExt && $AppType eq "counters") { + my %Data = (); + my $line = GetStatsLine(\%Data); + exit 1 unless defined $line && $line ne ""; + + my $DataAggregate = $StaticData{config}{"$AppExt+data"}; + printf "# Data Aggregate : %s\n", $DataAggregate; + my @Values = split " ", $DataAggregate; + if ($#ARGV >= 0 && $ARGV[0] eq "config") { + my @Keys = grep {$_ =~ /^$AppExt-/;} keys %{$StaticData{config}}; + foreach my $k (@Keys) { + my $v = $StaticData{config}{$k}; + $k =~ s/^$AppExt-//; + printf "%-16s %s\n", $k, $v; + } + printf "graph_order %s\n", (join " ", sort @Values); + foreach my $k (@Values) { + if (exists $Data{$k}) { + my $label = $k; + if (exists $StaticData{label}{$k}) { + $label = $StaticData{label}{$k}; + } + printf "%s.label %s\n", $k, $label; + if (exists $StaticData{label}{"$k.info"}) { + $label = $StaticData{label}{"$k.info"}; + } + printf "%s.info %s\n", $k, $label; + printf "%s.min 0\n", $k; + printf "%s.type DERIVE\n", $k; + } + } + exit 0; + } + + if ($#ARGV < 0) { + foreach my $k (@Values) { + if (exists $Data{$k}) { + printf "%s.value %s\n", $k, $Data{$k}; + } + } + exit 0; + } + exit 1; +} + +# ------------------------------------------------ +# +# +if (defined $AppExt && $AppType eq "ratio") { + my %Data = (); + + my $line = GetStatsLine(\%Data); + exit 1 unless defined $line && $line ne ""; + + my $DataAggregate = $StaticData{config}{"$AppExt+data"}; + printf "# Data Aggregate : %s\n", $DataAggregate; + my ($Ratios, $Base, undef) = split "/", $DataAggregate; + exit 1 unless defined $Ratios && defined $Base; + + my @Values = split ",", $Ratios; + my @Den = split "[ ]+", $Base; + + if ($#ARGV >= 0 && $ARGV[0] eq "config") { + my @Keys = grep {$_ =~ /^$AppExt-/;} keys %{$StaticData{config}}; + foreach my $k (@Keys) { + my $v = $StaticData{config}{$k}; + $k =~ s/^$AppExt-//; + printf "%-16s %s\n", $k, $v; + } + + my $nr = 0; + printf "graph_order %s\n", (join " ", sort @Values); + foreach my $num (@Values) { + printf "# running for %s\n", $num; + my @Num = split "[ ]+", $num; + + my $k = sprintf "%s%02d", $AppExt, $nr++; + my $label = $k; + if (exists $StaticData{label}{$k}) { + $label = $StaticData{label}{$k}; + } + printf "%s.label %s\n", $k, $label; + if (exists $StaticData{label}{"$k.info"}) { + $label = $StaticData{label}{"$k.info"}; + } + printf "%s.info %s\n", $k, $label; + printf "%s.min 0\n", $k; + printf "%s.max 100\n", $k; + printf "%s.type GAUGE\n", $k; + } + exit 0; + } + + if ($#ARGV < 0) { + my %Old = (); + my %Diff = (); + if (EvalBoolean($StaticData{config}{"$AppExt+state"})) { + ReadState(\%Old); + %Diff = (); + DiffFromState(\%Old, \%Data, \%Diff); + } else { + %Diff = %Data; + } + if ($debug) { + foreach my $k (sort keys %Data) { + printf "# DIFF %-16s %12d %12d %12d\n", $k, $Old{$k}, $Data{$k}, + $Diff{$k}; + } + } + my $nr = 0; + foreach my $num (@Values) { + printf "# running for %s\n", $num; + my @Num = split "[ ]+", $num; + my $n = 0; + my $d = 0; + foreach my $k (@Num) { + if (exists $Diff{$k}) { + $n += $Diff{$k}; + } + } + foreach my $k (@Den) { + if (exists $Diff{$k}) { + $d += $Diff{$k}; + } + } + my $k = sprintf "%s%02d", $AppExt, $nr++; + printf "%s.value %s\n", $k, $d > 0 ? ((100. * $n) / $d) : 0; + } + if (EvalBoolean($StaticData{config}{"$AppExt+state"})) { + SaveState(\%Data); + } + exit 0; + } + exit 1; +} + +# ------------------------------------------------ +# +# +if (defined $AppExt && $AppType eq "dbcounters") { + my $DataAggregate = $StaticData{config}{"$AppExt+data"}; + printf "# Data Aggregate : %s\n", $DataAggregate; + my @Values = split " ", $DataAggregate; + if ($#ARGV >= 0 && $ARGV[0] eq "config") { + my @Keys = grep {$_ =~ /^$AppExt-/;} keys %{$StaticData{config}}; + foreach my $k (@Keys) { + my $v = $StaticData{config}{$k}; + $k =~ s/^$AppExt-//; + printf "%-16s %s\n", $k, $v; + } + printf "graph_order %s\n", (join " ", sort @Values); + foreach my $k (@Values) { + if (1) { + my $label = $k; + if (exists $StaticData{label}{$k}) { + $label = $StaticData{label}{$k}; + } + printf "%s.label %s\n", $k, $label; + if (exists $StaticData{label}{"$k.info"}) { + $label = $StaticData{label}{"$k.info"}; + } + printf "%s.info %s\n", $k, $label; + printf "%s.type GAUGE\n", $k; + } + } + exit 0; + } + + if ($#ARGV < 0) { + foreach my $k (@Values) { + my $dbfile = "$WORKDIR/$k.db"; + $dbfile = "$CONSTDIR/$k.db" unless -f $dbfile; + next unless -f $dbfile; + my @DATA = `j-makemap -c -b $dbfile`; + chomp @DATA; + my $nrec = 0; + foreach my $line (@DATA) { + if ($line =~ /\s+(\d+)\s+records/) { + $nrec = $1; + printf "%s.value %s\n", $k, $nrec; + last; + } + } + } + exit 0; + } + exit 1; +} + +# ------------------------------------------------ +# +# +sub GetStatsLine { + my ($h, undef) = @_; + return undef unless -f $FSTATS; + my $line = `tail -1 $FSTATS`; + return undef unless defined $line && $line ne ""; + + my @Values = split / /, $line; + my $time = time(); + if ($Values[0] =~ /^\d+$/) { + $time = shift @Values; + } + $h->{timestamp} = $time; + foreach my $lx (@Values) { + $lx = lc $lx; + if ($lx =~ /(\S+)=\((\d+)\)/) { + $h->{$1} = $2; + } + } + return $line; +} + +# ------------------------------------------------ +# +# +sub ReadState { + my ($h, undef) = @_; + return 0 unless defined $h; + + return 0 unless defined $StateFile && $StateFile ne "" && -f $StateFile; + + open FSTATE, "< $StateFile" || return 0; + while (my $s = ) { + chomp $s; + next if $s =~ /^\s*$/; + next if $s =~ /^\s*#/; + my ($a, $b) = split " ", $s; + $b = 0 unless defined $b; + $h->{$a} = $b; + } + close FSTATE; + { + my %St = Munin::Plugin::restore_state(); + foreach my $k (sort keys %St) { + printf "# %-20s %s\n", $k, $St{$k}; + } + } + return 1; +} + +# ------------------------------------------------ +# +# +sub SaveState { + my ($h, undef) = @_; + + return 0 unless defined $StateFile && $StateFile ne ""; + my @ST = qw(); + open FSTATE, "> $StateFile" || return 0; + foreach my $k (keys %{$h}) { + push @ST, (sprintf "%s %s\n", $k, $h->{$k}); + printf FSTATE "%-20s %s\n", $k, $h->{$k}; + } + close FSTATE; + + Munin::Plugin::save_state(%$h); + return 1; +} + +# ------------------------------------------------ +# +# +sub DiffFromState { + my ($old, $new, $delta, undef) = @_; + return 0 unless defined $old && defined $new; + $delta = $new unless defined $delta; + + return 0 unless exists $old->{timestamp}; + return 0 unless exists $new->{timestamp}; + my $dt = $new->{timestamp} - $old->{timestamp}; + return 0 unless $dt > 1; + + $delta->{timestamp} = $new->{timestamp}; + foreach my $k (sort keys %{$new}) { + next if $k eq "timestamp"; + if (exists $old->{$k}) { + $delta->{$k} = $new->{$k} - $old->{$k}; + $delta->{$k} *= 300. / $dt; + } else { + $delta->{$k} = 0.; + } + } + return 1; +} + +# ------------------------------------------------ +# +# +sub GetStaticData { + my ($h, undef) = @_; + my @DATA = ; + my @SOPT = qw(); + chomp @DATA; + my $in = 0; + my $key = ""; + foreach my $s (@DATA) { + if ($key eq "") { + if ($s =~ /==\s+BEGIN\s+(\S+)\s+==/) { + $key = $1; + } + next; + } + if ($s =~ /==\s+END\s+(\S*\s+)?==/) { + $key = ""; + next; + } + next if $s =~ /^\s*$/; + push @{$h->{$key}{_data_}}, $s; + my ($a, $b) = split " ", $s, 2; + next unless defined $a && $a ne ""; + $b = "" unless defined $b; + $h->{$key}{$a} = $b; + } +} + +# ------------------------------------------------ +# +# +sub EvalBoolean { + my ($v, undef) = @_; + + # return defined $v && $v =~ /^(1|yes|true|oui|vrai)$/i; + if (defined $v) { + return 1 if $v =~ /^(1|yes|true|oui|vrai)$/i; + } + return 0; +} + +# ------------------------------------------------ +# +# +__DATA__ + +== BEGIN label == +conn Connections +conn.info SMTP Connections per time unit +msgs Messages +msgs.info Messages per time unit +rcpt Recipients +rcpt.info Recipients per time unit + +greymsgs Greylisted Messages +greymsgs.info Greylisted Messages per time unit +greyrcpt Greylisted Recipients +greyrcpt.info Greylisted Recipients per time unit + +bayesham Legitimate (Stat Filter) +bayesham.info Legitimate Messages - Statistical Filter +bayesspam Unwanted (Stat Filter) +bayesspam.info Unwanted Messages - Statistical Filter + +matching Pattern Matching +matching.info Unwanted Messages - Pattern Matching Filter +urlbl URL Blacklist +urlbl.info Unwanted Messages - URL Blacklist + +throttle Connection Rate +throttle.info Connections rejected by connection rate +badrcpt Bad Recipients +badrcpt.info Connections rejected - excessive bad recipients +localuser Internal addresses +localuser.info Messages sent to internal use only addresses +badmx Bad Sender MX +badmx.info Messages rejected - Bad Sender MX +rcptrate Recipient Rate +rcptrate.info Messages rejected by recipient rate + +files Files +files.info Files +xfiles X-Files +xfiles.info X-Files + +kbytes Volume (KBytes) +kbytes.info Volume (KBytes) per time unit + +j-greyvalid Grey Validated records +j-greyvalid.info Grey Validated records +j-greypend Grey Waiting records +j-greypend.info Grey Waiting records +j-greywhitelist Grey Whitelisted records +j-greywhitelist.info Grey Whitelisted records +j-greyblacklist j-greyblacklist +j-greyblacklist.info j-greyblacklist + +ratiospamham Ratio Messages +ratiospamham.info Ratio Messages +ratiospamham00 Legitimate messages +ratiospamham00.info Legitimate messages +ratiospamham01 Unwanted messages +ratiospamham01.info Unwanted messages + +== END label == + + +== BEGIN config == +activity-graph_title j-chkmail - Filter activity +activity-graph_category j-chkmail +activity-graph_vlabel counts per second +activity-graph_scale no +activity+data conn msgs rcpt +activity+type counters +activity+enable yes + +greylisting-graph_title j-chkmail - Greylisting activity +greylisting-graph_category j-chkmail +greylisting-graph_vlabel counts per second +greylisting-graph_scale no +greylisting+data greymsgs greyrcpt +greylisting+type counters +greylisting+enable yes + +statfilter-graph_title j-chkmail - Statistical classification +statfilter-graph_category j-chkmail +statfilter-graph_vlabel counts per second +statfilter-graph_scale no +statfilter+data bayesham bayesspam +statfilter+type counters +statfilter+enable yes + +contentfilter-graph_title j-chkmail - Content Filtering +contentfilter-graph_category j-chkmail +contentfilter-graph_vlabel counts per second +contentfilter-graph_scale no +contentfilter+data bayesspam matching urlbl +contentfilter+type counters +contentfilter+enable yes + +behaviour-graph_title j-chkmail - Behaviour filtering - rejections +behaviour-graph_category j-chkmail +behaviour-graph_vlabel counts per second +behaviour-graph_scale no +behaviour+data throttle badrcpt localuser badmx rcptrate +behaviour+type counters +behaviour+enable yes + +xfiles-graph_title j-chkmail - Suspected files filtering +xfiles-graph_category j-chkmail +xfiles-graph_vlabel counts per second +xfiles-graph_scale no +xfiles+data files xfiles +xfiles+type counters +xfiles+enable yes + +volume-graph_title j-chkmail - Volume handled by the filter +volume-graph_category j-chkmail +volume-graph_vlabel KBytes per second +volume-graph_scale no +volume+data kbytes +volume+type counters +volume+enable yes + +ratiospamham-graph_title j-chkmail - Statistical classification +ratiospamham-graph_category j-chkmail +ratiospamham-graph_vlabel Ratio (%) +ratiospamham-graph_scale yes +ratiospamham-graph_args --lower-limit 0 --upper-limit 100 --rigid +ratiospamham+data bayesham, bayesspam / bayesham bayesspam +ratiospamham+type ratio +ratiospamham+state yes +ratiospamham+enable yes + +greydb-graph_title j-chkmail - Greylisting Database Size +greydb-graph_category j-chkmail +greydb-graph_vlabel records +greydb-graph_scale no +greydb-graph_args --lower-limit 0 +greydb+data j-greypend j-greyvalid j-greywhitelist +greydb+type dbcounters +greydb+enable yes + +cdb-graph_title j-chkmail - Static databases +cdb-graph_category j-chkmail +cdb-graph_vlabel records +cdb-graph_scale no +cdb-graph_args --lower-limit 0 +cdb+data j-policy j-urlbl j-bayes j-rcpt +cdb+type dbcounters +cdb+enable yes + +== END config ==