#!/usr/local/bin/perl -w

=head1 NAME

dnsresponse - Plugin to monitor DNS resolution times.
"Poor man's smokeping" :)

=head1 APPLICABLE SYSTEMS

Any unix system.

Dependencies:
 - Net::DNS: https://metacpan.org/pod/Net::DNS e.g. libnet-dns-perl

=head1 CONFIGURATION

The following shows the default configuration.

  [dnsresponse_*]
    env.site www.google.com
    env.times 20
    env.verify no
    env.timeout 30

Where the plugin suffix represents the DNS-Server which should be queried.
env.site: which domain-name should be queried.
env.times: how often a value should be queried.
env.verify: yes: check that we get a response back from the server and show the number of verified responses
env.timeout: timeout in seconds to use when querying servers

=head1 INTERPRETATION

The plugin shows the average and median times taken to resolve a site.

=head1 MAGIC MARKERS

  #%# family=auto
  #%# capabilities=autoconf,suggest

=head1 BUGS

None known.

=head1 VERSION

$Id: dnsresponse_ 61 2009-04-14 09:11:00Z stsimb $

=head1 AUTHOR

Copyright (c) 2009 by Sotiris Tsimbonis.
Copyright (c) 2023 by Andreas Perhab, WT-IO-IT GmbH.

=head1 LICENSE

GPLv2

=cut

#%# family=auto
#%# capabilities=autoconf suggest

use strict;
use warnings;

my $DEBUG = exists $ENV{'MUNIN_DEBUG'} ? $ENV{'MUNIN_DEBUG'} : 0;
my $site = exists $ENV{'site'} ? $ENV{'site'} : "www.google.com";
my $times = exists $ENV{'times'} ? $ENV{'times'} : "20";
my $verify = exists $ENV{'verify'} ? lc($ENV{'verify'}) eq "yes" : 0;
my $timeout = exists $ENV{'timeout'} ? $ENV{'timeout'} : 30;
my $resconf="/etc/resolv.conf";

use File::Basename;
my $basename=basename($0);
my $scriptname;
my $dnsip;
($scriptname, $dnsip) = split("_", $basename);
print "DBG: target dns ip $dnsip\n" if ($DEBUG>0);

if ( defined $ARGV[0] and $ARGV[0] eq "config" ) {
	print "graph_title $dnsip DNS response time\n";
	print "graph_vlabel milliseconds\n";
	print "graph_scale no\n";
	print "graph_category dns\n";
	print "graph_info Time taken by $dnsip to resolve $site $times times.\n";
	print "graph_info Time taken by $dnsip to resolve $site.\n";
	#my @val = ("min", "avg", "median", "max");
	my @val = ("avg", "median", "stddev");
	my $value;
	foreach $value ( @val ) {
		if ($value eq "stddev") {
			print "$value.info Standard deviation (variance).\n";
		} else {
			print "$value.info $value time taken by $dnsip to resolve $site $times times.\n";
		}
		print "$value.label $value\n";
#		print "$value.type DERIVE\n";
#		print "$value.min 0\n";
#		print "$value.warning 100\n";
		if ($value ne "stddev") {
			print "$value.warning ".($timeout*1000)."\n";
		}
#		print "$value.critical 600\n";
	}
	if ( $verify ) {
		print "verified.label Responses\n";
		print "verified.info Number of responses received (sent $times queries)\n";
		print "verified.min 0\n";
		print "verified.warning $times:\n";
		print "verified.critical 1:\n";
	}
	exit 0;
}

if ( defined $ARGV[0] and $ARGV[0] eq "suggest" ) {
	if (-s $resconf) {
		open (FILE, "< $resconf") || die "Could not open $resconf: $!\n";
		my $line;
		while ($line = <FILE>) {
			if ($line =~ /^nameserver/) {
				my $ns; my $ip;
				($ns, $ip) = split(" ", $line);
				print "$ip\n";
			}
		}
		exit 0;
	} else {
		print "ERROR reading $resconf\n";
		exit 1;
	}
}

use Time::HiRes qw ( gettimeofday tv_interval );
use Net::DNS;

my $res = Net::DNS::Resolver->new(
	nameservers => [$dnsip],
	recurse     => 1,
	debug       => $DEBUG,
);
# as we send multiple queries a retry in the DNS resolver would multiply the timeout value
$res->retry(0);
# set retrans time and udp timeout to the same time to effectively act as a timeout value
$res->retrans($timeout);
$res->udp_timeout($timeout);

my $i;
my @restimes;
my @verified_answers;
for ($i=1; $i<=$times; $i++) {
	my $t0 = [gettimeofday];
	my $answer = $res->send($site);
	my $elapsed = tv_interval ($t0);
	push(@restimes, $elapsed);
	print "DBG: count $i elapsed $elapsed\n" if ($DEBUG>0);
	if ($verify && $answer && $answer->string =~ /^;; (Answer|Response) received.*/) {
		print "DBG: answer verified\n" if ($DEBUG>0);
		push(@verified_answers, $answer)
	}
}


@restimes=sort(@restimes);

#my $min=$restimes[0]*1000;
my $average=mean(@restimes)*1000;
my $median=median(@restimes)*1000;
my $stddev=std_dev_ref_sum(@restimes)*1000;
#my $max=$restimes[$times-1]*1000;

#print "min.value $min\n";
print "avg.value $average\n";
print "median.value $median\n";
print "stddev.value $stddev\n";
#print "max.value $max\n";
if ($verify) {
	print "verified.value " . scalar(@verified_answers) . "\n";
}

sub mean {
	my $result;
	foreach (@_) { $result += $_ }
	return $result / @_;
}

sub median {
	my @ar = @_;
	my $elements = scalar(@ar);
	return $ar[(int($elements-1)/2)];
#	if ($elements % 2) {
#		return $ar[($elements-1)/2];
#	} else {
#	return ($ar[($elements-1)/2-0.5]+$ar[($elements-1)/2+0.5])/2;
#	}
}

# Standard Deviance function from http://www.linuxjournal.com/article/6540
sub std_dev_ref_sum {
	my @ar = @_;
	my $elements = scalar @ar;
	my $sum = 0;
	my $sumsq = 0;

	foreach (@ar) {
		$sum += $_;
		$sumsq += ($_ **2);
	}

	return sqrt( $sumsq/$elements - (($sum/$elements) ** 2));
}
