#!/usr/local/bin/perl -w =pod rrd4cmk.pl is a local check for Checkmk that checks an RRD database for thresholds. See https://docs.checkmk.com/latest/en/localchecks.html and https://oss.oetiker.ch/rrdtool/ for a general explanation. Move rrd4cmk.pl to /usr/lib/check_mk_agent/local/ on Linux, or /usr/local/lib/check_mk_agent/local/ on FreeBSD, and make it executable. Also install the Perl module for rrdtool, e.g. on FreeBSD select "Build PERL module" during "make -C /usr/ports/databases/rrdtool config install clean". rrd4cmk.pl expect its configuration in $MK_CONFDIR/rrd4cmk.conf, usually /etc/check_mk/rrd4cmk.conf on Linux or /usr/local/etc/check_mk/rrd4cmk.conf on FreeBSD. The configuration file has to be a valid Perl file and must contain an array of references to hashes, each defining a local check. Each hash (read: definition of a check) must at least contain these entries: rrdfile : Full path the to rrd database file dsname : Name of the data source rra : Name of the round robin archive (or consolidation function) aggregate : One of "count", "sum", "avg", "min", or "max" compare : One of "<", "<=", ">=", or ">" yellow : Threshold for warnings red : Threshold for critical errors Optionally, each local check accepts these options: service_name : Service name shown on Checkmk; defaults to the name of the data source statustext_ok : Detail text reported to Checkmk if yellow threshold has not been reached; defaults to "OK" statustext_yellow : Detail text reported to Checkmk if red threshold has not been reached; defaults to "WARNING" statustext_red : Detail text reported to Checkmk if red threshold has been exceeded; defaults to "ERROR" metrics_name : Metrics name provided to Checkmk; see https://docs.checkmk.com/latest/en/localchecks.html#metrics defaults to the name of the data source min : The lowest possible value that the result may reach; used to provide Checkmk the metrics of this check; see https://docs.checkmk.com/latest/en/localchecks.html#metrics max : The highest possible value that the result may reach; used to provide Checkmk the metrics of this check; see https://docs.checkmk.com/latest/en/localchecks.html#metrics rrdopts : Anonymous array containing options that are passed to rrdfetch. See https://oss.oetiker.ch/rrdtool/doc/rrdfetch.en.html convert : Anonymous function which converts the final result before comparing it to the red and yellow thresholds. The following example rrd4cmk.conf defines two local checks: - The first one summarises all maximum values of the data source "bytes_in" from the rrd database file "/var/db/rrd/traffic.rrd" for the past 3 days, and converts the sum to megabytes. It will then raise a warning if your incoming traffic within the past 3 days exceeds 100 MB, or will raise an error if the traffic exceeds 500 MB. - The second one checks all average values of the data source "temperature" from "/var/db/rrd/bathroom.rrd" for the last 24 hours. It will raise a warning if the average temperature is equal to or below 15 degrees, or will raise an error if it's equal to or below 5 degrees: [{ rrdfile => "/var/db/rrd/traffic.rrd", rrdopts => ["-s", "-3 day" ], dsname => "bytes_in", rra => "MAX", aggregate => "sum", compare => ">", service_name => "Incoming amount of data", statustext_ok => "Traffic is ok", statustext_yellow => "Traffic is high", statustext_red => "Traffic is beyond alarming threshold", yellow => 100, red => 500, convert => sub { my ($result) = @_; return int($result / 1_000_000); }, }, { rrdfile => "/var/db/rrd/bathroom.rrd", dsname => "temperature", rra => "AVG", aggregate => "min", compare => "<=", service_name => "Average temperature in my bathroom", statustext_ok => "Temperature is humid", statustext_yellow => "Temperature is cold", statustext_red => "Temperature is freezing", yellow => 15, red => 5, }]; =cut use strict; use warnings; use RRDs; # aggregate functions my %AGGREGATES = ( "count" => sub { my ($curval, $result) = @_; return (!defined $curval ? $result : $result ? $result + 1 : 1); }, "sum" => sub { my ($curval, $result) = @_; return (!defined $curval ? $result : $result ? ($result + $curval) : $curval); }, "avg" => sub { my ($curval, $result) = @_; if (defined $curval) { if ($result) { $result->[0]++; $result->[1] += $curval; } else { $result = [1, $curval]; } } return $result; }, "min" => sub { my ($curval, $result) = @_; if (defined $curval && (!defined $result || ($result > $curval))) { return $curval; } else { return $result; } }, "max" => sub { my ($curval, $result) = @_; if (defined $curval && (!defined $result || ($result < $curval))) { return $curval; } else { return $result; } }, ); # comperator functions my %CMPS = ( "<" => sub { return ($_[0] < $_[1]); }, "<=" => sub { return ($_[0] <= $_[1]); }, ">=" => sub { return ($_[0] >= $_[1]); }, ">" => sub { return ($_[0] > $_[1]); }, ); sub run_report ($$$) { my ($configfile, $entry, $report) = @_; # sanity checks if (ref($report) ne "HASH") { return "entry $entry in $configfile isn't a hash reference"; } foreach my $option(qw(rrdfile dsname rra aggregate compare)) { if (!$report->{$option}) { return "entry $entry in $configfile is missing required option $option"; } } foreach my $option(qw(yellow red)) { if (!defined $report->{$option}) { return "entry $entry in $configfile is missing required option $option"; } } if ($report->{rrdopts} && (ref($report->{rrdopts}) ne "ARRAY")) { return "option rrdopts of entry $entry in $configfile isn't an " . "array reference"; } my $convert_func = $report->{convert}; if ($convert_func && (ref($convert_func) ne "CODE")) { return "option convert of entry $entry in $configfile isn't an " . "function reference"; } # defaults $report->{service_name} ||= $report->{dsname}; $report->{metrics_name} ||= $report->{dsname}; $report->{statustext_ok} ||= "OK"; $report->{statustext_yellow} ||= "WARNING"; $report->{statustext_red} ||= "ERROR"; # fetch data my @cmd = ($report->{rrdfile}, $report->{rra}); push @cmd, @{$report->{rrdopts}} if $report->{rrdopts}; my ($ts, $step, $names, $data) = RRDs::fetch(@cmd); my $err = RRDs::error; return "error while fetching data from $report->{rrdfile}: $err" if $err; my $dsidx; for ($dsidx = 0; $dsidx <= $#$names; $dsidx++) { last if ($report->{dsname} eq $names->[$dsidx]); } if ($dsidx > $#$names) { return "no data source named $report->{dsname} in $report->{rrdfile}"; } # aggregate data my $aggregate_func = $AGGREGATES{$report->{aggregate}}; if (!$aggregate_func) { return "unknown aggregate $report->{aggregate} in entry $entry in " . $configfile; } my $result; foreach my $line(@$data) { my $curval = $line->[$dsidx]; $result = &$aggregate_func($curval, $result); } my $status = 3; my $statustext = "Unknown result"; my $metric = "-"; if (defined $result) { # optionally convert the result $result = &$convert_func($result) if $convert_func; # check result my $cmp_func = $CMPS{$report->{compare}}; if (!$cmp_func) { return "unknown compare $report->{compare} in entry $entry in " . $configfile; } if (&$cmp_func($result, $report->{red})) { $status = 2; $statustext = $report->{statustext_red}; } elsif (&$cmp_func($result, $report->{yellow})) { $status = 1; $statustext = $report->{statustext_yellow}; } else { $status = 0; $statustext = $report->{statustext_ok}; } # construct the metric $metric = "$report->{metrics_name}=$result;$report->{yellow};$report->{red}"; foreach my $option(qw(min max)) { $metric .= ";"; $metric .= $report->{$option} if defined $report->{$option}; } } # finally, print cmk compatible result print "$status \"$report->{service_name}\" $metric $statustext\n"; return; } # main die "MK_CONFDIR not set" if !$ENV{MK_CONFDIR}; my $configfile = $ENV{MK_CONFDIR} . "/rrd4cmk.conf"; my $config = do($configfile); if (!$config) { die "couldn't parse $configfile: $@" if $@; die "couldn't do $configfile: $!" if !defined $config; die "couldn't run $configfile" if !$config; } if (ref($config) ne "ARRAY") { die "$configfile doesn't contain an array reference"; } for (my $entry = 0; $entry <= $#$config; $entry++) { my $error = run_report($configfile, $entry, $config->[$entry]); print "P \"unknown\" - $error\n" if $error; }