#!/usr/bin/perl -Tw
# bindgraph -- a BIND statistics rrdtool frontend
# copyright (c) 2003 Marco Delaurenti <dela@linux.it>
# copyright (c) 2003 Marco d'Itri <md@linux.it>
# based on mailgraph (c) David Schweikert <dws@ee.ethz.ch>
# Released under the terms of the GNU General Public License.
use RRDs;
use strict;
use POSIX qw(uname);
my $VERSION = '0.1';
# hostname. will be printed in the HTML page
my $hostname = (POSIX::uname())[1];
# path of the RRD database
my $rrd = '/var/db/bindgraph/bindgraph.rrd';
# temporary directory where the images will be saved
my $tmp_dir = '/tmp/bindgraph';
my $xpoints = 620;
my $ypoints = 250;
my $rows = 24 * 60 * 7;
# HTML tags to e.g. load a style sheet or force automatic refresh
my $htmlheader = '';
# IMG tag attributes. e.g. 'width=717 height=474'
my $imgsize = '';
my $cache_time = 60;
my @graphs = (
{ title => 'Last Hours Graph', seconds => 3600*5, },
{ title => 'Day Graph', seconds => 3600*24, },
{ title => 'Week Graph', seconds => 3600*24*7, },
{ title => 'Month Graph', seconds => 3600*24*31, },
{ title => 'Year Graph', seconds => 3600*24*365, },
);
my @query_t = qw(AAAA CNAME NS ANY _other_ A PTR SOA TKEY);
my %color = (
MX => 'AA0000',
A => 'FF0080',
PTR => '8080C0',
TKEY => '00cc00',
CNAME => 'ff00ff',
SOA => '00ffff',
AAAA => 'ffff00',
NS => 'FF8000',
ANY => 'ff0000',
_other_ => '0000ff',
);
main();
exit 0;
##############################################################################
sub graph($$$;$) {
my ($file, $range, $title, $small) = @_;
my $step = $range / $rows;
my @rrdef = map { (
"DEF:$_=$rrd:$_:AVERAGE",
"DEF:m$_=$rrd:$_:MAX",
"CDEF:r$_=$_,60,*",
# "CDEF:rm$_=m$_,60,*",
"CDEF:d$_=$_,UN,0,$_,IF,$step,*",
"CDEF:s$_=PREV,UN,d$_,PREV,IF,d$_,+"
) } @query_t;
my @rrprint;
my $stack = 0;
foreach my $qt (@query_t) {
# my $type = 'LINE1';
my $type = ($stack++ == 0) ? 'AREA' : 'STACK';
my $qts = sprintf('%7s', $qt);
if ($small) {
push @rrprint, "$type:$qt#" . ($color{$qt} || '000000');
} else {
push @rrprint, "$type:$qt#" . ($color{$qt} || '000000')
. ":query $qts";
push @rrprint, "GPRINT:s$qt:MAX:total\\: %8.0lf q";
push @rrprint, "GPRINT:$qt:AVERAGE:average\\: %.2lf q/s";
push @rrprint, "GPRINT:m$qt:MAX:max\\: %.0lf q/s\\l";
# if you want q/m instead of q/s
# push @rrprint, "GPRINT:s$qt:MAX:total\\: %8.0lf q";
# push @rrprint, "GPRINT:r$qt:AVERAGE:average\\: %.2lf q/m";
# push @rrprint, "GPRINT:rm$qt:MAX:max\\: %.0lf q/m\\l";
}
}
my $comment = 'last update: ' . localtime(last_update($rrd))
. ' graph created on ' . localtime(time) . '\r';
$comment =~ s|:|\\:|g unless $RRDs::VERSION < 1.199908;
my ($text, $xs, $ys) = RRDs::graph(
$file,
'--imgformat', 'PNG',
'--width', $xpoints,
'--height', $ypoints,
'--start', '-' . $range,
'--end', '-' . int($range * 0.01),
'--vertical-label', 'queries/secons',
'--title', $title,
'--lazy',
@rrdef,
@rrprint,
'COMMENT:\s',
'COMMENT:' . $comment,
);
my $err = RRDs::error;
die_fatal("RRDs::graph($file, ...): $err") if $err;
}
sub generate_send_graph($$$;$) {
my ($file, $range, $title, $small) = @_;
my @sb = stat($file);
if (not @sb or (time - $sb[9]) > $cache_time) {
graph($file . '.tmp', $range, $title, $small);
rename($file . '.tmp', $file) or die_fatal("rename: $!");
@sb = stat($file);
}
print "Content-Type: image/png\n"
. "Content-Length: $sb[7]\n"
. "Cache-Control: public\n"
. "Last-Modified: " . gmt_date($sb[9]) . "\n"
. "Expires: " . gmt_date($sb[9] + $cache_time) . "\n"
. "\n";
return if $ENV{REQUEST_METHOD} eq 'HEAD';
open(IMG, $file) or die "cannot open $file: $!";
my $data;
print $data while read(IMG, $data, 4096);
close IMG;
}
sub die_fatal {
my ($message) = @_;
print "Content-Type: text/plain; charset=UTF-8\n\n"
. "ERROR: $message\n";
exit 0;
}
sub last_update {
my ($rrd) = @_;
my $last = RRDs::last($rrd);
my $err = RRDs::error;
die "RRDs::last($rrd): $err" if $err;
return $last;
}
sub gmt_date {
my ($when) = @_;
my ($sec, $min, $hr, $mday, $mon, $year, $wday, $yday, $isdst)
= gmtime($when || time);
my $nmon = (qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec))[$mon];
my $nday = (qw(Sun Mon Tue Wed Thu Fri Sat Sun))[$wday];
return sprintf('%s, %02d %s %d %02d:%02d:%02d GMT',
$nday, $mday, $nmon, $year + 1900, $hr, $min, $sec);
}
sub print_html() {
my $page = <<HEADER;
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>DNS Statistics for $hostname</title>$htmlheader
</head>
<body>
<h1>DNS Statistics for $hostname</h1>
HEADER
for my $n (0 .. $#graphs) {
$page .= "<h2>$graphs[$n]{title}</h2>\n"
. qq#<p><img border="0" alt="bindgraph image $n" $imgsize #
. qq#src="$ENV{SCRIPT_NAME}/bindgraph_${n}.png">\n#;
}
# please do not remove this link from the generated page. thank you!
$page .= <<END;
<p><a href="http://www.linux.it/~md/software/">bindgraph</a> $VERSION
by Marco Delaurenti and Marco d'Itri
</body>
</html>
END
# the Content-Length header will enable HTTP/1.0 persistent connections
print "Content-Type: text/html; charset=UTF-8\n"
. "Content-Length: " . (length $page) . "\n"
# . "Last-Modified: " . gmt_date() . "\n"
# the validator will change for each request. this is OK (?)
. "ETag: $$." . (time) . "\n"
. "Expires: " . gmt_date(time + 60 * 60) . "\n"
. "\n"
. $page;
}
sub main {
if (not $ENV{PATH_INFO}) {
print_html();
exit 0;
}
$ENV{REQUEST_URI} =~ m#^(.+)/[^/]+?$#; # untaint
my $uri = $1;
$uri =~ s#/#,#g;
$uri =~ s#~#tilde,#g;
die_fatal("ERROR: $tmp_dir does not exist") if not -d $tmp_dir;
if (not -d "$tmp_dir/$uri") {
mkdir("$tmp_dir/$uri", 0777)
or die_fatal("ERROR: cannot create $tmp_dir: $!");
}
if ($ENV{PATH_INFO} !~ /^\/bindgraph_(small|\d)\.png$/) {
die_fatal("ERROR: unknown image $ENV{PATH_INFO}");
}
my $file = "$tmp_dir/$uri/bindgraph_$1.png";
if ($1 eq 'small') {
$cache_time = 300;
generate_send_graph($file, $graphs[0]{seconds}, $graphs[0]{title}, 1);
} else {
generate_send_graph($file, $graphs[$1]{seconds}, $graphs[$1]{title});
}
}
# vim ts=4
syntax highlighted by Code2HTML, v. 0.9.1