#!/usr/bin/perl -w
#
# $Id: tinydns-readstats.txt,v 1.9 2005/02/01 14:40:22 nate Exp $
#
# Formatting functionality adapted from:
# tinydns log formatting utility
# based on Faried Nawaz's logfile formatter for dnscache
# by Kenji Rikitake <kenji.rikitake@acm.org> 29-JUL-2000
#
# The idea of opening up multilog for writing and sending
# log output to it was taken from tinydns-rrd by Ask Bjrn 
# Hansen, along with a code snippit or two.
#
# What's left was written by me, Nate Campi <nate@campin.net>
##################################################################
#
# Usage:
# 
# If you have a high traffic tinydns server leave out the --log
# option and pipe to multilog in order to minimize I/O. This is
# an example daemontools log run file (e.g. /service/tinydns/log/run):
#
# #!/bin/sh
# exec setuidgid dnslog tinydns-readstats.pl
#
# If you want pretty logs sent to multilog use the option --logpretty. 
# To run multilog you need a log run script something like this:
# 
# #!/bin/sh
# exec setuidgid dnslog tinydns-readstats.pl --logpretty -- multilog t ./main
#
# If you simply want to use this script to prettify your logs, use the
# --nostats option in your log/run script:
#
# #!/bin/sh
# exec setuidgid dnslog tinydns-readstats.pl --nostats --logpretty -- multilog t ./main
#
# You can use this with dnscache now as well,just add the --dnscache 
# option (/service/dnscache/log/run):
#
# #!/bin/sh
# exec setuidgid dnslog tinydns-readstats.pl --dnscache --log -- multilog t ./main
#
##################################################################
#
# To query these stats over SNMP use these lines in net-snmp snmpd.conf:
#
# exec VALUES /bin/echo A PTR ANY MX NS CNAME SOA SRV AAAA TOTAL
# exec bindstats /bin/cat /home/zoneaxfr/stats/stats_file
#
# See http://www.campin.net/DNS/graph.html for the rest of what you need to
# graph the stats.
#
##################################################################
#
# Hmm, each time I implemented this on a box, the stats file was already 
# in place and valid from a prototype version of these scripts. I totally
# spaced on whether it did the right thing when no file existed, the
# script should take some care to make sure things are in order.
#
# Make sure the file is there, readable, and has some valid values in
# it by running UNIX commands like this:
#
# $ mkdir -p /home/zoneaxfr/stats
# $ echo 0 0 0 0 0 0 0 0 0 0 > /home/zoneaxfr/stats/stats_file
# $ chown -R dnslog /home/zoneaxfr/stats
#
# Just be sure that if your logging account isn't named "dnslog" that
# you substitute the correct username in the chown command ("Gdnslog"
# perhaps).
#
##################################################################

use Getopt::Long;
use Fcntl qw(:DEFAULT :flock);
use strict;

my $stats_file = "/home/zoneaxfr/stats/stats_file";
my $stats_file_temp = "/home/zoneaxfr/stats/stats_file.temp";
my $stats_flush_interval = 60; # between 60 and 300 seconds is probably best
my $time = time();
my $stats_flush_time = ( $time + $stats_flush_interval );

# Scott Middlebrooks <scott DOT middlebrooks AT harrynorman DOT com> had
# a problem with zombies, contributed the following to reap them
$SIG{CHLD} = \&REAPER;
sub REAPER {
	my $waitedpid;
	while (($waitedpid = waitpid(-1, &WNOHANG)) > 0) {
	}
	$SIG{CHLD} = \&REAPER;
}

my ( 	$total, $srv, $any, $a, $ns,
	$cname, $soa, $aaaa, $mx, $ptr,
	$other, @line, 

	$oldtotal, $oldsrv, $oldany, $olda, $oldns,
	$oldcname, $oldsoa, $oldaaaa, $oldmx, $oldptr,
	
	$total_a, $total_any, $total_srv, $total_total,
	$total_ns, $total_soa, $total_cname, $total_aaaa,
	$total_mx, $total_ptr,
) = 0;

my ( 
	$DEBUG, $query_types, %opts, $pid, $i, @stats, $dnscache,
);


