#!/usr/bin/perl -w
#
# apache-monitor - store Apache usage stats into a RRD file
# Apache Security, http://www.apachesecurity.net
# Copyright (C) 2004 Ivan Ristic <ivanr@webkreator.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#

# This script will fetch a mod_status page, parse out the data,
# and store it in a RRD file.
#
# Usage:
#
#   apache-monitor /path/prefix http://www.apache.org/server-status/
#

use RRDs;
use LWP::UserAgent;

$DEBUG = 0;
$VERSION = "1.1";

sub get_status_page {
    my $url = shift(@_) . "?auto";

    my $ua = new LWP::UserAgent;
    $ua->timeout(30);
    $ua->agent("apache-monitor/" . $VERSION);

    my $request = HTTP::Request->new(GET => $url);
    my $response = $ua->request($request);

    if (!$response->is_success) {
        die("HTTP request failed: ", $response->status_line, "\n");
    }

    return $response->content;
}

sub parse_page {
    my $what = shift(@_);
    my $content = shift(@_);

    my %results = split/:\s*|\n/, $content;
    if (keys %results != 10) {
        die("Failed to parse $what");
    }

    if ($results{"BusyServers"}) {
        $results{"BusyWorkers"} = $results{"BusyServers"};
        $results{"IdleWorkers"} = $results{"IdleServers"};
    }

    $results{"s__"} = $results{"Scoreboard"} =~ tr/_/_/;
    $results{"s_s"} = $results{"Scoreboard"} =~ tr/S/S/;
    $results{"s_r"} = $results{"Scoreboard"} =~ tr/R/R/;
    $results{"s_w"} = $results{"Scoreboard"} =~ tr/W/W/;
    $results{"s_k"} = $results{"Scoreboard"} =~ tr/K/K/;
    $results{"s_d"} = $results{"Scoreboard"} =~ tr/D/D/;
    $results{"s_c"} = $results{"Scoreboard"} =~ tr/C/C/;
    $results{"s_l"} = $results{"Scoreboard"} =~ tr/L/L/;
    $results{"s_g"} = $results{"Scoreboard"} =~ tr/G/G/;
    $results{"s_i"} = $results{"Scoreboard"} =~ tr/I/I/;

    return %results;
}

sub store_data {
    my $name = shift(@_);
    my $rrd_name = $name . ".rrd";
    my $time = shift(@_);
    my %data = @_;

    # create the RRD file on the fly
    if (! -e $rrd_name) {
        # create the RRD file since it does not exist
        RRDs::create($rrd_name,
            "-s 60",
            "DS:totalAccesses:GAUGE:120:0:U",
            "DS:totalKbytes:GAUGE:120:0:U",
            "DS:cpuLoad:GAUGE:120:0:U",
            "DS:uptime:GAUGE:120:0:U",
            "DS:reqPerSec:GAUGE:120:0:U",
            "DS:bytesPerSec:GAUGE:120:0:U",
            "DS:bytesPerReq:GAUGE:120:0:U",
            "DS:busyWorkers:GAUGE:120:0:U",
            "DS:idleWorkers:GAUGE:120:0:U",
            "DS:sc__:GAUGE:120:0:U",
            "DS:sc_s:GAUGE:120:0:U",
            "DS:sc_r:GAUGE:120:0:U",
            "DS:sc_w:GAUGE:120:0:U",
            "DS:sc_k:GAUGE:120:0:U",
            "DS:sc_d:GAUGE:120:0:U",
            "DS:sc_c:GAUGE:120:0:U",
            "DS:sc_l:GAUGE:120:0:U",
            "DS:sc_g:GAUGE:120:0:U",
            "DS:sc_i:GAUGE:120:0:U",
             # one week of high detail data
            "RRA:AVERAGE:0.5:1:10080", 
             # five years of low detail data
            "RRA:AVERAGE:0.5:60:8760" 
        );

        my $err = RRDs::error;
        die "RRD error: $err\n" if $err;
    }

    $rrd_command = $time
        . ":" . $data{"Total Accesses"}
        . ":" . $data{"Total kBytes"}
        . ":" . $data{"CPULoad"}
        . ":" . $data{"Uptime"}
        . ":" . $data{"ReqPerSec"}
        . ":" . $data{"BytesPerSec"}
        . ":" . $data{"BytesPerReq"}
        . ":" . $data{"BusyWorkers"}
        . ":" . $data{"IdleWorkers"}
        . ":" . $data{"s__"}
        . ":" . $data{"s_s"}
        . ":" . $data{"s_r"}
        . ":" . $data{"s_w"}
        . ":" . $data{"s_k"}
        . ":" . $data{"s_d"}
        . ":" . $data{"s_c"}
        . ":" . $data{"s_l"}
        . ":" . $data{"s_g"}
        . ":" . $data{"s_i"}
    ;

    if ($DEBUG) {
        print "RRDupdate($rrd_name)=$rrd_command\n";
    }
    RRDs::update($rrd_name, $rrd_command);

    my $err = RRDs::error;
    die "RRD error: $err\n" if $err;
}

sub process_server {
    my $name = shift(@_);
    my $url = shift(@_);

    # get data from the previous run
    my ($old_page, %old_data);
    if (open(FILE, "${name}.dat")) {
        $old_page = "";
        while(<FILE>) {
            $old_page = $old_page . $_;
        }
        close(FILE);

        %old_data = parse_page("stored data", $old_page);
        if (!%old_data) {
            die("Failed to parse stored data (in file ${name}.dat)");
        }
    }

    my $page = get_status_page($url);
    my %data = parse_page("URL output", $page);
    if (!%data) {
        die("Failed to parse URL output");
    }

    # save page output for later
    open(FILE, ">${name}.dat") || die("Failed to create file ${name}.dat");
    print FILE $page;
    close(FILE);

    # update statistics, but only if the old data is present
    if (%old_data) {
        # preserve Apache-provided values
        $data{"_ReqPerSec"} = $data{"ReqPerSec"};
        $data{"_BytesPerSec"} = $data{"BytesPerSec"};
        $data{"_BytesPerReq"} = $data{"BytesPerReq"};

        if ($old_data{"Uptime"} > $data{"Uptime"}) {
            # server restarted in the meantime
            $data{"ReqPerSec"} = $data{"Total Accesses"} / $data{"Uptime"};
            $data{"BytesPerSec"} = 1024 * ($data{"Total kBytes"} / $data{"Uptime"});
            $data{"BytesPerReq"} = 1024 * ($data{"Total kBytes"} / $data{"Total Accesses"});
        } else {
            # server still running
            my $time_difference = $data{"Uptime"} - $old_data{"Uptime"};
            $data{"ReqPerSec"} = ( $data{"Total Accesses"} - $old_data{"Total Accesses"} ) / $time_difference;
            $data{"BytesPerSec"} = 1024 * ( $data{"Total kBytes"} - $old_data{"Total kBytes"} ) / $time_difference;
            $data{"BytesPerReq"} = 1024 * ( $data{"Total kBytes"} - $old_data{"Total kBytes"} ) / ($data{"Total Accesses"} - $old_data{"Total Accesses"} );
        }
    }

    $time = time();
    store_data($name, $time, %data);
}

if (@ARGV != 2) {
    print "Usage: <storage path w/o extension> <Apache server-status URL>\n";
    exit;
}

my($name, $url) = @ARGV;
$name =~ s/\.rrd$//;
$name =~ s/\.dat$//;
$url =~ s/\?auto$//;
process_server($name, $url);
