mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2009-03-01 07:21 pm

[dw-free] Found the original checkin of SPUD from 2004 'wcmtools' CVS repository. Bring a copy of t

[commit: http://hg.dwscoalition.org/dw-free/rev/d62f415767a4]

Found the original checkin of SPUD from 2004 'wcmtools' CVS repository.
Bring a copy of this into our repository as it may be the most useful way of
monitoring statistics on mogile/perlbal/memcache/etc. We'll see.

Patch by [staff profile] mark.

Files modified:
  • src/spud/bin/cmdshell
  • src/spud/bin/gatherer
  • src/spud/bin/plugins/config_generator.pl
  • src/spud/bin/plugins/memcached.pl
  • src/spud/bin/plugins/mogilefsd.pl
  • src/spud/bin/plugins/mogstored.pl
  • src/spud/bin/plugins/perlbal.pl
  • src/spud/bin/plugins/test.pl
  • src/spud/bin/replicator
  • src/spud/bin/rrd-storage
  • src/spud/bin/server
  • src/spud/bin/wrapper
  • src/spud/conf/authorized_keys
  • src/spud/conf/cmdshell.conf
  • src/spud/conf/sshd_config
  • src/spud/conf/stats-local.conf
  • src/spud/conf/stats.conf
  • src/spud/doc/README.txt
--------------------------------------------------------------------------------
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/cmdshell
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/cmdshell	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,100 @@
+#!/usr/bin/perl
+#
+# Danga's Command Shell
+# Glue that takes incoming connection on STDIN/STDOUT and forwards it to a
+# local server running on a certain port.  This is meant to be an account's
+# shell in /etc/passwd.
+#
+# Configuration file format:
+#
+#   <command> = <server> : <port>
+#
+# Caveats & Warnings:
+# This program must be in a bin directory.  E.g. /home/lj/bin/cmdshell. It
+# will extrapolate the location of its config file as the portion
+# before the bin/ plus conf/cmdshell.conf.  So, for the example above,
+# /home/lj/conf/cmdshell.conf is expected to exist.
+#
+# Copyright 2004, Danga Interactive
+#
+# Authors:
+#   Mark Smith <marksmith@danga.com>
+#
+# License:
+#   undecided.
+#
+
+# uses
+use strict;
+use IO::Socket;
+
+# known commands
+my %commands;
+
+# try to guess the config file location?
+if ($0 =~ m!^(.+)/bin/cmdshell$!) {
+    my $conf = "$1/conf/cmdshell.conf";
+    die "Config file $conf not found." unless -e $conf;
+
+    # open and parse
+    open FILE, "<$conf"
+        or die "Unable to open config file $conf: $!\n";
+    while (my $line = <FILE>) {
+        if ($line =~ /^\s*([^#].*)\s*=\s*(.*)\s*/) {
+            my ($l, $r) = (trim($1), trim($2));
+            if ($r =~ /^(.+)\s*:\s*(\d+)/) {
+                my ($host, $port) = (trim($1), trim($2)+0);
+                die "Invalid port '$2' for command $l in '$line'.\n" unless $port;
+                $commands{$l} = [ $host, $port ];
+            } else {
+                die "Invalid config file line: $line\n";
+            }
+        }
+    }
+    close FILE;
+} else {
+   die "Unable to guess config file based off of path: $0\n";
+}
+
+# we're running, verify we have info
+my $user = $ENV{LJUSERNAME};
+my $cmd = lc($ENV{LJCOMMAND});
+$cmd = '' unless $cmd =~ /^golive|replicator$/;
+die "No information for command '$cmd'\n" unless $user && $cmd;
+
+# now based on command, open connection to port
+my $socket = IO::Socket::INET->new(Proto => 'tcp',
+                                   PeerAddr => $commands{$cmd}->[0],
+                                   PeerPort => $commands{$cmd}->[1],)
+    or die "Unable to connect to command server ($commands{$cmd}->[0]:$commands{$cmd}->[1])\n";
+$socket->autoflush(1);
+
+# parent pid
+my $ppid = $$;
+
+# now fork
+my $kidpid;
+die "Unable to fork: $!\n" unless defined($kidpid = fork());
+
+# basic parts that handle reading/writing from the two sides
+if ($kidpid) {
+    # parent
+    while (defined (my $line = <$socket>)) {
+        print STDOUT $line;
+    }
+    kill("TERM" => $kidpid); # death to the children
+} else {
+    # child
+    while (defined (my $line = <STDIN>)) {
+        print $socket $line;
+    }
+    kill("TERM" => $ppid); # kill parent since we died
+}
+
+# little trimming sub
+sub trim {
+    my $res = shift;
+    $res =~ s/^\s+//;
+    $res =~ s/\s+$//;
+    return $res;
+}
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/gatherer
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/gatherer	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,440 @@
+#!/usr/bin/perl
+#
+# Danga's Statistics Gatherer
+# Gathers statistics using plugins.
+#
+# Command line options:
+#
+#    --conf STRING    set what config file to use for options
+#    --server STRING  set what server to point at
+#    --port INT       the port of the server to use
+#    --foreground     if present, tell the server to force to the foreground
+#
+# Configuration file format:
+#
+#    server = STRING          location of statistics server
+#    port = INT               port number server listens on
+#
+#    job: plugin(parameter, parameter)
+#    job2: plugin
+#    helperplugin(parameter, parameter)
+#    ...
+#
+# Copyright 2004, Danga Interactive
+#
+# Authors:
+#   Mark Smith <marksmith@danga.com>
+#
+# License:
+#   undecided.
+#
+
+use strict;
+use lib "$ENV{LJHOME}/cgi-bin";
+use Getopt::Long;
+use POSIX qw(:sys_wait_h);
+use Danga::Daemon;
+
+# config
+my $path = "$ENV{LJHOME}/bin/spud";
+my $plugins_path = "$path/plugins";
+
+# command line config
+my %opts;
+GetOptions \%opts, qw/ conf=s server=s port=i /;
+
+# reverse mapping; ( pid => job-name )
+my %pids;
+
+# mapping of what we expect to die soon
+my %todie;
+
+# called if we die
+sub fail {
+    foreach my $pid (keys %pids) {
+        kill 15, $pid;
+    }
+    $_[0] ||= 'no reason provided';
+    die "shutdown detected: $_[0]\n";
+}
+
+# now daemonize
+Danga::Daemon::daemonize(
+    \&worker,
+    {
+        interval => 1,
+        listenport => 13500,
+        chdir => $path,
+        shutdowncode => \&fail,
+    },
+);
+
+# ( plugin-name =>
+#    {
+#       modtime => int, # last modified time of plugin file (for reload detection)
+#       children => [ pid, pid, pid, ... ], # pids of workers doing this plugin
+#       file => str, # filename
+#       reload => bool, # if on, reload_plugins() will reload this plugin
+#       registered => bool, # if on, this plugin has registered correctly and is good to go
+#       code => {
+#           register => sub { ... },
+#           worker => sub { ... },
+#       },
+#    }
+# );
+my %plugins;
+
+# ( job-name =>
+#    {
+#       plugin => plugin-name,
+#       respawn_with => plugin-name,
+#       pid => pid,
+#       options => [ option, option, option, ... ], # list of options from config file
+#       reload => bool, # if on, reload_plugins() will reload this job
+#       active => bool, # if on, this is active per the config file
+#    }
+# );
+my %jobs;
+
+# array of plugins that want to help us out; array of [ pluginname, [ opts ] ]
+my @helpers;
+
+# cached socket to our stat server
+my $sock;
+
+sub set {
+    # sends a statistic up to the stat server
+    $sock ||= IO::Socket::INET->new(PeerAddr => $opts{server},
+                                    PeerPort => $opts{port},);
+    fail("can't create socket: $!")
+        unless $sock;
+
+    # send this on up to the server
+    while (scalar(@_) >= 2) {
+        $sock->print("QSET $_[0] $_[1]\r\n");
+        shift @_; shift @_;
+    }
+}
+
+# keeps track of whether or not this is the first worker run
+sub worker {
+    # step 1: check through plugins directory to see what plugins have changed (or are new)
+    check_plugin_modtimes();
+
+    # step 2: check for a changed config file and load/reload as necessary
+    check_config();
+
+    # step 3: now that we know exactly what's changed, call master reloader
+    reload();
+
+    # step 4: run through any defined helpers so they can, well, help
+    foreach my $helpref (@helpers) {
+        my ($plugin, $opts) = @$helpref;
+        next unless $plugins{$plugin} && $plugins{$plugin}->{registered} &&
+                    $plugins{$plugin}->{code}->{helper};
+        eval {
+            $plugins{$plugin}->{code}->{helper}->($opts);
+        };
+        debug($@) if $@;
+    }
+}
+
+sub check_plugin_modtimes {
+    opendir DIR, $plugins_path
+        or fail("Unable to open plugins directory for reading");
+    foreach my $file (readdir(DIR)) {
+        next if $file =~ /^\./;
+        next unless $file =~ /^(.+)\.pl$/;
+        my $plugin = $1;
+
+        # create an empty record if this is the first time we've found this plugin
+        $plugins{$plugin} ||= {
+            modtime => 0,
+            reload => 1,
+            registered => 0,
+            children => [],
+            file => "$plugins_path/$file",
+        };
+
+        # compare modtime and mark for reload if necessary
+        my $mt = (stat($plugins{$plugin}->{file}))[9];
+        $plugins{$plugin}->{reload} = 1
+            if $mt > $plugins{$plugin}->{modtime};
+        $plugins{$plugin}->{modtime} = $mt;
+    }
+    closedir DIR;
+}
+
+sub check_config {
+    fail("Config file not found")
+        unless -e $opts{conf};
+    my $mt = (stat($opts{conf}))[9];
+    $opts{config_modtime} ||= 0;
+    reload_config()
+        if $mt > $opts{config_modtime};
+    $opts{config_modtime} = $mt;
+}
+
+sub reload_config {
+    debug("");
+    debug("configuration file reloading");
+
+    # we mark all jobs as inactive, so they get marked as active below
+    foreach (keys %jobs) {
+        $jobs{$_}->{active} = 0
+            unless $jobs{$_}->{respawn_with};
+    }
+
+    # clear out all helpers, as they should get readded.  they aren't in
+    # separate threads so it doesn't matter if we readd them every time.
+    @helpers = ();
+
+    open FILE, "<$opts{conf}"
+        or fail("Unable to open config file: $!");
+    foreach my $line (<FILE>) {
+        # ignore comments and clean surrounding whitespace
+        next if $line =~ /^\s*#/;
+        $line =~ s/^\s+//;
+        $line =~ s/[\s\r\n]+$//;
+        next unless length $line;
+
+        # shortcut; set some options
+        if ($line =~ /^(\w+)\s*=\s*(.+)$/) {
+            $opts{$1} = $2;
+            next;
+        }
+
+        # extract any options contained in (...) and going to the end of the line
+        my $optionstr;
+        if ($line =~ s/(?:\((.*)\))$//) {
+            $optionstr = $1;
+        }
+        my @options =
+            map { (s/^\s+//, s/\s$//, 1) ? $_ : undef } # now trim whitespace front and back
+            split(/,/, $optionstr);                     # split option string on commas
+
+        # now see if it has a job + plugin left, or just a plugin
+        if ($line =~ /^([-\w:.]+)\s*:\s*(\w+)$/) {
+            # this is a job definition
+            my ($job, $plugin) = ($1, $2);
+            fail("Error adding $job to job list")
+                unless add_job($job, $plugin, \@options);
+
+        } elsif ($line =~ /^(\w+)$/) {
+            # this is just a helper plugin
+            fail("Plugin $1 not defined")
+                unless $plugins{$1};
+
+            # push name of plugin on helper list
+            debug("helper from plugin $1 added");
+            push @helpers, [ $1, \@options ];
+
+        } else {
+            fail("Unknown format: $line($optionstr)");
+        }
+    }
+    close FILE;
+    debug("configuration file reloaded");
+}
+
+# main processor that goes through everything we have and reloads as necessary.  this
+# also handles reaping our children.
+sub reload {
+    # iterate over any dead children we might have picked up
+    while ((my $pid = waitpid(-1, WNOHANG)) > 0) {
+        if (my $job = delete($pids{$pid})) {
+            if ($jobs{$job}->{active}) {
+                debug("[$job] dead; pid = $pid; marking to reload; unexpected!");
+                $jobs{$job}->{reload} = 1;
+                $jobs{$job}->{pid} = 0;
+            } else {
+                debug("[$job] dead; pid = $pid; inactive job, NOT reloading");
+                delete $jobs{$job};
+            }
+        } else {
+            if (delete $todie{$pid}) {
+                debug("child death; pid = $pid; expected death, already reloaded");
+            } else {
+                debug("ERROR: $pid died but we have no record of it");
+            }
+        }
+    }
+
+    # iterate over plugins and reload as necessary
+    foreach my $plugin (sort keys %plugins) {
+        next unless $plugins{$plugin}->{reload};
+        debug("reloading plugin: $plugin");
+
+        # now require the file
+        my $file = $plugins{$plugin}->{file};
+        unless (my $ret = do $file) {
+            if ($@) {
+                warn "couldn't parse $file: $@\n";
+            } elsif (! defined $ret) {
+                warn "couldn't do $file: $!\n";
+            } else {
+                warn "couldn't run $file\n";
+            }
+            next;
+        }
+
+        # now mark any jobs with this plugin to reload
+        foreach my $job (keys %jobs) {
+            $jobs{$job}->{reload} = 1
+                if $jobs{$job}->{plugin} eq $plugin ||
+                   $jobs{$job}->{respawn_with} eq $plugin;
+        }
+    }
+
+    # now that we know all the plugins are loaded, iterate over jobs so we can get
+    # the plugins spawned and doing something
+    foreach my $job (sort keys %jobs) {
+        my $plugin = $plugins{$jobs{$job}->{plugin}};
+        fail("can't add job for plugin with no worker code: job = $job; plugin = $jobs{$job}->{plugin}")
+            unless ref $plugin->{code}->{worker};
+
+        # see if we need to kill off this job
+        unless ($jobs{$job}->{active}) {
+            debug("killing job: $job");
+            if ($jobs{$job}->{pid}) {
+                kill 15, $jobs{$job}->{pid};
+            } else {
+                delete $pids{$jobs{$job}->{pid}};
+                delete $jobs{$job};
+            }
+            next;
+        }
+
+        # now, the following path does a reload of this job if necessary
+        next unless $jobs{$job}->{reload} && $plugin->{registered};
+        debug("reloading job: $job");
+
+        # kill off this child if we had one
+        if ($jobs{$job}->{pid}) {
+            kill 15, $jobs{$job}->{pid};
+            delete $pids{$jobs{$job}->{pid}};
+            $todie{$jobs{$job}->{pid}} = 1;
+            debug("[$job] killing child; pid = $jobs{$job}->{pid}");
+            $jobs{$job}->{pid} = 0;
+        }
+
+        # bust out a child for this job
+        my $pid = fork;
+        fail("can't fork child: $!") if !defined $pid;
+        unless ($pid) {
+            # child path; do some basic setup and then call the worker
+            $0 .= " [$jobs{$job}->{plugin}: $job]";
+            $SIG{INT} = undef; # in case parent is in the foreground
+            $SIG{TERM} = undef; # no special handling for this
+
+            # call the child which should do all the work and return when it's done
+            $plugin->{code}->{worker}->($job, $jobs{$job}->{options});
+
+            # when the above returns, the worker is done, so we exit
+            exit 0;
+        }
+
+        # if we get here we're a parent, which means we need to mark this child as
+        # run and that we don't need to do anything more
+        $jobs{$job}->{pid} = $pid;
+        $jobs{$job}->{reload} = 0;
+        $pids{$pid} = $job;
+    }
+}
+
+# called by plugins to let us know that they want to be active.  they have to provide a
+# certain set of minimum functionality which we use.  we also import some things into
+# their namespace.
+sub register_plugin {
+    my ($plugin, $package, $opts) = @_;
+    return unless $plugin && $package && $plugins{$plugin} && $opts;
+
+    # make sure they gave us enough functions
+    unless (ref $opts->{register} && (ref $opts->{worker} || ref $opts->{helper})) {
+        debug("${plugin} did not provide minimum functionality: register and either worker or helper");
+        return;
+    }
+
+    # now create some aliases in their package so they can get to debug and set
+    eval "*${package}::debug = *main::debug;";
+    eval "*${package}::set = *main::set;";
+    eval "*${package}::add_job = *main::add_job;";
+    eval "*${package}::get_var = *main::get_var;";
+    eval "*${package}::mark_inactive_by_plugin = *main::mark_inactive_by_plugin;";
+
+    # call the plugin's register function so that it knows we've acknowledged its presence
+    unless ($opts->{register}->()) {
+        debug("${plugin}::register() didn't return true");
+        return;
+    }
+
+    # done reloading, mark as reloaded (so we don't reload next time)
+    $plugins{$plugin}->{code} = $opts;
+    $plugins{$plugin}->{reload} = 0;
+    $plugins{$plugin}->{registered} = 1;
+}
+
+# called by us and by helpers to add jobs to the list.  if called by a plugin, $respawn_with
+# must be specified and should be set to the name of the plugin.  otherwise, this job will
+# die the next time the config file is changed.
+sub add_job {
+    my ($job, $plugin, $options, $respawn_with) = @_;
+    fail("Bad input to add_job: job = $job, plugin = $plugin")
+        unless $job && $plugin;
+    $options ||= [];
+
+    # now print out debugging info
+    #debug("found config: $job: $plugin(" . join(', ', @$options) . ")");
+
+    # make sure this plugin exists
+    fail("Plugin $plugin not defined")
+        unless $plugins{$plugin};
+
+    # default %jobs setup
+    $jobs{$job} ||= {
+        plugin => $plugin,
+        pid => 0,
+        reload => 1,
+        options => $options,
+        respawn_with => $respawn_with,
+    };
+    $jobs{$job}->{active} = 1; # on unconditionally
+
+    # now determine if this job needs reloading
+    $jobs{$job}->{reload} = 1 unless $jobs{$job}->{pid};
+    if (scalar(@$options) == scalar(@{$jobs{$job}->{options}})) {
+        # compare options one by one, reload if any have changed
+        for (my $i = 0; $i < scalar(@$options); $i++) {
+            $jobs{$job}->{reload} = 1
+                if $options->[$i] ne $jobs{$job}->{options}->[$i];
+        }
+    } else {
+        # number of options changed, reload them all
+        $jobs{$job}->{reload} = 1;
+    }
+
+    # if reload, copy in new options just in case
+    if ($jobs{$job}->{reload}) {
+        @{$jobs{$job}->{options}} = @$options;
+    }
+
+    return 1;
+}
+
+# called by helpers to mark everything they've spawned as inactive before
+# they begin another round of adding jobs.  this is basically a way to say
+# to the gatherer that a process is dead.  if it's not re-added immediately
+# by the helper, it gets killed off in the next round of reaping.
+sub mark_inactive_by_plugin {
+    my $plugin = shift;
+
+    foreach my $job (keys %jobs) {
+        $jobs{$job}->{active} = 0
+            if $jobs{$job}->{respawn_with} eq $plugin;
+    }
+}
+
+# used by plugins to get access to variables set in the config file
+sub get_var {
+    return $opts{$_[0] || ''};
+}
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/plugins/config_generator.pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/plugins/config_generator.pl	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,113 @@
+# automatic monitoring config generator plugin for LiveJournal.  to use, add a
+# line like this to your SPUD config:
+#
+# config_generator(mysql, perlbal, memcached, mogstored, mogilefsd)
+#
+# written by Mark Smith <junior@danga.com>
+
+package ConfigGenPlugin;
+
+use strict;
+
+# called when we're loaded.  here we can do anything necessary to set ourselves
+# up if we want.  in this case we just load the LJ libraries.
+sub register {
+    # load up our livejournal files
+    use lib "$ENV{LJHOME}/cgi-bin";
+    require 'ljlib.pl';
+
+    # signal success if we get here
+    return 1;
+}
+
+# this is called and given the job name as the first parameter and an array ref of
+# options passed in as the second parameter.
+sub helper {
+    my $options = shift;
+
+    # put options into hashref for easy use later
+    my %opts;
+    foreach my $opt (@$options) {
+        my @parms = split(/\s*=\s*/, $opt);
+        my $job = shift(@parms);
+        $opts{$job} = \@parms;
+    }
+
+    # this is the main loop
+    LJ::start_request();
+
+    # mark all of our jobs as being inactive so that if we don't readd them below
+    # they'll get reaped automatically.
+    mark_inactive_by_plugin('config_generator');
+
+    # look for any perlbals that need monitoring jobs
+    if ($opts{perlbal}) {
+        while (my ($srvr, $ipaddr) = each %LJ::PERLBAL_SERVERS) {
+            add_job("perlbal.$srvr", "perlbal", [ $ipaddr, @{$opts{perlbal}} ], 'config_generator');
+        }
+    }
+
+    # and now memcache servers
+    if ($opts{memcached}) {
+        foreach my $host (@LJ::MEMCACHE_SERVERS) {
+            my $ipaddr = ref $host ? $host->[0] : $host;
+            add_job("memcached.$ipaddr", "memcached", [ $ipaddr, @{$opts{memcached}} ], 'config_generator');
+        }
+    }
+
+    # mogilefsd
+    if ($opts{mogilefsd} && %LJ::MOGILEFS_CONFIG) {
+        foreach my $ipaddr (@{$LJ::MOGILEFS_CONFIG{hosts}}) {
+            add_job("mogilefsd.$ipaddr", "mogilefsd", [ $ipaddr, @{$opts{mogilefsd}} ], 'config_generator');
+        }
+    }
+
+    # mogstored
+    if ($opts{mogstored} && %LJ::MOGILEFS_CONFIG) {
+        my $mgd = new MogileFS::Admin(hosts => $LJ::MOGILEFS_CONFIG{hosts});
+        if ($mgd) {
+            my (%hosthash, %devhash);
+
+            if (my $hosts = $mgd->get_hosts) {
+                foreach my $h (@$hosts) {
+                    $hosthash{$h->{hostid}} = $h;
+                }
+            }
+
+            if (my $devs = $mgd->get_devices) {
+                foreach my $d (@$devs) {
+                    $devhash{$d->{devid}} = $d;
+                }
+            }
+
+            foreach my $devid (keys %devhash) {
+                my $host = $hosthash{$devhash{$devid}->{hostid}};
+                add_job("mogstored.dev$devid", "mogstored",
+                        [ "http://$host->{hostip}:$host->{http_port}/dev$devid/usage", @{$opts{mogstored}} ],
+                        'config_generator');
+            }
+
+            foreach my $host (values %hosthash) {
+                my $ipaddr = "$host->{hostip}:7501";
+                add_job("mogstored.$ipaddr", "perlbal", [ $ipaddr, @{$opts{perlbal} || []} ], 'config_generator');
+            }
+        }
+    }
+
+    if ($opts{mysql} || $opts{db} || $opts{database}) {
+        
+    }
+
+    # done, call end request and sleep for a while
+    LJ::end_request();
+}
+
+# calls the registrar in the main program, giving them information about us.  this
+# has to be called as main:: or just ::register_plugin because we're in our own
+# package and we want to talk to the register function in the main namespace.
+main::register_plugin('config_generator', 'ConfigGenPlugin', {
+    register => \&register,
+    helper => \&helper,
+});
+
+1;
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/plugins/memcached.pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/plugins/memcached.pl	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,63 @@
+# memcache monitoring plugin for SPUD.  this is a simple plugin that gets stats
+# information from memcache and sticks it in the server.
+#
+# written by Mark Smith <junior@danga.com>
+
+package MemcachedPlugin;
+
+use strict;
+
+# called when we're loaded.  here we can do anything necessary to set ourselves
+# up if we want.
+sub register {
+    debug("memcached plugin registered");
+    return 1;
+}
+
+# this is called and given the job name as the first parameter and an array ref of
+# options passed in as the second parameter.
+sub worker {
+    my ($job, $options) = @_;
+    my $ipaddr = shift(@{$options || []});
+    my $interval = shift(@{$options || []}) || 5;
+    return unless $ipaddr;
+
+    # loop and get statistics every second
+    my $sock;
+    my $read_input = sub {
+        my @out;
+        while (<$sock>) {
+            s/[\r\n\s]+$//;
+            last if /^END/;
+            push @out, $_;
+        }
+        return \@out;
+    };
+    while (1) {
+        $sock ||= IO::Socket::INET->new(PeerAddr => $ipaddr, Timeout => 3);
+        return unless $sock;
+
+        # basic states command
+        print $sock "stats\r\n";
+        my $out = $read_input->();
+        foreach my $line (@$out) {
+            if ($line =~ /^STAT\s+([\w:]+)\s+(.+)$/) {
+                my ($stat, $val) = ($1, $2);
+                set("$job.$stat", $val);
+            }
+        }
+
+        # now sleep some between doing things
+        sleep $interval;
+    }
+}
+
+# calls the registrar in the main program, giving them information about us.  this
+# has to be called as main:: or just ::register_plugin because we're in our own
+# package and we want to talk to the register function in the main namespace.
+main::register_plugin('memcached', 'MemcachedPlugin', {
+    register => \&register,
+    worker => \&worker,
+});
+
+1;
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/plugins/mogilefsd.pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/plugins/mogilefsd.pl	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,64 @@
+# mogilefsd monitoring plugin.  this looks at the stats which is a very quick
+# operation for the mogilefsd server.  plans for this plugin are to start
+# monitoring replication, recent queries, etc.
+#
+# written by Mark Smith <junior@danga.com>
+
+package MogilefsdPlugin;
+
+use strict;
+
+# called when we're loaded.  here we can do anything necessary to set ourselves
+# up if we want.
+sub register {
+    debug("mogilefsd plugin registered");
+    return 1;
+}
+
+# this is called and given the job name as the first parameter and an array ref of
+# options passed in as the second parameter.
+sub worker {
+    my ($job, $options) = @_;
+    my $ipaddr = shift(@{$options || []});
+    my $interval = shift(@{$options || []}) || 5;
+    return unless $ipaddr;
+
+    # test plugin simply loops and once a second sets a "heartbeat"
+    my $sock;
+    my $read_input = sub {
+        my @out;
+        while (<$sock>) {
+            s/[\r\n\s]+$//;
+            last if /^\./;
+            push @out, $_;
+        }
+        return \@out;
+    };
+    while (1) {
+        $sock ||= IO::Socket::INET->new(PeerAddr => $ipaddr, Timeout => 3);
+        return unless $sock;
+
+        # basic states command
+        print $sock "!stats\r\n";
+        my $out = $read_input->();
+        foreach my $line (@$out) {
+            if ($line =~ /^([\w:]+)\s+(.+)$/) {
+                my ($stat, $val) = ($1, $2);
+                set("$job.$stat", $val);
+            }
+        }
+
+        # now sleep some between doing things
+        sleep $interval;
+    }
+}
+
+# calls the registrar in the main program, giving them information about us.  this
+# has to be called as main:: or just ::register_plugin because we're in our own
+# package and we want to talk to the register function in the main namespace.
+main::register_plugin('mogilefsd', 'MogilefsdPlugin', {
+    register => \&register,
+    worker => \&worker,
+});
+
+1;
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/plugins/mogstored.pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/plugins/mogstored.pl	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,74 @@
+# mogstored device monitoring plugin.  this fetches the 'usage' file from a device
+# and parses it, putting the information in the server.
+#
+# written by Mark Smith <junior@danga.com>
+
+package MogstoredPlugin;
+
+# packages we need
+use LWP::Simple;
+use Time::HiRes qw(gettimeofday tv_interval);
+
+# called when we're loaded.  here we can do anything necessary to set ourselves
+# up if we want.
+sub register {
+    debug("mogstored plugin registered");
+    return 1;
+}
+
+# this is called and given the job name as the first parameter and an array ref of
+# options passed in as the second parameter.
+sub worker {
+    my ($job, $options) = @_;
+    my $url = shift(@{$options || []});
+    my $interval = shift(@{$options || []}) || 60;
+    return unless $url;
+
+    # get stats every $interval seconds
+    while (1) {
+        my $t0 = [ gettimeofday ];
+        my $doc = get($url);
+        my $time = tv_interval($t0);
+        unless (defined $doc) {
+            set("$job.status", "fetch_failure");
+            sleep $interval;
+            next;
+        }
+
+        # split the doc and parse
+        my %stats;
+        foreach (split(/\r?\n/, $doc)) {
+            next unless /^(\w+):\s+(.+)$/;
+            my ($key, $val) = ($1, $2);
+            $stats{$key} = $val;
+        }
+
+        # if we couldn't parse it
+        unless ($stats{time} && $stats{total} && $stats{used} && $stats{available}) {
+            set("$job.status", "parse_failure");
+            sleep $interval;
+            next;
+        }
+
+        # mark this as successfully retrieved
+        set("$job.status", "success");
+        set("$job.time", $stats{time});
+        set("$job.used", $stats{used});
+        set("$job.available", $stats{available});
+        set("$job.total", $stats{total});
+        set("$job.delay", sprintf("%5.3f", $time));
+
+        # sleep a good 60 seconds, as this file doesn't change very often
+        sleep $interval;
+    }
+}
+
+# calls the registrar in the main program, giving them information about us.  this
+# has to be called as main:: or just ::register_plugin because we're in our own
+# package and we want to talk to the register function in the main namespace.
+main::register_plugin('mogstored', 'MogstoredPlugin', {
+    register => \&register,
+    worker => \&worker,
+});
+
+1;
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/plugins/perlbal.pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/plugins/perlbal.pl	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,65 @@
+# perlbal monitoring plugin.  very simple right now, this gets the output of the states
+# command and saves it to spud.  this is also used on the mogstored sidechannel, which
+# is a perlbal management interface.
+# 
+# written by Mark Smith <junior@danga.com>
+
+package PerlbalPlugin;
+
+use strict;
+
+# called when we're loaded.  here we can do anything necessary to set ourselves
+# up if we want.
+sub register {
+    debug("perlbal plugin registered");
+    return 1;
+}
+
+# this is called and given the job name as the first parameter and an array ref of
+# options passed in as the second parameter.
+sub worker {
+    my ($job, $options) = @_;
+    my $ipaddr = shift(@{$options || []});
+    my $interval = shift(@{$options || []}) || 5;
+    return unless $ipaddr;
+
+    # try to get states every second
+    my $sock;
+    my $read_input = sub {
+        my @out;
+        while (<$sock>) {
+            s/[\r\n\s]+$//;
+            last if /^\./;
+            push @out, $_;
+        }
+        return \@out;
+    };
+    while (1) {
+        $sock ||= IO::Socket::INET->new(PeerAddr => $ipaddr, Timeout => 3);
+        return unless $sock;
+
+        # basic states command
+        print $sock "states\r\n";
+        my $out = $read_input->();
+        foreach my $line (@$out) {
+            if ($line =~ /^(.+?)\s+(\w+)\s+(\d+)$/) {
+                my ($class, $state, $count) = ($1, $2, $3);
+                $class =~ s/^(.+::)//;
+                set("$job.$class.$state", $count);
+            }
+        }
+
+        # now sleep some between doing things
+        sleep $interval;
+    }
+}
+
+# calls the registrar in the main program, giving them information about us.  this
+# has to be called as main:: or just ::register_plugin because we're in our own
+# package and we want to talk to the register function in the main namespace.
+main::register_plugin('perlbal', 'PerlbalPlugin', {
+    register => \&register,
+    worker => \&worker,
+});
+
+1;
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/plugins/test.pl
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/plugins/test.pl	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,37 @@
+# test/heartbeat plugin for SPUD statistic gathering system
+# written by Mark Smith <junior@danga.com>
+
+# this is mostly a demonstration of how to create a very simple plugin for SPUD.
+# more complex examples can be found elsewhere in the plugins directory.
+
+# doesn't matter what package you're in
+package TestPlugin;
+
+# called when we're loaded.  here we can do anything necessary to set ourselves
+# up if we want.
+sub register {
+    debug("test plugin registered");
+    return 1;
+}
+
+# this is called and given the job name as the first parameter and an array ref of
+# options passed in as the second parameter.
+sub worker {
+    my ($job, $options) = @_;
+
+    # test plugin simply loops and once a second sets a "heartbeat"
+    while (1) {
+        set("test.$job" => 1);
+        sleep 1;
+    }
+}
+
+# calls the registrar in the main program, giving them information about us.  this
+# has to be called as main:: or just ::register_plugin because we're in our own
+# package and we want to talk to the register function in the main namespace.
+main::register_plugin('test', 'TestPlugin', {
+    register => \&register,
+    worker => \&worker,
+});
+
+1;
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/replicator
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/replicator	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,224 @@
+#!/usr/bin/perl
+#
+# Danga's Statistics Server Replicator
+# Very lightweight program that replicates data from one statistic server (subscribe *)
+# to another one.  Very, very simple and lightweight.  Uses open3() to use the ssh command
+# to connect to a remote statistics server.
+#
+# Command line options:
+#
+#    -c STRING  set what config file to use for options
+#    -D         if present, tell the server to daemonize
+#
+# Configuration file format:
+#
+#    server = STRING          location of statistics server
+#    port = INT               port number server listens on
+#    ssh_host = STRING        host of remote SSH to tunnel through
+#    ssh_port = INT           port SSH daemon is on
+#    ssh_key = STRING         filename to use for our private key
+#    ssh_user = STRING        username to identify as to SSH server
+#
+# Copyright 2004, Danga Interactive
+#
+# Authors:
+#   Mark Smith <marksmith@danga.com>
+#
+# License:
+#   undecided.
+#
+
+# uses
+use strict;
+use IO::Socket;
+use IO::Select;
+use IPC::Open3;
+use Getopt::Long;
+use Carp;
+use POSIX ":sys_wait_h";
+use Socket qw(PF_INET IPPROTO_TCP SOCK_STREAM);
+use Danga::Socket;
+
+# Command-line options will override
+my ($daemonize, $conf_file);
+Getopt::Long::Configure( "bundling" );
+Getopt::Long::GetOptions(
+    'D|daemon'   => \$daemonize,
+    'c|config=s' => \$conf_file,
+);
+die "You must at least specify --config=FILENAME for me to work.\n" unless $conf_file;
+die "File '$conf_file' doesn't exist for configuration.\n" unless -e $conf_file;
+
+# parse the config file
+my %config;
+open FILE, "<$conf_file"
+    or die "Unable to open config file: $!\n";
+while (my $line = <FILE>) {
+    if ($line =~ /^\s*([^#].*)\s*=\s*(.*)\s*/) {
+        my ($l, $r) = (trim($1), trim($2));
+        if ($l eq 'server') { $config{to}{host} = $r; }
+        elsif ($l eq 'port') { $config{to}{port} = $r+0; }
+        elsif ($l eq 'ssh_key') { $config{from}{key} = $r; }
+        elsif ($l eq 'ssh_host') { $config{from}{host} = $r; }
+        elsif ($l eq 'ssh_port') { $config{from}{port} = $r+0; }
+        elsif ($l eq 'ssh_user') { $config{from}{username} = $r; };
+    }
+}
+close FILE;
+
+# daemonize?
+daemonize() if $daemonize;
+
+# connect to stats server we're replicating TO.  messy.
+my $dsock = new_connection()
+    or die "Can't get initial socket... exiting.\n";
+
+# now setup our ssh open3 sockets
+my ($reader, $writer);
+my $sshpid = open3($writer, $reader, $reader, 'ssh', '-C', '-tt', '-i', $config{from}{key},
+                   '-p', $config{from}{port}, '-l', $config{from}{username}, $config{from}{host});
+print $writer "sub *\n";
+
+# always kill off our SSH connection
+$SIG{TERM} = sub { kill 15, $sshpid; exit 0; };
+$SIG{INT} = sub { kill 15, $sshpid; exit 0; };
+
+# variables used later
+my $readbuf;
+
+# this is our main reading loop
+my $sobj = new IO::Select;
+$sobj->add($reader);
+
+# post event loop
+my $postloop = sub {
+    # check if somehow ssh died on us? :-/
+    my $pid = waitpid -1, WNOHANG;
+    if ($pid == $sshpid) {
+        # sleep a few seconds and try to spawn a new one
+        $sshpid = 0;
+        while (!$sshpid) {
+            print "Lost SSH connection... sleeping 5 seconds.\n";
+            sleep 5;
+            $sshpid = open3($writer, $reader, $reader, 'ssh', '-C', '-tt', '-i', $config{from}{key},
+                            '-p', $config{from}{port}, '-l', $config{from}{username}, $config{from}{host});
+            print $writer "sub *\n";
+        }
+    }
+
+    # if our parent socket is closed...
+    unless ($dsock && !$dsock->{closed}) {
+        # create a new one if we can
+        print "Lost SPUD connection... reconnecting...\n";
+        $dsock = new_connection();
+        unless ($dsock) {
+            print "\tUnable to connect... pausing a second.\n";
+            sleep 1;
+            return 1;
+        }
+    }
+
+    # see if we can read from our socket yet
+    my @ready = $sobj->can_read(0.1);
+    return 1 unless @ready;
+
+    # must be ready to read
+    my $bytes = sysread $reader, $readbuf, 1024, length $readbuf;
+    while ($readbuf =~ s/(.+?)\r?\n//) {
+        my $line = $1;
+        next unless $line =~ /^set/i;
+        $dsock->write("Q$line\r\n");
+    }
+    return 1;
+};
+
+# now configure the client
+Client->SetLoopTimeout(100); # 100 milliseconds timeout
+Client->SetPostLoopCallback($postloop); # have it call us
+
+# now run the event loop
+Client->EventLoop();
+
+# kill off our child too
+kill 15, $sshpid;
+print "replicator terminating\n";
+
+# daemonizer routine
+sub daemonize {
+    my($pid, $sess_id, $i);
+
+    ## Fork and exit parent
+    if ($pid = fork) { exit 0; }
+
+    ## Detach ourselves from the terminal
+    croak "Cannot detach from controlling terminal"
+        unless $sess_id = POSIX::setsid();
+
+    ## Prevent possibility of acquiring a controling terminal
+    $SIG{'HUP'} = 'IGNORE';
+    if ($pid = fork) { exit 0; }
+
+    ## Change working directory
+    chdir "/";
+
+    ## Clear file creation mask
+    umask 0;
+
+    ## Close open file descriptors
+    close(STDIN);
+    close(STDOUT);
+    close(STDERR);
+
+    ## Reopen stderr, stdout, stdin to /dev/null
+    open(STDIN,  "+>/dev/null");
+    open(STDOUT, "+>&STDIN");
+    open(STDERR, "+>&STDIN");
+}
+
+# little trimming sub
+sub trim {
+    my $res = shift;
+    $res =~ s/^\s+//;
+    $res =~ s/\s+$//;
+    return $res;
+}
+
+# connect anew to the SPUD server we're replicating to
+# NOTE: can return undef if we can't get to the server!
+sub new_connection {
+    my $sock;
+    socket $sock, PF_INET, SOCK_STREAM, IPPROTO_TCP;
+    die "error: can't make socket\n"
+        unless $sock && defined fileno($sock);
+    IO::Handle::blocking($sock, 0);
+    connect $sock, Socket::sockaddr_in($config{to}{port}, Socket::inet_aton($config{to}{host}));
+    my $dsock = Client->new($sock)
+        or return undef;
+    $dsock->watch_write(1);
+    $dsock->watch_read(1);
+    return $dsock;
+}
+
+###########################################################################
+### Client class for use in processing input/output
+package Client;
+
+use base "Danga::Socket";
+
+sub event_read {
+    # read and toss, we don't care about input from the user here
+    my Client $self = $_[0];
+    my $bref = $self->read;
+}
+
+sub event_err {
+    # connection died?
+    my Client $self = $_[0];
+    $self->close('event_err');
+}
+
+sub event_hup {
+    # connection to server died...
+    my Client $self = $_[0];
+    $self->close('event_hup');
+}
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/rrd-storage
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/rrd-storage	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,132 @@
+#!/usr/bin/perl
+
+use strict;
+use RRDs;
+use IO::Socket::INET;
+use Getopt::Long;
+use Danga::Daemon;
+
+# parse out command line options
+my ($server, $port, $rrd_path);
+my $res = GetOptions(
+    "server=s"   => \$server,
+    "port=i"     => \$port,
+    "rrd-path=s" => \$rrd_path,
+);
+die "Unable to parse options.\n" unless $res;
+
+# make sure we have a minimum set of options
+die "Please specify a path to access the RRD files in --rrd-path.\n"
+    unless $rrd_path;
+die "RRD path $rrd_path does not exist or is not a directory.\n"
+    unless -e $rrd_path && -d $rrd_path;
+die "Requires --server (and a --port if it's not 9107).\n"
+    unless $server;
+
+# setup more internal variables
+$port ||= 9107;
+my $spud_server = "$server:$port";
+
+# connect to local spud server
+my $sock = IO::Socket::INET->new(PeerAddr => $spud_server)
+    or die "Can't connect to server: $!\n";
+print $sock "sub *\r\n";
+
+# data storage
+my $shutdown = 0;
+my $lastpurge = time();
+my (%data, %exists);
+
+# now we want to daemonize
+Danga::Daemon::daemonize(
+    \&worker,
+    {
+        interval => 1,
+    }
+);
+
+# process updates as they come in
+sub worker {
+    # try to reconnect?
+    debug("Worker start...");
+    unless ($sock) {
+        $sock = IO::Socket::INET->new(PeerAddr => $spud_server)
+            or return;
+        print $sock "sub *\r\n";
+    }
+
+    # loop and process what they're giving us
+    debug("Beginning main loop...");
+    while (<$sock>) { 
+        my $line = $_;
+        $line =~ s/[\r\n]+$//;
+        next unless $line =~ /^set\s+(\d+)\s+(\S+)\s+(\d+)$/i;
+        my ($what, $time, $dpoint) = ($2, $1, $3);
+
+        # fix up the colons in $what to be underscores :/
+        $what =~ s/:/_/g;
+        push @{$data{$what} ||= []}, [ $time, $dpoint ];
+
+        # and if necessary, do a purge
+        if ($Danga::Daemon::stop || ($lastpurge + 60 < time())) {
+            do_purge();
+            $lastpurge = time();
+        }
+
+        # done if we were told to shutdown
+        return if $Danga::Daemon::stop;
+    }
+
+    # got undef from $sock... so undef $sock itself
+    debug("Oops, lost connection to SPUD server...");
+    $sock = undef;
+}
+
+sub do_purge {
+    debug("** Beginning purge...");
+
+    # dump to file
+    my ($keys, $count);
+    foreach my $what (keys %data) {
+        # update this particular file
+        $keys++;
+        my $fn = "$rrd_path/$what";
+        unless ($exists{$fn}) {
+            unless (-e $fn) {
+                RRDs::create($fn,
+                    "--start", "-120",            # up to two minutes ago... should be enough
+                    "--step", "5",                # data is expected to be 5 seconds apart
+                    "DS:val:GAUGE:10:U:U",        # all data in 'val', 10 seconds before UNKNOWN
+                    'RRA:AVERAGE:0.5:1:1440',     # past 2 hours of data
+                    'RRA:AVERAGE:0.5:60:288',     # 5 minute averages for the day
+                    'RRA:AVERAGE:0.5:360:336',    # 30 minute averages for a week
+                    'RRA:AVERAGE:0.5:720:720',    # 1 hour averages for 30 days
+                    'RRA:AVERAGE:0.5:17280:365',  # 1 day averages for a year
+                    'RRA:AVERAGE:0.5:120960:520', # 1 week averages for 10 years
+                );
+                if (my $err = RRDs::error) {
+                    die "Error creating RRD file: $err\n";
+                }
+            }
+            $exists{$fn} = 1;
+        }
+
+        # now pipe out the updates
+        my @updates;
+        foreach my $row (@{$data{$what}}) {
+            $count++;
+            push @updates, "$row->[0]:$row->[1]";
+        }
+        RRDs::update($fn, @updates);
+        #debug("\t$what: ", scalar(@updates), " written");
+        if (my $err = RRDs::error) {
+            warn "WARNING: Error updating $fn: $err\n";
+        }
+    }
+    debug("\tupdates processed: $count");
+    debug("\tfiles touched: $keys");
+    debug("\tratio: %.2f updates per file\n", ($count / $keys));
+
+    # and now that we're done with that old data ...
+    %data = ();
+}
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/server
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/server	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,465 @@
+#!/usr/bin/perl
+#
+# Danga's Statistics Server
+# This program listens on a port and acts as a very generic data server.  It lets you get
+# and set data as well as subscribe to information flows given glob patterns.  Commands:
+#
+#   get <what>
+#       get some particular data.  also gives you the time this data was set.  you can also
+#       specify a pseudo-regexp here in the same format as the subscribe command.
+#   [q]set [time] <what> <data>
+#       set what to be data.  if you specify a time, it will set the time you set it as that
+#       time.  also, if you specify qset, you will not be given an OK prompt afterwards.
+#   quickset <on|off>
+#       if you turn quickset on, sets will be processed without processing the subscriptions,
+#       instead waiting until you turn quickset off, at which point all pending subscriptions
+#       will be processed.
+#   subscribe <pattern>
+#       pattern is some combination of letters and asterisks. "foo.*" subscribes to anything
+#       that happens to start with "foo.".  you can also subscribe to simply "*" to get all
+#       information that gets set.  or, "*.system" to see everything relating to a system,
+#       e.g. mysql, memcache, etc.
+#   list
+#       list your subscription patterns.  if you want to remove subscriptions, you need to
+#       reconnect to the server.
+#   listall
+#       list all active subscriptions.  (see what other people are watching, if you care.)
+#   quit
+#       disconnect your socket nicely.
+#   shutdown
+#       kill the statistics server.
+#   uptime
+#       print out the current server statistics... number of keys and subscriptions and the
+#       time the server has been running.
+#
+# Command line options:
+#
+#    -p INT     set the port the server should listen on
+#    -D         if present, tell the server to daemonize
+#
+# Copyright 2004, Danga Interactive
+#
+# Authors:
+#   Mark Smith <marksmith@danga.com>
+#
+# License:
+#   undecided.
+#
+
+package StatServ;
+
+use strict;
+use Getopt::Long;
+use IO::Socket::INET;
+use POSIX;
+use Carp;
+use Socket qw(IPPROTO_TCP SO_KEEPALIVE TCP_NODELAY SOL_SOCKET);
+use constant TCP_KEEPIDLE  => 4; # Start keeplives after this period
+use constant TCP_KEEPINTVL => 5; # Interval between keepalives
+use constant TCP_KEEPCNT   => 6; # Number of keepalives before death
+
+# declare our globals/defaults
+my $conf_port = 9107;
+my $daemonize = 0;
+my $uptime = time();
+my $quickset = 0;
+my $lastcleantime = time();
+
+# internal data, not to be touched externally
+my %data; # { "fact-name" => [ [ set-time, data ], [ set-time, data ], ... ] }
+my @sortedkeys; # sorted keys (for pretty output)
+my %subs; # { "fact-name" => [ $subscriber, $subscriber, ... ] }
+my @sublist; # list of subscriptions
+
+# Command-line options will override
+Getopt::Long::Configure('bundling');
+Getopt::Long::GetOptions(
+    'p|port=i' => \$conf_port,
+    'D|daemon' => \$daemonize,
+);
+
+# establish SERVER socket, bind and listen.
+my $server = IO::Socket::INET->new(LocalPort => $conf_port,
+                                   Type      => SOCK_STREAM,
+                                   Proto     => 'tcp',
+                                   Blocking  => 1,
+                                   Reuse     => 1,
+                                   Listen    => 10)
+    or die "Error creating socket: $@\n";
+
+# make socket nonblocking
+IO::Handle::blocking($server, 0);
+
+my $accept_handler = sub {
+    my $csock = $server->accept();
+    return unless $csock;
+
+    IO::Handle::blocking($csock, 0);
+    setsockopt($csock, IPPROTO_TCP, TCP_NODELAY, pack("l", 1)) or die;
+
+    # Enable keep alive
+    (setsockopt($csock, SOL_SOCKET, SO_KEEPALIVE,   pack("l", 1))  &&
+     setsockopt($csock, IPPROTO_TCP, TCP_KEEPIDLE,  pack("l", 30)) &&
+     setsockopt($csock, IPPROTO_TCP, TCP_KEEPCNT,   pack("l", 10)) &&
+     setsockopt($csock, IPPROTO_TCP, TCP_KEEPINTVL, pack("l", 30)) &&
+     1
+    ) || die "Couldn't set keep-alive settings on socket (Not on Linux?)";
+
+    my $client = Client->new($csock);
+    $client->watch_read(1);
+};
+
+# daemonize if we should
+daemonize() if $daemonize;
+
+Client->OtherFds(fileno($server) => $accept_handler);
+Client->EventLoop();
+exit 0;
+
+#############################################################################
+###### SUBROUTINES ##########################################################
+#############################################################################
+
+sub daemonize {
+    my($pid, $sess_id, $i);
+
+    ## Fork and exit parent
+    if ($pid = fork) { exit 0; }
+
+    ## Detach ourselves from the terminal
+    croak "Cannot detach from controlling terminal"
+        unless $sess_id = POSIX::setsid();
+
+    ## Prevent possibility of acquiring a controling terminal
+    $SIG{'HUP'} = 'IGNORE';
+    if ($pid = fork) { exit 0; }
+
+    ## Change working directory
+    chdir "/";
+
+    ## Clear file creation mask
+    umask 0;
+
+    ## Close open file descriptors
+    close(STDIN);
+    close(STDOUT);
+    close(STDERR);
+
+    ## Reopen stderr, stdout, stdin to /dev/null
+    open(STDIN,  "+>/dev/null");
+    open(STDOUT, "+>&STDIN");
+    open(STDERR, "+>&STDIN");
+}
+
+# internal setter that has the logic that actually handles setting key/value
+# pairs.  also performs notifications, unless specifically told otherwise.
+sub _set {
+    my ($time, $key, $val, $nosubs) = @_;
+    $nosubs ||= 0;
+    
+    # push onto array
+    unless (defined $data{$key}) {
+        $data{$key} = [];
+        @sortedkeys = sort keys %data;
+    }
+    unshift @{$data{$key}}, [ $time, "$val" ];
+    pop @{$data{$key}} if @{$data{$key}} > 100;
+
+    # handle subscriptions
+    unless ($nosubs) {
+        unless (defined $subs{$key}) {
+            $subs{$key} = []; # define it and set blank if it's not defined
+            foreach my $sub (@sublist) {
+                push @{$subs{$key}}, $sub->[0]
+                    if $key =~ /$sub->[1]/i;
+            }
+        }
+        $_->set_line("$time $key $val") foreach @{$subs{$key}};
+    }   
+
+    # done
+    return 1;
+}
+
+# internal getter.  gets a value for a single key.
+sub _get {
+    my $key = shift;
+    return wantarray ? () : undef unless $data{$key};
+
+    # now, get data. return all points? or just most recent?
+    return $data{$key} if wantarray;
+    return undef unless @{$data{$key}};
+    return $data{$key}->[0]->[1]; # text of most recent value
+}
+
+# internal incrementers.  simply gets a value and sets it to val +/- 1.
+# this is used by stats within the server only, and not exposed to the
+# outside world as of right now.
+sub _incr { _set(time(), $_[0], _get($_[0])+1, 1); }
+sub _decr { _set(time(), $_[0], _get($_[0])-1, 1); }
+
+#####################################################################
+### C L I E N T   C L A S S
+#####################################################################
+package Client;
+
+use Danga::Socket ();
+use base qw{Danga::Socket};
+use fields qw(read_buf t_out_buf do_buffer);
+
+sub new {
+    my Client $self = shift;
+    $self = fields::new($self) unless ref $self;
+    $self->SUPER::new( @_ );
+
+    StatServ::_incr("server.clients.total");
+    return $self;
+}
+
+# Client
+sub event_read {
+    my $self = shift;
+
+    my $bref = $self->read(1024);
+    return $self->close() unless defined $bref;
+    $self->{read_buf} .= $$bref;
+
+    StatServ::_incr("server.clients.event_reads");
+    while ($self->{read_buf} =~ s/^(.+?)\r?\n//) {
+        my $line = $1;
+        $self->process_line( $line );
+    }
+}
+
+# Client
+sub event_err { StatServ::_incr("server.clients.event_errs"); my $self = shift; $self->close; }
+sub event_hup { StatServ::_incr("server.clients.event_hups"); my $self = shift; $self->close; }
+
+sub process_line {
+    my Client $self = shift;
+    my $line = shift;
+
+    StatServ::_incr("server.clients.process_lines");
+
+    # clean out closed clients every few seconds
+    my $now = time();
+    if ($lastcleantime + 10 < $now) {
+        foreach my $key (keys %subs) {
+            @{$subs{$key}} = grep { !$_->{closed} } @{$subs{$key}};
+        }
+        @sublist = grep { !$_->[0]->{closed} } @sublist;
+        $lastcleantime = $now;
+    }
+
+    if ($line =~ /^(\w+)\s*(.*)/) {
+        my ($cmd, $args) = ($1, $2);
+        $cmd = lc($cmd);
+        my ($one, $two);
+        if ($args =~ /^([-\w.:]+)\s+(.+)/) {
+            ($one, $two) = ($1, $2);
+        }
+
+        # statistics keeping
+        StatServ::_incr("server.clients.commands");
+
+        # see if this is a command we know about
+        if ($cmd eq 'get') {
+            # get something
+            StatServ::_incr("server.com_$cmd");
+            return $self->err_line('no_args') unless $args;
+            if (defined $data{$args} && ref $data{$args} eq 'ARRAY') {
+                my $ct = 0;
+                $self->begin_output();
+                foreach my $row (@{$data{$args}}) {
+                    $ct++;
+                    $self->ok_line("$ct $row->[0] $row->[1]");
+                }
+                $self->ok_line('done');
+                return $self->end_output();
+            } else {
+                # see if this should be used as a regular expression
+                if ($args =~ /[.*]/) {
+                    $args =~ s/\./\\./g;
+                    $args =~ s/\*/.*/g;
+                    $self->begin_output();
+                    foreach my $key (@sortedkeys) {
+                        if ($key =~ /$args/i) {
+                            my $first = $data{$key}->[0];
+                            $self->ok_line("$key $first->[0] $first->[1]");
+                        }
+                    }
+                    $self->ok_line('done');
+                    return $self->end_output();
+                }
+                return $self->err_line("not_found");
+            }
+
+        } elsif ($cmd =~ /^(q)?set$/) {
+            # set something
+            StatServ::_incr("server.com_$cmd");
+            return $self->err_line('need_two_args') unless defined $one && defined $two;
+            my $quiet = $1 eq 'q' ? 1 : 0;
+
+            # see if $one happens to be a number?  that means they gave us a time to use.
+            my $time;
+            if ($one =~ /^\d+$/ && $two =~ /^([-\w.:]+)\s+(.+)$/) {
+                $time = $one + 0;
+                ($one, $two) = ($1, $2);
+            }
+            $time = time() unless defined $time;
+
+            # push data onto front
+            StatServ::_set($time, $one, $two);
+
+            # all done
+            return $quiet ? 1 : $self->ok_line();
+
+        } elsif ($cmd =~ /^sub/) {
+            # subscribe to something
+            # convert "*.mysql" to ".*\.mysql", etc.
+            StatServ::_incr("server.com_$cmd");
+            return $self->err_line('no_args') unless $args;
+            $args =~ s/\./\\./g;
+            $args =~ s/\*/.*/g;
+            return $self->ok_line($args) # no dupes in @sublist!
+                if grep { $args eq $_->[1] && $self == $_->[0] } @sublist;
+            push @sublist, [ $self, $args ];
+
+            # now see what subscriptions this matches
+            foreach my $key (keys %subs) {
+                if ($key =~ /$args/i) {
+                    # no dupes!
+                    next if grep { $_ == $self } @{$subs{$key}};
+                    push @{$subs{$key}}, $self;
+                }
+            }
+
+            # return okay to the person who set something
+            return $self->ok_line($args);
+
+        } elsif ($cmd =~ /^list(all)?$/) {
+            # list out your subscriptions
+            StatServ::_incr("server.com_$cmd");
+            foreach my $sub (@sublist) {
+                $self->ok_line("$sub->[0] $sub->[1]")
+                    if $1 eq 'all' || $sub->[0] == $self;
+            }
+            return $self->ok_line('done');
+
+        } elsif ($cmd eq 'shutdown') {
+            # kill ourselves
+            StatServ::_incr("server.com_$cmd");
+            exit 0;
+
+        } elsif ($cmd eq 'uptime') {
+            # figure out our uptime
+            StatServ::_incr("server.com_$cmd");
+            my $c = time() - $uptime;
+            my ($d, $h, $m, $s) = (int($c / 86400), int(($c % 86400) / 3600),
+                                   int(($c % 3600) / 60), int($c % 60));
+            my $l = sprintf 'stats-server up %d days %02d:%02d:%02d', $d, $h, $m, $s;
+            $self->begin_output();
+            $self->ok_line($l);
+            $l = sprintf '%d subscriptions, %d total keys', scalar(@sublist), scalar(@sortedkeys);
+            $self->ok_line($l);
+            $self->ok_line('done');
+            return $self->end_output();
+
+        } elsif ($cmd eq 'quickset') {
+            StatServ::_incr("server.com_$cmd");
+            return $self->err_line('not_on_or_off') unless $args eq 'on' || $args eq 'off';
+
+            # now set it?
+            $quickset = $args eq 'on' ? 1 : 0;
+            return 1;
+
+        } elsif ($cmd eq 'quit') {
+            # simply quit ... at some point we should clear out subscriptions that they
+            # have setup... but until then? nah.
+            StatServ::_incr("server.com_$cmd");
+            return $self->close;
+
+        }
+    }
+
+    # oops, don't know what they wanted
+    return $self->err_line('unknown_command');
+}
+
+sub ok_line {
+    my Client $self = shift;
+    my $args = shift;
+    if ($self->{do_buffer}) {
+        StatServ::_incr("server.lines.ok.buffered");
+        $self->{t_out_buf} .= "OK $args\r\n";
+    } else {
+        StatServ::_incr("server.lines.ok.immediate");
+        $self->write("OK $args\r\n");
+    }
+    return 1;
+}
+
+sub set_line {
+    my Client $self = shift;
+    my $args = shift;
+    if ($self->{do_buffer}) {
+        StatServ::_incr("server.lines.set.buffered");
+        $self->{t_out_buf} .= "SET $args\r\n";
+    } else {
+        StatServ::_incr("server.lines.set.immediate");
+        $self->write("SET $args\r\n");
+    }
+    return 1;
+}
+
+sub err_line {
+    my Client $self = shift;
+    my $err_code = shift;
+    my $err_text = {
+        'unknown_command' => "Unknown server command",
+        'not_found' => "Information not in record",
+        'no_args' => "No arguments provided",
+        'need_two_args' => "Two arguments needed",
+        'no_subscriptions' => "No subscriptions found",
+        'not_on_or_off' => "Argument wasn't of form 'on' or 'off'",
+    }->{$err_code};
+
+    if ($self->{do_buffer}) {
+        StatServ::_incr("server.lines.err.buffered");
+        $self->{t_out_buf} .= "ERR $err_code $err_text\r\n";
+    } else {
+        StatServ::_incr("server.lines.err.immediate");
+        $self->write("ERR $err_code $err_text\r\n");
+    }
+    return 0;
+}
+
+sub begin_output {
+    my Client $self = shift;
+    StatServ::_incr("server.lines.begin_outputs");
+    $self->{do_buffer} = 1;
+    return 1;
+}
+
+sub end_output {
+    my Client $self = shift;
+    StatServ::_incr("server.lines.end_outputs");
+    $self->{do_buffer} = 0;
+    $self->write($self->{t_out_buf});
+    $self->{t_out_buf} = '';
+    return 1;
+}
+
+sub eurl
+{
+    my $a = $_[0];
+    $a =~ s/([^a-zA-Z0-9_\,\-.\/\\\: ])/uc sprintf("%%%02x",ord($1))/eg;
+    $a =~ tr/ /+/;
+    return $a;
+}
+
+# Local Variables:
+# mode: perl
+# c-basic-indent: 4
+# indent-tabs-mode: nil
+# End:
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/bin/wrapper
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/bin/wrapper	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,162 @@
+#!/usr/bin/perl
+#
+# Danga's Statistics Wrapper
+# This little program simply runs a command and pipes the output to a
+# statistics server so that you can see the output of the command.  Data
+# will be stored in $KEY.output and $KEY.status, so you can determine
+# exactly how long a run took.
+#
+# Command line options:
+#
+#    -s STRING    server to connect to
+#    -p INT       port statistics server is running on
+#    -k STRING    key to put data in ($KEY.status, $KEY.output)
+#    -e STRING    command to execute
+#    -c STRING    file to load config data from
+#
+# Configuration file format:
+#
+#    server = STRING     location of server
+#    port = INT          port of server
+#
+# Copyright 2004, Danga Interactive
+#
+# Authors:
+#   Mark Smith <marksmith@danga.com>
+#
+# License:
+#   undecided.
+#
+
+# uses
+use strict;
+use IO::Socket;
+use IPC::Open3;
+use Getopt::Long;
+use Carp;
+use POSIX;
+
+# Command-line options will override
+my ($daemonize, $conf_file, $key, $host, $port, $cmd);
+Getopt::Long::Configure( "bundling" );
+Getopt::Long::GetOptions(
+    'D|daemon'    => \$daemonize,
+    'c|config=s'  => \$conf_file,
+    'k|key=s'     => \$key,
+    'p|port=i'    => \$port,
+    's|server=s'  => \$host,
+    'e|exec=s' => \$cmd,
+);
+
+# if we don't have all the data we need, parse a config file
+die "No key (-k KEY) specified.\n" unless $key;
+unless ($port && $host) {
+    die "You must at least specify --config=FILENAME for me to work.\n" unless $conf_file;
+    die "File '$conf_file' doesn't exist for configuration.\n" unless -e $conf_file;
+
+    # parse the config file
+    open FILE, "<$conf_file"
+        or die "Unable to open config file: $!\n";
+    while (my $line = <FILE>) {
+        if ($line =~ /^\s*([^#].*)\s*=\s*(.*)\s*/) {
+            my ($l, $r) = (trim($1), trim($2));
+            if ($l eq 'server') { $host = $r; }
+            elsif ($l eq 'port') { $port = $r+0; }
+        }
+    }
+    close FILE;
+}
+
+# if cmd was specified, use that
+@ARGV = split /\s+/, $cmd if $cmd;
+
+# error checking
+die "No server to connect to.\n" unless $port && $host;
+die "No arguments to run.\n" unless @ARGV;
+die "File to run ($ARGV[0]) doesn't seem to exist.\n" unless -e $ARGV[0];
+
+# daemonize?
+daemonize() if $daemonize;
+
+# connect to stats server we're replicating TO
+my $socket = IO::Socket::INET->new(Proto => 'tcp',
+                                   PeerAddr => $host,
+                                   PeerPort => $port,)
+    or die "Unable to connect to local stats server ($host:$port): $!\n";
+$socket->autoflush(1);
+
+# now setup our open3 socket
+my ($reader, $writer);
+my $procpid = open3($writer, $reader, $reader, @ARGV);
+
+# parent pid
+my $ppid = $$;
+
+# now fork
+my $kidpid;
+die "Unable to fork: $!\n" unless defined($kidpid = fork());
+
+# basic parts that handle reading/writing from the two sides
+if ($kidpid) {
+    # parent
+    while (defined (my $line = <$socket>)) {
+        # we do this to flush the read buffer, but we don't need to
+        # echo this information to anybody, as all of this is going
+        # to be in the form of "OK" and such saying that data got set
+    }
+    kill("TERM" => $kidpid); # death to the children
+    waitpid $kidpid, 0;
+} else {
+    # child
+    print $socket "QSET $key.status started\n";
+    while (defined (my $line = <$reader>)) {
+        # take the output and set it
+        $line = trim($line);
+        print $socket "QSET $key.output $line\n";
+    }
+    print $socket "QSET $key.status finished\n";
+    $socket->close;
+
+    # now kill off our parent
+    kill("TERM" => $ppid);
+}
+
+# daemonizer routine
+sub daemonize {
+    my($pid, $sess_id, $i);
+
+    ## Fork and exit parent
+    if ($pid = fork) { exit 0; }
+
+    ## Detach ourselves from the terminal
+    croak "Cannot detach from controlling terminal"
+        unless $sess_id = POSIX::setsid();
+
+    ## Prevent possibility of acquiring a controling terminal
+    $SIG{'HUP'} = 'IGNORE';
+    if ($pid = fork) { exit 0; }
+
+    ## Change working directory
+    chdir "/";
+
+    ## Clear file creation mask
+    umask 0;
+
+    ## Close open file descriptors
+    close(STDIN);
+    close(STDOUT);
+    close(STDERR);
+
+    ## Reopen stderr, stdout, stdin to /dev/null
+    open(STDIN,  "+>/dev/null");
+    open(STDOUT, "+>&STDIN");
+    open(STDERR, "+>&STDIN");
+}
+
+# little trimming sub
+sub trim {
+    my $res = shift;
+    $res =~ s/^\s+//;
+    $res =~ s/[\r\n\s]+$//;
+    return $res;
+}
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/conf/authorized_keys
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/conf/authorized_keys	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,1 @@
+environment="LJUSERNAME=replicator",environment="LJCOMMAND=replicator" ssh-dss SOMEKEY replicator@comment.field.stuff
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/conf/cmdshell.conf
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/conf/cmdshell.conf	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,5 @@
+# configuration for the command shell. these are commands setup so that if
+# someone comes in over SSH and their key specifies an environment variable
+# of LJCOMMAND, it will be cross referenced against this list
+
+replicator = 1.2.3.4 : 9107
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/conf/sshd_config
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/conf/sshd_config	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,27 @@
+### begin config section #####################################################
+
+# basic setup information.  we run on a different port because this
+# SSH daemon's only job is to let our lj management user through.
+Port 222
+
+# important: only allow us in.  set this to the username that you want to
+# allow into the system.  see the documentation for more setup information.
+AllowUsers ljmgmt
+
+### end config section #######################################################
+
+# only allow sshv2
+Protocol 2
+
+# do not allow root to log in (they shouldn't be able to due to AllowUsers
+# above, but just to be extra paranoid...)
+PermitRootLogin no
+
+# must use SSH keys to login to this daemon
+PasswordAuthentication no
+
+# be very silent (should also touch .hushlogin in the ljmgmt home directory)
+PrintMotd no
+
+# required for our authorized_keys file to work
+PermitUserEnvironment yes
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/conf/stats-local.conf
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/conf/stats-local.conf	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,15 @@
+# configuration for the replicator and internal statistic accumulator.
+ 
+# first setup our main configuration information to point to where the server
+# should be found at.  this server is the local network server at the near
+# endpoint.  this is where data is replicated TO.
+server = 127.0.0.1
+port = 9106
+ 
+# now setup the information about the SSH connection we're going to use to
+# gather data from the remote server.  this is where we will get the data,
+# or, the point we're replicating FROM.
+ssh_key = /home/lj/.ssh/id_dsa
+ssh_host = 1.2.3.4
+ssh_port = 222
+ssh_user = ljmgmt
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/conf/stats.conf
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/conf/stats.conf	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,7 @@
+# global configuration options, this points to our SPUD server
+server = 1.2.3.4
+port = 9107
+
+# automatically add monitoring plugins for all of the LiveJournal services.  the
+# times after the equal sign are intervals between plugin updates in seconds.
+config_generator(mysql = 5, perlbal = 5, memcached = 5, mogstored = 60, mogilefsd = 5)
diff -r 6ad827ca8a90 -r d62f415767a4 src/spud/doc/README.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/spud/doc/README.txt	Sun Mar 01 19:21:55 2009 +0000
@@ -0,0 +1,67 @@
+SPUD -- Statistic Polling and Updating Daemon
+        System Productivity and Utilization Daemon
+        ...or something
+
+-- Introduction --
+SPUD is a set of programs that are designed to collect statistics using
+a variety of methods that are easily extendable to gather information on
+almost anything you could want to get statistics on.  It then stores these
+bits of data in a simple hash server that supports retrieving data enmasse
+as well as subscribing to update events.
+
+
+-- The Components --
+There are several pieces to the system:
+
+ - server
+   A simple server that stores key/value pairs with 100 data points
+   of history.
+
+ - gatherer
+   The program that gathers the actual numbers and puts them in a
+   stats-server.
+
+ - replicator
+   Works in conjunction with a properly configured SSH daemon and the
+   cmdshell utility to get stats from one secure network to another.
+
+ - wrapper
+   A little script that effectively "wraps" another shell command and
+   pipes its STDOUT/STDERR to appropriately named keys on the server.
+   Also keeps track of the start/end times of the last time the command
+   ran.
+
+ - cmdshell
+   A simple utility program that works with stats-replicator.
+
+For more information on each component, please see the various
+configuration files in the conf directory as well as the opening comments
+of each program.  They are fairly well described in there.
+
+
+-- Setup --
+More documentation to come after the bugs are worked out and the system is
+actually setup and in use.  But, in general:
+
+1. Run a stats-server in your internal network somewhere and note the port.
+
+2. Setup the stats.conf for the statistic gatherers.  Then run it once on
+a machine that's got some spare time to do gathering.  Make sure to point
+this configuration's config files at the server from step 1.
+
+3. Setup an SSH daemon and dummy account on a border machine that you can
+get to from the outside world and that can also see the internal network.
+Set the shell of the dummy user to cmdshell and configure cmdshell.  Also
+setup the authorized_keys file appropriately.  You will want a command
+(e.g. replicator) that points at the server setup in step 1.
+
+4. Setup a second stats-server on your local machine/network.  Note down
+the port again.
+
+5. Setup the stats-local.conf on your local end to point to the SSH daemon
+that you setup in step 3.  Now run the replicator and point it at this
+config file and it will start copying data from the first stats server to
+the second.
+
+That's it.  Now get some statistics displayers and all of the data gathered
+and put onto the first server will be replicated over to the second server.
--------------------------------------------------------------------------------