%opts = ('log'    => 0,
	'logpretty' => 0,
	'nostats' => 0,
	'dnscache' => $dnscache,
	'debug'  => $DEBUG,
);

GetOptions (\%opts,
            'log!',
            'nostats!',
            'dnscache!',
            'logpretty!',
            'debug!',
            )
  or exit 2;

$DEBUG = $opts{debug};
$dnscache = $opts{dnscache};

die "Can't use both --log and --logpretty at once\n" if $opts{log} and $opts{logpretty};

if ( $opts{log} || $opts{logpretty} ) { # pipe to multilog

	$| = 1;
	my $command = join " ", @ARGV; 
	open (MULTI, "|$command") or die "Could not open $command: $!";

	my $oldfh = select MULTI;
	$| = 1;
	select $oldfh;
}
 
while (<STDIN>) {

	$time = time();

	# increment the running total - unless it is a "starting tinydns" line,
	# I don't know of any other non-query lines, let me know if there are any
	if ($dnscache) {
		$total++ if /^query/ ; 
	} else { # then we're running tinydns
		$total++ unless /starting tinydns/ ; 
	}

	print "INPUT before transformation is $_\n" if $DEBUG;

	print MULTI "$_" if $opts{log}; # output for multilog's pleasure

	unless ($dnscache) {
	
		# convert addresses in hex to dotted decimal notation.
		s/\b([a-f0-9]{8})\b/join(".", unpack("C*", pack("H8", $1)))/eg;
	
		# clean up the rest
		s/^([\d.]+):(\w+):(\w+) ([\+\-\/]) \b([a-f0-9]+) \b([-.\w]+)/printQueryLine($1,$2,$3,$4,$5,$6)/e;
	
		print "INPUT after transformation is $_\n" if $DEBUG;
	
		print MULTI "$_" if $opts{logpretty}; # output in pretty format for multilog's pleasure
	
		@line = split(/\s+/);    # split it for easy parsing
	
		SWITCH: {
			if ( $line[2] eq "soa" )	{ $soa++; last SWITCH; }
			if ( $line[2] eq "ptr" )	{ $ptr++; last SWITCH; }
			if ( $line[2] eq "mx" )		{ $mx++; last SWITCH; }
			if ( $line[2] eq "a" )		{ $a++; last SWITCH; }
			if ( $line[2] eq "srv" )	{ $srv++; last SWITCH; }
			if ( $line[2] eq "ns" ) 	{ $ns++; last SWITCH; }
			if ( $line[2] eq "cname" )	{ $cname++; last SWITCH; }
			if ( $line[2] eq "any" )	{ $any++; last SWITCH; }
			if ( $line[2] eq "aaaa" )	{ $aaaa++; last SWITCH; }
			$other++;
		}
	}

	if ( !($opts{nostats}) && ($time >= $stats_flush_time) ) { #flush the stats with a child proc

		$stats_flush_time += $stats_flush_interval; # set the time to flush stats again

		$pid = fork();
		die "Cannot fork: $!" unless defined($pid);
		if ($pid == 0) { 
			# Child process
			updateStats();
			exit(0);   # Child process exits when it is done.
		}

		# clear out the stats now that we've flushed them to disk

		(	$total, $srv, $any, $a, $ns,
			$cname, $soa, $aaaa, $mx, $ptr,
			$other, @line, ) = 0;

		
	} # else 'tis the parent process, which goes back to processing logs

}


### subs

sub printQueryLine {
  my ($host, $port, $query_id, $flag, $query_type, $query) = @_;

  # pad hostname

  my $ret = "$host:";
  $ret .= hex($port);
  $ret .= ":" . hex($query_id);
  $ret .= " " . $flag;
  $ret .= " " . queryType(hex($query_type)) . " $query";
  
  return $ret;
}

