#!/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 # 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] # 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 # 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 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 # # 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: