]> jfr.im git - munin-plugins.git/commitdiff
update
authorJohn Runyon <redacted>
Sun, 10 Dec 2023 23:20:26 +0000 (16:20 -0700)
committerJohn Runyon <redacted>
Sun, 10 Dec 2023 23:20:26 +0000 (16:20 -0700)
hddtemp_smartctl [new file with mode: 0755]
nvme [new file with mode: 0755]
php_fpm
postfix_mailvolume
unbound_munin_ [new file with mode: 0755]

diff --git a/hddtemp_smartctl b/hddtemp_smartctl
new file mode 100755 (executable)
index 0000000..2ba6138
--- /dev/null
@@ -0,0 +1,287 @@
+#!/usr/bin/perl -w
+# -*- perl -*-
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+hddtemp_smartctl - Plugin to monitor harddrive temperatures through
+SMART
+
+=head1 CONFIGURATION
+
+This plugin needs to run as root or some other user that has access to
+the harddrive devices.
+
+The following environment variables are used
+
+ smartctl  - path to smartctl executable
+ drives           - List drives to monitor. E.g. "env.drives hda hdc".
+ type_$dev - device type for one drive, e.g. "env.type_sda 3ware,0"
+             or more typically "env.type_sda ata" if sda is a SATA disk.
+ args_$dev - additional arguments to smartctl for one drive,
+             e.g. "env.args_hda -v 194,10xCelsius".  Use this to make
+             the plugin use the --all or -a option if your disk will
+             not return its temperature when only the -A option is
+             used.
+ dev_$dev  - monitoring device for one drive, e.g. twe0
+
+If the "smartctl" environment variable is not set the plugin will
+search your $PATH, /usr/bin, /usr/sbin, /usr/local/bin and
+/usr/local/sbin for a file called "smartctl", and use that.
+
+If the "drives" environment variable is not set, the plugin will
+attempt to search for drives to probe.
+
+=head1 MAGIC MARKERS
+
+ #%# family=auto
+ #%# capabilities=autoconf
+
+=head1 AUTHOR
+
+Copyright (c) 2005, Lutz Peter Christoph
+All rights reserved.
+
+2016-08-27, Gabriele Pohl (contact@dipohl.de)
+Fix for github issue #690
+
+=head1 LICENSE
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+  * Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+
+  * Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in
+    the documentation and/or other materials provided with the
+    distribution.
+
+  * The name and aliases of Lutz Peter Christoph ("Lupe Christoph",
+    "Lutz Christoph") may not be used to endorse or promote products
+    derived from this software without specific prior written
+    permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+=head1 NOTES
+
+Note for users of RAID controllers (smartmontools currently only
+supports 3ware): you can specify the drives attached to your RAID
+controller(s) as raiddev_num (e.g. sda_0). Then you must specify the
+type like this: type_sda_0 3ware,0.
+
+Recent versions of the kernel driver use a separate major device
+number for monitoring purposes, like /dev/twe<n> or /dev/twa<n>. This
+can be put in the e.g. dev_sda environment variable, to allow the user
+to keep sda as the name of the disk.
+
+=cut
+
+use File::Spec::Functions qw(splitdir);
+use lib $ENV{'MUNIN_LIBDIR'};
+use Munin::Plugin;
+
+my $DEBUG = $ENV{'MUNIN_DEBUG'} || 0;
+
+my $smartctl;
+
+if (exists $ENV{smartctl}) {
+   $smartctl = $ENV{smartctl};
+   if (defined $ARGV[0] and $ARGV[0] eq 'autoconf') {
+      # The real "autoconf" section follows later. But here we need to check for requirements, too.
+      if (! -e $smartctl) {
+        print "no (Predefined smartctl ($smartctl) does not exist)\n";
+        exit 0;
+      } elsif (! -x $smartctl) {
+        print "no (Predefined smartctl ($smartctl) is not executable)\n";
+        exit 0;
+      }
+   } else {
+      # immediate failure is allowed outside of "autoconf"
+      die "$smartctl does not exist\n" unless (-e $smartctl);
+      die "$smartctl is not executable\n" unless (-x $smartctl);
+   }
+} else {
+   # Not defined in %ENV? Check obvious places
+    my @dirs = split(':', $ENV{PATH});
+    push (@dirs, qw(/usr/bin /usr/sbin /usr/local/bin /usr/local/sbin) );
+
+    until ($smartctl or @dirs == 0) {
+      my $dir = shift @dirs;
+      my $path = $dir.'/smartctl';
+      $smartctl = $path if -x $path;
+    }
+
+    unless ($smartctl) {
+        if (defined $ARGV[0] and $ARGV[0] eq 'autoconf') {
+            print "no ('smartctl' executable not found)\n";
+            exit 0;
+        } else {
+            die "'smartctl' executable not found\n";
+        }
+    }
+}
+
+my @drives;
+
+# Try to get a default set of drives
+if ($^O eq 'linux') {
+  # On Linux, we know how to enumerate ide drives.
+  my @drivesIDE;
+  if (-d '/proc/ide') {
+    opendir(IDE, '/proc/ide');
+    @drivesIDE = grep /hd[a-z]/, readdir IDE;
+    closedir(IDE);
+  }
+
+  # Look for SCSI / SATA drives in /sys
+  my @drivesSCSI;
+  if (-d '/sys/block/') {
+    opendir(SCSI, '/sys/block/');
+    @drivesSCSI = grep /sd[a-z]/, readdir SCSI;
+    closedir(SCSI);
+  }
+
+  # Look for NVMe drives in /sys
+  my @drivesNVME;
+  if (-d '/sys/block/') {
+    opendir(NVME, '/sys/block/');
+    @drivesNVME = grep /nvme[0-9]+n[0-9]+/, readdir NVME;
+    closedir(NVME);
+  }
+
+  # Get list of all drives we found
+  @drives=(@drivesIDE,@drivesSCSI,@drivesNVME);
+
+} elsif ($^O eq 'freebsd') {
+  opendir(DEV, '/dev');
+  @drives = grep /^(ada?|da)[0-9]+$/, readdir DEV;
+  closedir(DEV);
+} elsif ($^O eq 'solaris') {
+  @drives = map { s@.*/@@ ; $_ } glob '/dev/rdsk/c*t*d*s2';
+}
+
+@drives = split ' ', $ENV{drives} if exists $ENV{drives};
+
+# Sort list of drives
+@drives = sort @drives;
+
+warn "[DEBUG] Drives: ",join(', ',@drives),"\n" if $DEBUG;
+
+if (defined $ARGV[0]) {
+  if ($ARGV[0] eq 'autoconf') {
+    if (@drives) {
+      my $cmd = command_for_drive_device($drives[0],
+                                         device_for_drive($drives[0]));
+      if (`$cmd` =~ /Temperature/) {
+        print "yes\n";
+      } else {
+        print "no (first drive not supported, configure the plugin)\n";
+      }
+      exit 0;
+    } else {
+      print "no (no drives known)\n";
+      exit 0;
+    }
+  } elsif ($ARGV[0] eq 'config') {
+    print "graph_title HDD temperature\n";
+    print "graph_vlabel Degrees Celsius\n";
+    print "graph_category sensors\n";
+    print "graph_info This graph shows the temperature in degrees Celsius of the hard drives in the machine.\n";
+    foreach (@drives) {
+        my @dirs = splitdir($_);
+        print clean_fieldname($_) . ".label " . $dirs[-1] . "\n";
+        print clean_fieldname($_) . ".max 100\n";
+        print clean_fieldname($_) . ".warning 57\n";
+        print clean_fieldname($_) . ".critical 60\n";
+    }
+    exit 0;
+  }
+}
+
+foreach my $drive (@drives) {
+  warn "[DEBUG] Processing $drive\n" if $DEBUG;
+  my $fulldev = device_for_drive($drive);
+
+  my $cmd = command_for_drive_device($drive, $fulldev);
+  warn "[DEBUG] Command for $drive is % $cmd %\n" if $DEBUG;
+
+  my $output = `$cmd`;
+  my $cmd_exit = $?;
+
+  # Strip header
+  $output =~ s/.*?\n\n//s;
+  # Strip trailer
+  $output =~ s/Please specify device type with the -d option.\n//s;
+  $output =~ s/Use smartctl -h to get a usage summary//s;
+  $output =~ s/\n+$//s;
+
+  if ($cmd_exit != 0) {
+    print "$drive.value U\n";
+    if ($cmd_exit == -1) {
+      warn "[ERROR] Command $cmd on drive $drive failed to execute: $!";
+    } else {
+      my $smartctl_exit = $cmd_exit >> 8;
+      print "$drive.extinfo Command '$cmd' on drive $drive failed with exit($smartctl_exit)\n";
+
+      warn "[ERROR] Command $cmd on drive $drive failed with exit($smartctl_exit): $output";
+    }
+    next;
+  }
+  if ($output =~ /Current Drive Temperature:\s*(\d+)/) {
+    print "$drive.value $1\n";
+  } elsif ($output =~ /^(194 Temperature_(Celsius|Internal).*)/m) {
+    my @F = split /\s+/, $1;
+    print "$drive.value $F[9]\n";
+  } elsif ($output =~ /^(231 Temperature_Celsius.*)/m) {
+    my @F = split ' ', $1;
+    print "$drive.value $F[9]\n";
+  } elsif ($output =~ /^(190 (Airflow_Temperature_Cel|Temperature_Case).*)/m) {
+    my @F = split ' ', $1;
+    print "$drive.value $F[9]\n";
+  } elsif ($output =~ /Temperature:\s*(\d+) Celsius/) {
+     print "$drive.value $1\n";
+  } else {
+      print "$drive.value U\n";
+      print "$drive.extinfo Temperature not detected in smartctl output\n";
+  }
+}
+
+
+sub device_for_drive {
+    my ($drive) = @_;
+
+    my $dev = $drive =~ /(.*)(?:_\d+)$/ ? $1 : $drive;
+
+    my $fulldev = '/dev/';
+    $fulldev .= 'rdsk/' if $^O eq 'solaris';
+    $fulldev .= exists $ENV{'dev_'.$drive} ? $ENV{'dev_'.$drive} : $dev;
+
+    return $fulldev;
+}
+
+
+sub command_for_drive_device {
+    my ($drive, $fulldev) = @_;
+
+    my $cmd = $smartctl.' -A ';
+    $cmd .= $ENV{'args_'.$drive}.' ' if exists $ENV{'args_'.$drive};
+    $cmd .= '-d '.$ENV{'type_'.$drive}.' ' if exists $ENV{'type_'.$drive};
+    $cmd .= $fulldev;
+
+}
diff --git a/nvme b/nvme
new file mode 100755 (executable)
index 0000000..b70e42f
--- /dev/null
+++ b/nvme
@@ -0,0 +1,340 @@
+#! /usr/bin/perl
+# -*- mode: perl; perl-indent-level: 4 -*-
+
+=head1 NAME
+
+nvme - Munin plugin to monitor the use of NVMe devices
+
+=head1 APPLICABLE SYSTEMS
+
+Linux systems with NVMe (Non-Volatile Memory storage attached via PCIe
+bus).
+
+=head1 CONFIGURATION
+
+The plugin uses nvme(1) from the nvme-cli project to read status from
+the NVMe devices.  This requires root access.
+
+  [nvme]
+    user root
+
+When setting alert levels per device, use graph and basename of device
+name, e.g., 'nvme0n1', to make environment variable:
+
+    env.nvme_usage_nvme0n1_warning 5:
+    env.nvme_usage_warning 8:
+
+If your device names change on reboot you can also use the labels
+(based on serial numbers) to set the warning and critical labels
+
+    env.nvme_usage_SN_1234567_warning 8:101
+    env.nvme_usage_SN_1234567_critical 5:101
+
+=head1 INTERPRETATION
+
+This is a multigraph plugin which makes three graphs.
+
+=head2 nvme_usage
+
+This reports how much of capacity is allocated in each NVMe
+"namespace".  The report is in percent.  This number may not have much
+relation to actual use, e.g., if deleted data areas have not been
+trimmed/discarded.
+
+Default warning and critical: '95', '98'
+
+=head2 nvme_bytes
+
+This reports read and write activity on each NVMe device, in bytes per
+second.  Ideally there should be much more read than write.  If they
+are symmetrical, you are using your NVMe as a very expensive FIFO, and
+if you write more than you read, you should probably look for archival
+storage instead.
+
+It is a good idea to compare these numbers to I/O counters from
+diskstats.  If they are much higher, look into whether the write
+amplification can be due to suboptimal I/O request sizes.
+
+This graph does not support alerting.
+
+=head2 nvme_writecycles
+
+This graphs is intended to give an indication of how much life there
+is left in your NVMe.  It calculates the number of bytes written
+during each device's lifetime against the capacity of the device,
+thereby getting an average number of write cycle each cell has
+experienced.
+
+A prosumer NVMe will handle a few thousand writes to each cell before
+the error rate gets out of hand.
+
+No default values for warning and critical.
+
+=head2 nvme_spare
+
+All NVMe has set a side reserve space to remap media errors.  This
+graphs how much is left in percent, taken directly from smart-log
+output.
+
+Default warning and critical: '10:', '3:'
+
+=head1 MAGIC MARKERS
+
+  #%# family=auto
+  #%# capabilities=autoconf
+
+=head1 BUGS
+
+None known.
+
+=head1 VERSION
+
+  1.1
+
+=head1 AUTHOR
+
+Kjetil Torgrim Homme <kjetil.homme@redpill-linpro.com>
+
+=head1 LICENSE
+
+GPLv2
+
+=cut
+
+use strict;
+use Munin::Plugin;
+use IPC::Cmd qw(can_run);
+use File::Basename;
+
+# Check that multigraph is supported
+need_multigraph();
+
+# Return undef if no problem, otherwise explanation
+sub autoconf_problem {
+    return if can_run('nvme');
+    if (open(my $mods, '/proc/modules')) {
+        while (<$mods>) {
+            return "missing nvme(1)" if /^nvme[^a-z]/;
+        }
+        close($mods);
+    }
+    return "missing nvme";    # vague message for non-Linux
+}
+
+sub run_nvme {
+    my (@cmd) = @_;
+    my @lines;
+    if (can_run('nvme') && open(my $nvme, '-|', 'nvme', @cmd)) {
+        @lines = <$nvme>;
+        close($nvme);
+        warn "nvme: probably needs to run as user root\n" if $? && $> != 0;
+    }
+    @lines;
+}
+
+sub human_to_bytes {
+    my ($str) = @_;
+    my %units = (
+        kB => 1000,
+        MB => 1000_000,
+        GB => 1000_000_000,
+        TB => 1000_000_000_000,
+        PB => 1000_000_000_000_000,    # I wish I had need for this
+    );
+    $str =~ /(\d+(\.\d+)?)\s+(.B)/;
+    int($1 * $units{$3});
+}
+
+sub nvme_list {
+    # Node             SN                   Model                                    Namespace Usage                      Format           FW Rev
+    # ---------------- -------------------- ---------------------------------------- --------- -------------------------- ---------------- --------
+    # /dev/nvme1n1     S464NB0K601188N      Samsung SSD 970 EVO 2TB                  1         695.50  GB /   2.00  TB    512   B +  0 B   1B2QEXE7
+    my %devices;
+
+    my $recognised_output;
+    my $lineno = 0;
+    for (run_nvme('list')) {
+        ++$lineno;
+        if (m:^Node\s+SN\s+Model\s+Namespace Usage:) {
+            ++$recognised_output;
+        } elsif (m:^(/\S+)\s+(\S+)\s+(\S.*\S)\s{3,}(\d+)\s+(\S+\s+.B)\s+/\s+(\S+\s+.B):) {
+            $devices{'SN_'.$2} = {
+                device    => $1,
+                sn        => $2,
+                model     => $3,
+                namespace => $4,
+                usage     => human_to_bytes($5),
+                capacity  => human_to_bytes($6),
+            };
+        } elsif ($lineno > 2) {
+            # could not parse device information
+            $recognised_output = 0;
+        }
+    }
+    if ($lineno && !$recognised_output) {
+        warn "Could not recognise output from 'nvme list', please report\n";
+    }
+    \%devices;
+}
+
+sub smart_log {
+    my ($dev) = @_;
+    my %info;
+    for (run_nvme('smart-log', $dev)) {
+        next if /^Smart Log/;
+        if (/(.*?)\s+:\s+(.*)/) {
+            my ($var, $value) = ($1, $2);
+            $var =~ s/\s/_/g;
+            if ($value =~ /^\d+(,\d\d\d)+$/) {
+                $value =~ s/,//g;
+            }
+            $info{lc $var} = $value;
+        }
+    }
+    return \%info;
+}
+
+sub my_print_thresholds {
+    my ($label, $graph, $device, $warn_default, $crit_default) = @_;
+    my $dev = basename($device);
+    my ($warn_label, $crit_label) = get_thresholds($graph, "${graph}_${label}_warning", "${graph}_${label}_critical",
+                                       $warn_default, $crit_default);
+    my ($warn, $crit) = get_thresholds($graph, "${graph}_${dev}_warning", "${graph}_${dev}_critical",
+                                       $warn_label, $crit_label);
+    print "${label}.warning $warn\n" if defined $warn;
+    print "${label}.critical $crit\n" if defined $crit;
+}
+
+use Data::Dumper;
+
+my $mode = ($ARGV[0] or "print");
+
+my $problem = autoconf_problem();
+my $list    = nvme_list();
+
+if ($mode eq 'autoconf') {
+    if (keys %{$list}) {
+        print "yes\n";
+    } else {
+        printf("no (%s)\n", $problem || "no devices to monitor");
+    }
+    exit 0;
+}
+
+my @sn = sort keys %{$list};
+
+if ($mode eq 'config') {
+    my $sn_list = join(' ', @sn);
+
+    print <<'EOF';
+multigraph nvme_usage
+graph_title NVME Namespace Usage
+graph_order $sn_list
+graph_vlabel Percent used
+graph_scale no
+graph_category disk
+graph_info How much space is used
+EOF
+    for (@sn) {
+        my $device = $list->{$_}->{device};
+        print <<"EOF";
+$_.label $device used
+$_.type GAUGE
+$_.max 100
+$_.min 0
+EOF
+        my_print_thresholds($_, 'nvme_usage', $device, '95', '98');
+    }
+    print <<'EOF';
+multigraph nvme_bytes
+graph_title NVME Bytes Read / Written
+graph_order $sn_list
+graph_vlabel bytes read (-) / written (+) per ${graph_period}'
+graph_category disk
+graph_info How much data is read and written
+graph_period second
+EOF
+    for (@sn) {
+        print <<"EOF";
+${_}_r.label $list->{$_}->{device}
+${_}_r.type DERIVE
+${_}_r.min 0
+${_}_r.graph no
+${_}_w.label $list->{$_}->{device}
+${_}_w.type DERIVE
+${_}_w.min 0
+${_}_w.negative ${_}_r
+EOF
+    }
+    print <<'EOF';
+multigraph nvme_writecycles
+graph_title NVME Write Cycles
+graph_order $sn_list
+graph_vlabel Cycles
+graph_args --logarithmic
+graph_category disk
+graph_info How much data has been written in lifetime divided by capacity
+EOF
+    for (@sn) {
+        my $device = $list->{$_}->{device};
+        print <<"EOF";
+$_.label $device write cycles
+$_.type GAUGE
+$_.min 0
+EOF
+        my_print_thresholds($_, 'nvme_writecycles', $device);
+    }
+    print <<'EOF';
+multigraph nvme_spare
+graph_title Available spare blocks
+graph_order $sn_list
+graph_vlabel Percent
+graph_category disk
+graph_info Spare capacity for replacing bad blocks
+EOF
+    for (@sn) {
+        my $device = $list->{$_}->{device};
+        print <<"EOF";
+$_.label $device spare capacity
+$_.type GAUGE
+$_.min 0
+$_.max 100
+EOF
+        my_print_thresholds($_, 'nvme_spare', $device, '10:', '3:');
+    }
+} else {
+    for (@sn) {
+        $list->{$_}->{smart} = smart_log($list->{$_}->{device});
+    }
+    print "multigraph nvme_usage\n";
+    for (@sn) {
+        my $info = $list->{$_};
+        my $used = 100 * $info->{usage} / $info->{capacity};
+        print "$_.value $used\n";
+    }
+    print "multigraph nvme_bytes\n";
+    for (@sn) {
+        my $info   = $list->{$_};
+        my $rbytes = $info->{smart}->{data_units_read};
+        my $wbytes = $info->{smart}->{data_units_written};
+        print "${_}_r.value $rbytes\n";
+        print "${_}_w.value $wbytes\n";
+    }
+    print "multigraph nvme_writecycles\n";
+    for (@sn) {
+        my $info = $list->{$_};
+
+        # The unit size reported is 1000 blocks.
+        my $cycles = $info->{smart}->{data_units_written} * 512_000 / $info->{capacity};
+        print "$_.value $cycles\n";
+    }
+    print "multigraph nvme_spare\n";
+    for (@sn) {
+        my $info = $list->{$_};
+
+        # The unit size reported is 1000 blocks.
+        my $spare = $info->{smart}->{available_spare};
+        $spare =~ s/%//;
+        print "$_.value $spare\n";
+    }
+}
diff --git a/php_fpm b/php_fpm
index 9872cb669481612b8ab50f3b280e7c7b2458467f..a9d6b4b3d632ea5d340a555ce6b2bae4dccd786d 100755 (executable)
--- a/php_fpm
+++ b/php_fpm
@@ -61,6 +61,7 @@ if [ "$1" = "config" ]; then
        echo 'graph_category php'
        echo 'rate.label Connection rate'
        echo 'rate.type DERIVE'