sub queryType {
  my ($type) = shift;

  my $ret = "";
 
 # i only list the ones that are in dnscache's dns.h.
 SWITCH: {
    ($type == 1)        && do { $ret = "a";     last SWITCH; };
    ($type == 2)        && do { $ret = "ns";    last SWITCH; };
    ($type == 5)        && do { $ret = "cname"; last SWITCH; };
    ($type == 6)        && do { $ret = "soa";   last SWITCH; };
    ($type == 12)       && do { $ret = "ptr";   last SWITCH; };
    ($type == 13)       && do { $ret = "hinfo"; last SWITCH; };
    ($type == 15)       && do { $ret = "mx";    last SWITCH; };
    ($type == 16)       && do { $ret = "txt";   last SWITCH; };
    ($type == 17)       && do { $ret = "rp";    last SWITCH; };
    ($type == 24)       && do { $ret = "sig";   last SWITCH; };
    ($type == 25)       && do { $ret = "key";   last SWITCH; };
    ($type == 28)       && do { $ret = "aaaa";  last SWITCH; };
    ($type == 252)      && do { $ret = "axfr";  last SWITCH; };
    ($type == 255)      && do { $ret = "any";   last SWITCH; };
    do { $ret .= "$type "; last SWITCH; };
  }
  return $ret;
}

sub updateStats {

	sysopen(STATS_FILE,"$stats_file", O_RDWR|O_CREAT) ||
	    die "Sorry, I couldn't open $stats_file for writing: $!\n";

	flock(STATS_FILE, LOCK_EX)
		or die "Can't write-lock $stats_file: $!\n";
	
	sysopen(STATS_FILE_TEMP,"$stats_file_temp", O_RDWR|O_CREAT) ||
            die "Sorry, I couldn't open $stats_file_temp for writing: $!\n";

	flock(STATS_FILE_TEMP, LOCK_EX)
		or die "Can't write-lock $stats_file_temp: $!\n";

while (<STATS_FILE>) {
		chomp;
		@stats = split(/\s+/);    # split it for easy parsing
	
		$olda     = $stats[0];
		$oldptr   = $stats[1];
		$oldany   = $stats[2];
		$oldmx    = $stats[3];
		$oldns    = $stats[4];
		$oldcname = $stats[5];
		$oldsoa   = $stats[6];
		$oldsrv   = $stats[7];
		$oldaaaa  = $stats[8];
		$oldtotal = $stats[9];
	}
	
	print "oldA oldPTR oldANY oldMX oldNS oldCNAME oldSOA oldSRV oldAAAA oldTOTAL\n" if $DEBUG;
	print "$olda $oldptr $oldany $oldmx $oldns $oldcname $oldsoa $oldsrv $oldaaaa $oldtotal\n" if $DEBUG;
	
	print "A PTR ANY MX NS CNAME SOA SRV AAAA TOTAL\n" if $DEBUG;
	print "$a $ptr $any $mx $ns $cname $soa $srv $aaaa $total\n" if $DEBUG;
	
	$total_a     = ( $olda     + $a     );
	$total_ptr   = ( $oldptr   + $ptr   );
	$total_any   = ( $oldany   + $any   );
	$total_any   = ( $oldany   + $any   );
	$total_mx    = ( $oldmx    + $mx    );
	$total_ns    = ( $oldns    + $ns    );
	$total_cname = ( $oldcname + $cname );
	$total_soa   = ( $oldsoa   + $soa   );
	$total_srv   = ( $oldsrv   + $srv   );
	$total_aaaa  = ( $oldaaaa  + $aaaa  );
	$total_total = ( $oldtotal + $total );

	# be careful and truncate it
	seek(STATS_FILE_TEMP, 0, 0)		or die "can't rewind numfile : $!";
	truncate(STATS_FILE_TEMP, 0)		or die "can't truncate $stats_file: $!";
	
	print STATS_FILE_TEMP "$total_a $total_ptr $total_any $total_mx $total_ns $total_cname $total_soa $total_srv $total_aaaa $total_total\n";

	rename("$stats_file_temp","$stats_file") || die "Can't rename $stats_file_temp to $stats_file: $!";
	
	close(STATS_FILE);
	close(STATS_FILE_TEMP);
	
	print "$total_a $total_ptr $total_any $total_mx $total_ns $total_cname $total_soa $total_srv $total_aaaa $total_total\n" if $DEBUG;

}

__END__