+       echo 'rate.min 0'
 
        echo 'multigraph php_fpm_slow'
        echo 'graph_title PHP-FPM Slow requests'
index 2d5afb12e2237442526c7ccd7ea0f492075c1263..d65755dba10e9f39c0d409f9ebae9dcc2fbd03c1 100755 (executable)
@@ -139,7 +139,7 @@ if ( $ARGV[0] and $ARGV[0] eq "config" ) {
 ($pos, $volume_delivered, $serialized_volumes_queue) = restore_state();
 
 
-if (!defined($volume_delivered) || !defined($pos) || $pos eq 0) { # they could be defined but 0 if the old plugin was run
+if (!defined($volume_delivered) || !defined($pos) || !$pos) { # they could be defined but 0 if the old plugin was run
 
        # No state file present.  Avoid startup spike: Do not read log
        # file up to now, but remember how large it is now, and next
diff --git a/unbound_munin_ b/unbound_munin_
new file mode 100755 (executable)
index 0000000..7aa2857
--- /dev/null
@@ -0,0 +1,578 @@
+#!/bin/sh
+#
+# plugin for munin to monitor usage of unbound servers.
+# To install copy this to /usr/local/share/munin/plugins/unbound_munin_
+# and use munin-node-configure (--suggest, --shell).
+#
+# (C) 2008 W.C.A. Wijngaards.  BSD Licensed.
+#
+# To install; enable statistics and unbound-control in unbound.conf
+#      server:         extended-statistics: yes
+#                      statistics-cumulative: no
+#                      statistics-interval: 0
+#      remote-control: control-enable: yes
+# Run the command unbound-control-setup to generate the key files.
+#
+# Environment variables for this script
+#      unbound_conf    - where the unbound.conf file is located.
+#      unbound_control - where to find unbound-control executable.
+#      spoof_warn      - what level to warn about spoofing
+#      spoof_crit      - what level to crit about spoofing
+#
+# You can set them in your munin/plugin-conf.d/plugins.conf file
+# with:
+# [unbound*]
+# user root
+# env.unbound_conf /usr/local/etc/unbound/unbound.conf
+# env.unbound_control /usr/local/sbin/unbound-control
+# env.spoof_warn 1000
+# env.spoof_crit 100000
+#
+# This plugin can create different graphs depending on what name
+# you link it as (with ln -s) into the plugins directory
+# You can link it multiple times.
+# If you are only a casual user, the _hits and _by_type are most interesting,
+# possibly followed by _by_rcode.
+#
+#      unbound_munin_hits      - base volume, cache hits, unwanted traffic
+#      unbound_munin_queue     - to monitor the internal requestlist
+#      unbound_munin_memory    - memory usage
+#      unbound_munin_by_type   - incoming queries by type
+#      unbound_munin_by_class  - incoming queries by class
+#      unbound_munin_by_opcode - incoming queries by opcode
+#      unbound_munin_by_rcode  - answers by rcode, validation status
+#      unbound_munin_by_flags  - incoming queries by flags
+#      unbound_munin_histogram - histogram of query resolving times
+#
+# Magic markers - optional - used by installation scripts and
+# munin-config:  (originally contrib family but munin-node-configure ignores it)
+#
+#%# family=auto
+#%# capabilities=autoconf suggest
+
+# POD documentation
+: <<=cut
+=head1 NAME
+
+unbound_munin_ - Munin plugin to monitor the Unbound DNS resolver.
+
+=head1 APPLICABLE SYSTEMS
+
+System with unbound daemon.
+
+=head1 CONFIGURATION
+
+  [unbound*]
+  user root
+  env.router 10.1.0.1
+  env.spoof_warn 1000
+  env.spoof_crit 100000
+
+Use the .env settings to override the defaults.
+
+=head1 USAGE
+
+Can be used to present different graphs. Use ln -s for that name in
+the plugins directory to enable the graph.
+unbound_munin_hits     - base volume, cache hits, unwanted traffic
+unbound_munin_queue    - to monitor the internal requestlist
+unbound_munin_memory   - memory usage
+unbound_munin_by_type  - incoming queries by type
+unbound_munin_by_class - incoming queries by class
+unbound_munin_by_opcode        - incoming queries by opcode
+unbound_munin_by_rcode - answers by rcode, validation status
+unbound_munin_by_flags - incoming queries by flags
+unbound_munin_histogram - histogram of query resolving times
+
+=head1 AUTHOR
+
+Copyright 2008 W.C.A. Wijngaards
+
+=head1 LICENSE
+
+BSD
+
+=cut
+
+state="${MUNIN_PLUGSTATE}/unbound.state"
+seentags="${MUNIN_PLUGSTATE}/unbound-seentags.state"
+warn=${spoof_warn:-1000}
+crit=${spoof_crit:-100000}
+lock=$state.lock
+
+# number of seconds between polling attempts.
+# makes the statefile hang around for at least this many seconds,
+# so that multiple links of this script can share the results.
+lee=55
+
+# to keep things within 19 characters
+ABBREV="-e s/total/t/ -e s/thread/t/ -e s/num/n/ -e s/query/q/ -e s/answer/a/ -e s/unwanted/u/ -e s/requestlist/ql/ -e s/type/t/ -e s/class/c/ -e s/opcode/o/ -e s/rcode/r/ -e s/edns/e/ -e s/mem/m/ -e s/cache/c/ -e s/mod/m/"
+
+# get value from $1 into return variable $value
+get_value ( ) {
+       value="`grep '^'$1'=' $state | sed -e 's/^.*=//'`"
+       if test "$value"x = ""x; then
+               value="0"
+       fi
+}
+
+# Update list of seen query types etc to seentags file. This is run while
+# holding the lock, after the state file is updated.
+update_seentags() {
+    tmplist="$(cat ${seentags} 2> /dev/null)
+num.query.type.A
+num.query.class.IN
+num.query.opcode.QUERY
+num.answer.rcode.NOERROR
+"
+    (echo "${tmplist}"; grep ^num ${state} | sed -e 's/=.*//') | sort -u > ${seentags}
+}
+
+# download the state from the unbound server.
+get_state ( ) {
+       # obtain lock for fetching the state
+       # because there is a race condition in fetching and writing to file
+
+       # see if the lock is stale, if so, take it
+       if test -f $lock ; then
+               pid="`cat $lock 2>&1`"
+               kill -0 "$pid" >/dev/null 2>&1
+               if test $? -ne 0 -a "$pid" != $$ ; then
+                       echo $$ >$lock
+               fi
+       fi
+
+       i=0
+       while test ! -f $lock || test "`cat $lock 2>&1`" != $$; do
+               while test -f $lock; do
+                       # wait
+                       i=`expr $i + 1`
+                       if test $i -gt 1000; then
+                               sleep 1;
+                       fi
+                       if test $i -gt 1500; then
+                               echo "error locking $lock" "=" `cat $lock`
+                               rm -f $lock
+                               exit 1
+                       fi
+               done
+               # try to get it
+               if echo $$ >$lock ; then : ; else break; fi
+       done
+       # do not refetch if the file exists and only LEE seconds old
+       if test -f $state; then
+               now=`date +%s`
+               get_value "time.now"
+               value="`echo $value | sed -e 's/\..*$//'`"
+               if test $now -lt `expr $value + $lee`; then
+                       rm -f $lock
+                       return
+               fi
+       fi
+
+       ssh -o ControlPath=/tmp/unbound-ssh.$$.sock -M -o ExitOnForwardFailure=yes -p 2222 -fnN -L /tmp/unbound-control.$$.sock:/tmp/unbound-control.sock root@$router >/dev/null # open an SSH tunnel
+       unbound-control -c /etc/unbound/unbound-control.conf -s /tmp/unbound-control.$$.sock stats > $state # run unbound-control over the SSH tunnel socket
+       if test $? -ne 0; then
+               echo "error retrieving data from unbound server"
+               rm -f $lock
+               exit 1
+       fi
+       ssh -o ControlPath=/tmp/unbound-ssh.$$.sock -O exit root@$router >/dev/null # close the SSH tunnel
+       rm -f /tmp/unbound-ssh.$$.sock /tmp/unbound-control.$$.sock # but it doesn't remove the socket, so do that
+
+       update_seentags
+       rm -f $lock
+}
+
+if test "$1" = "autoconf" ; then
+       if test ! -f $conf; then
+               echo no "($conf does not exist)"
+               exit 0
+       fi
+       if test ! -d `dirname $state`; then
+               echo no "(`dirname $state` directory does not exist)"
+               exit 0
+       fi
+       echo yes
+       exit 0
+fi
+
+if test "$1" = "suggest" ; then
+       echo "hits"
+       echo "queue"
+       echo "memory"
+       echo "by_type"
+       echo "by_class"
+       echo "by_opcode"
+       echo "by_rcode"
+       echo "by_flags"
+       echo "histogram"
+       exit 0
+fi
+
+# determine my type, by name
+id=`echo $0 | sed -e 's/^.*unbound_munin_//'`
+if test "$id"x = ""x; then
+       # some default to keep people sane.
+       id="hits"
+fi
+
+# if $1 exists in statefile, config is echoed with label $2
+exist_config ( ) {
+       mn=`echo $1 | sed $ABBREV | tr . _`
+       if grep '^'$1'=' $state >/dev/null 2>&1; then
+               echo "$mn.label $2"
+               echo "$mn.min 0"
+               echo "$mn.type ABSOLUTE"
+       fi
+}
+
+# print label and min 0 for a name $1 in unbound format
+p_config ( ) {
+       mn=`echo $1 | sed $ABBREV | tr . _`
+       echo $mn.label "$2"
+       echo $mn.min 0
+       echo $mn.type $3
+}
+
+if test "$1" = "config" ; then
+       if test ! -f $state; then
+               get_state
+       fi
+       echo "host_name $router"
+       case $id in
+       hits)
+               echo "graph_title Unbound DNS traffic and cache hits"
+               echo "graph_args --base 1000 -l 0"
+               echo "graph_vlabel queries / \${graph_period}"
+               echo "graph_scale no"
+               echo "graph_category dns"
+               echo "graph_period minute"
+               for x in `grep "^thread[0-9][0-9]*\.num\.queries=" $state |
+                       sed -e 's/=.*//'`; do
+                       exist_config $x "queries handled by `basename $x .num.queries`"
+               done
+               p_config "total.num.queries" "total queries from clients" "ABSOLUTE"
+               p_config "total.num.cachehits" "cache hits" "ABSOLUTE"
+               p_config "total.num.prefetch" "cache prefetch" "ABSOLUTE"
+               p_config "num.query.tcp" "TCP queries" "ABSOLUTE"
+               p_config "num.query.tcpout" "TCP out queries" "ABSOLUTE"
+               p_config "num.query.udpout" "UDP out queries" "ABSOLUTE"
+               p_config "num.query.tls" "TLS queries" "ABSOLUTE"
+               p_config "num.query.tls.resume" "TLS resumes" "ABSOLUTE"
+               p_config "num.query.ipv6" "IPv6 queries" "ABSOLUTE"
+               p_config "unwanted.queries" "queries that failed acl" "ABSOLUTE"
+               p_config "unwanted.replies" "unwanted or unsolicited replies" "ABSOLUTE"
+               echo "u_replies.warning $warn"
+               echo "u_replies.critical $crit"
+               echo "graph_info DNS queries to the recursive resolver. The unwanted replies could be innocent duplicate packets, late replies, or spoof threats."
+               ;;
+       queue)
+               echo "graph_title Unbound requestlist size"
+               echo "graph_args --base 1000 -l 0"
+               echo "graph_vlabel number of queries"
+               echo "graph_scale no"
+               echo "graph_category dns"
+               echo "graph_period minute"
+               p_config "total.requestlist.avg" "Average size of queue on insert" "GAUGE"
+               p_config "total.requestlist.max" "Max size of queue (in 5 min)" "GAUGE"
+               p_config "total.requestlist.overwritten" "Number of queries replaced by new ones" "GAUGE"
+               p_config "total.requestlist.exceeded" "Number of queries dropped due to lack of space" "GAUGE"
+               echo "graph_info The queries that did not hit the cache and need recursion service take up space in the requestlist. If there are too many queries, first queries get overwritten, and at last resort dropped."
+               ;;
+       memory)
+               echo "graph_title Unbound memory usage"
+               echo "graph_args --base 1024 -l 0"
+               echo "graph_vlabel memory used in bytes"
+               echo "graph_category dns"
+               echo "graph_period minute"
+               p_config "mem.cache.rrset" "RRset cache memory" "GAUGE"
+               p_config "mem.cache.message" "Message cache memory" "GAUGE"
+               p_config "mem.mod.iterator" "Iterator module memory" "GAUGE"
+               p_config "mem.mod.validator" "Validator module and key cache memory" "GAUGE"
+               p_config "msg.cache.count" "msg cache count" "GAUGE"
+               p_config "rrset.cache.count" "rrset cache count" "GAUGE"
+               p_config "infra.cache.count" "infra cache count" "GAUGE"
+               p_config "key.cache.count" "key cache count" "GAUGE"
+               echo "graph_info The memory used by unbound."
+               ;;
+       by_type)
+               echo "graph_title Unbound DNS queries by type"
+               echo "graph_args --base 1000 -l 0"
+               echo "graph_vlabel queries / \${graph_period}"
+               echo "graph_scale no"
+               echo "graph_category dns"
+               echo "graph_period minute"
+               for nm in `grep "^num.query.type" $seentags`; do
+                       tp=`echo $nm | sed -e s/num.query.type.//`
+                       p_config "$nm" "$tp" "ABSOLUTE"
+               done
+               echo "graph_info queries by DNS RR type queried for"
+               ;;
+       by_class)
+               echo "graph_title Unbound DNS queries by class"
+               echo "graph_args --base 1000 -l 0"
+               echo "graph_vlabel queries / \${graph_period}"
+               echo "graph_scale no"
+               echo "graph_category dns"
+               echo "graph_period minute"
+               for nm in `grep "^num.query.class" $seentags`; do
+                       tp=`echo $nm | sed -e s/num.query.class.//`
+                       p_config "$nm" "$tp" "ABSOLUTE"
+               done
+               echo "graph_info queries by DNS RR class queried for."
+               ;;
+       by_opcode)
+               echo "graph_title Unbound DNS queries by opcode"
+               echo "graph_args --base 1000 -l 0"
+               echo "graph_vlabel queries / \${graph_period}"
+               echo "graph_scale no"
+               echo "graph_category dns"
+               echo "graph_period minute"
+               for nm in `grep "^num.query.opcode" $seentags`; do
+                       tp=`echo $nm | sed -e s/num.query.opcode.//`
+                       p_config "$nm" "$tp" "ABSOLUTE"
+               done
+               echo "graph_info queries by opcode in the query packet."
+               ;;
+       by_rcode)
+               echo "graph_title Unbound DNS answers by return code"
+               echo "graph_args --base 1000 -l 0"
+               echo "graph_vlabel answer packets / \${graph_period}"
+               echo "graph_scale no"
+               echo "graph_category dns"
+               echo "graph_period minute"
+               for nm in `grep "^num.answer.rcode" $seentags`; do
+                       tp=`echo $nm | sed -e s/num.answer.rcode.//`
+                       p_config "$nm" "$tp" "ABSOLUTE"
+               done
+               p_config "num.answer.secure" "answer secure" "ABSOLUTE"
+               p_config "num.answer.bogus" "answer bogus" "ABSOLUTE"
+               p_config "num.rrset.bogus" "num rrsets marked bogus" "ABSOLUTE"
+               echo "graph_info answers sorted by return value. rrsets bogus is the number of rrsets marked bogus per \${graph_period} by the validator"
+               ;;
+       by_flags)
+               echo "graph_title Unbound DNS incoming queries by flags"
+               echo "graph_args --base 1000 -l 0"
+               echo "graph_vlabel queries / \${graph_period}"
+               echo "graph_scale no"
+               echo "graph_category dns"
+               echo "graph_period minute"
+               p_config "num.query.flags.QR" "QR (query reply) flag" "ABSOLUTE"
+               p_config "num.query.flags.AA" "AA (auth answer) flag" "ABSOLUTE"
+               p_config "num.query.flags.TC" "TC (truncated) flag" "ABSOLUTE"
+               p_config "num.query.flags.RD" "RD (recursion desired) flag" "ABSOLUTE"
+               p_config "num.query.flags.RA" "RA (rec avail) flag" "ABSOLUTE"
+               p_config "num.query.flags.Z" "Z (zero) flag" "ABSOLUTE"
+               p_config "num.query.flags.AD" "AD (auth data) flag" "ABSOLUTE"
+               p_config "num.query.flags.CD" "CD (check disabled) flag" "ABSOLUTE"
+               p_config "num.query.edns.present" "EDNS OPT present" "ABSOLUTE"
+               p_config "num.query.edns.DO" "DO (DNSSEC OK) flag" "ABSOLUTE"
+               echo "graph_info This graphs plots the flags inside incoming queries. For example, if QR, AA, TC, RA, Z flags are set, the query can be rejected. RD, AD, CD and DO are legitimately set by some software."
+               ;;
+       histogram)
+               echo "graph_title Unbound DNS histogram of reply time"
+               echo "graph_args --base 1000 -l 0"
+               echo "graph_vlabel queries / \${graph_period}"
+               echo "graph_scale no"
+               echo "graph_category dns"
+               echo "graph_period minute"
+               echo hcache.label "cache hits"
+               echo hcache.min 0
+               echo hcache.type ABSOLUTE
+               echo hcache.draw AREA
+               echo hcache.colour 999999
+               echo h64ms.label "0 msec - 66 msec"
+               echo h64ms.min 0
+               echo h64ms.type ABSOLUTE
+               echo h64ms.draw STACK
+               echo h64ms.colour 0000FF
+               echo h128ms.label "66 msec - 131 msec"
+               echo h128ms.min 0
+               echo h128ms.type ABSOLUTE
+               echo h128ms.colour 1F00DF
+               echo h128ms.draw STACK
+               echo h256ms.label "131 msec - 262 msec"
+               echo h256ms.min 0
+               echo h256ms.type ABSOLUTE
+               echo h256ms.draw STACK
+               echo h256ms.colour 3F00BF
+               echo h512ms.label "262 msec - 524 msec"
+               echo h512ms.min 0
+               echo h512ms.type ABSOLUTE
+               echo h512ms.draw STACK
+               echo h512ms.colour 5F009F
+               echo h1s.label "524 msec - 1 sec"
+               echo h1s.min 0
+               echo h1s.type ABSOLUTE
+               echo h1s.draw STACK
+               echo h1s.colour 7F007F
+               echo h2s.label "1 sec - 2 sec"
+               echo h2s.min 0
+               echo h2s.type ABSOLUTE
+               echo h2s.draw STACK
+               echo h2s.colour 9F005F
+               echo h4s.label "2 sec - 4 sec"
+               echo h4s.min 0
+               echo h4s.type ABSOLUTE
+               echo h4s.draw STACK
+               echo h4s.colour BF003F
+               echo h8s.label "4 sec - 8 sec"
+               echo h8s.min 0
+               echo h8s.type ABSOLUTE
+               echo h8s.draw STACK
+               echo h8s.colour DF001F
+               echo h16s.label "8 sec - ..."
+               echo h16s.min 0
+               echo h16s.type ABSOLUTE
+               echo h16s.draw STACK
+               echo h16s.colour FF0000
+               echo "graph_info Histogram of the reply times for queries."
+               ;;
+       esac
+
+       exit 0
+fi
+
+# do the stats itself
+get_state
+
+# get the time elapsed
+get_value "time.elapsed"
+if test $value = 0 || test $value = "0.000000"; then
+       echo "error: time elapsed 0 or could not retrieve data"
+       exit 1
+fi
+elapsed="$value"
+
+# print value for $1
+print_value ( ) {
+       mn=`echo $1 | sed $ABBREV | tr . _`
+       get_value $1
+       echo "$mn.value" $value
+}
+
+# print value if line already found in $2
+print_value_line ( ) {
+       mn=`echo $1 | sed $ABBREV | tr . _`
+       value="`echo $2 | sed -e 's/^.*=//'`"
+       echo "$mn.value" $value
+}
+
+
+case $id in
+hits)
+       for x in `grep "^thread[0-9][0-9]*\.num\.queries=" $state |
+               sed -e 's/=.*//'` total.num.queries \
+               total.num.cachehits total.num.prefetch num.query.tcp \
+               num.query.tcpout num.query.udpout num.query.tls num.query.tls.resume \
+               num.query.ipv6 unwanted.queries \
+               unwanted.replies; do
+               if grep "^"$x"=" $state >/dev/null 2>&1; then
+                       print_value $x
+               fi
+       done
+       ;;
+queue)
+       for x in total.requestlist.avg total.requestlist.max \
+               total.requestlist.overwritten total.requestlist.exceeded; do
+               print_value $x
+       done
+       ;;
+memory)
+       for x in mem.cache.rrset mem.cache.message mem.mod.iterator \
+               mem.mod.validator msg.cache.count rrset.cache.count \
+               infra.cache.count key.cache.count; do
+               print_value $x
+       done
+       ;;
+by_type)
+       for nm in `grep "^num.query.type" $seentags`; do
+               print_value $nm
+       done
+       ;;
+by_class)
+       for nm in `grep "^num.query.class" $seentags`; do
+               print_value $nm
+       done
+       ;;
+by_opcode)
+       for nm in `grep "^num.query.opcode" $seentags`; do
+               print_value $nm
+       done
+       ;;
+by_rcode)
+       for nm in `grep "^num.answer.rcode" $seentags`; do
+               print_value $nm
+       done
+       print_value "num.answer.secure"
+       print_value "num.answer.bogus"
+       print_value "num.rrset.bogus"
+       ;;
+by_flags)
+       for x in num.query.flags.QR num.query.flags.AA num.query.flags.TC num.query.flags.RD num.query.flags.RA num.query.flags.Z num.query.flags.AD num.query.flags.CD num.query.edns.present num.query.edns.DO; do
+               print_value $x
+       done
+       ;;
+histogram)
+       get_value total.num.cachehits
+       echo hcache.value $value
+       r=0
+       for x in histogram.000000.000000.to.000000.000001 \
+               histogram.000000.000001.to.000000.000002 \
+               histogram.000000.000002.to.000000.000004 \
+               histogram.000000.000004.to.000000.000008 \
+               histogram.000000.000008.to.000000.000016 \
+               histogram.000000.000016.to.000000.000032 \
+               histogram.000000.000032.to.000000.000064 \
+               histogram.000000.000064.to.000000.000128 \
+               histogram.000000.000128.to.000000.000256 \
+               histogram.000000.000256.to.000000.000512 \
+               histogram.000000.000512.to.000000.001024 \
+               histogram.000000.001024.to.000000.002048 \
+               histogram.000000.002048.to.000000.004096 \
+               histogram.000000.004096.to.000000.008192 \
+               histogram.000000.008192.to.000000.016384 \
+               histogram.000000.016384.to.000000.032768 \
+               histogram.000000.032768.to.000000.065536; do
+               get_value $x
+               r=`expr $r + $value`
+       done
+       echo h64ms.value $r
+       get_value histogram.000000.065536.to.000000.131072
+       echo h128ms.value $value
+       get_value histogram.000000.131072.to.000000.262144
+       echo h256ms.value $value
+       get_value histogram.000000.262144.to.000000.524288
+       echo h512ms.value $value
+       get_value histogram.000000.524288.to.000001.000000
+       echo h1s.value $value
+       get_value histogram.000001.000000.to.000002.000000
+       echo h2s.value $value
+       get_value histogram.000002.000000.to.000004.000000
+       echo h4s.value $value
+       get_value histogram.000004.000000.to.000008.000000
+       echo h8s.value $value
+       r=0
+       for x in histogram.000008.000000.to.000016.000000 \
+               histogram.000016.000000.to.000032.000000 \
+               histogram.000032.000000.to.000064.000000 \
+               histogram.000064.000000.to.000128.000000 \
+               histogram.000128.000000.to.000256.000000 \
+               histogram.000256.000000.to.000512.000000 \
+               histogram.000512.000000.to.001024.000000 \
+               histogram.001024.000000.to.002048.000000 \
+               histogram.002048.000000.to.004096.000000 \
+               histogram.004096.000000.to.008192.000000 \
+               histogram.008192.000000.to.016384.000000 \
+               histogram.016384.000000.to.032768.000000 \
+               histogram.032768.000000.to.065536.000000 \
+               histogram.065536.000000.to.131072.000000 \
+               histogram.131072.000000.to.262144.000000 \
+               histogram.262144.000000.to.524288.000000; do
+               get_value $x
+               r=`expr $r + $value`
+       done
+       echo h16s.value $r
+       ;;
+esac