1package POE::Component::RRDTool; 2# $Id: RRDTool.pm,v 1.26 2003/09/08 16:31:42 tcaine Exp $ 3 4use strict; 5 6use vars qw/ $VERSION $RRDTOOL_VERSION /; 7 8$VERSION = '0.18'; 9$RRDTOOL_VERSION = '__RRDTOOL_VERSION__'; 10 11# library includes 12use Carp; 13use POE::Session; 14use POE::Wheel::Run; 15use POE::Driver::SysRW; 16use POE::Filter::Line; 17use POE::Filter::Stream; 18 19use File::Basename qw( dirname ); 20use POSIX qw( :sys_wait_h ); 21 22sub IDLE () { 0 }; 23sub BUSY () { 1 }; 24 25# this is the block size for POE::Driver::SysRW. It needs to be large because the 26# output needs to generate only one event for all different RRD commands. 27#@@@@ Can this be replaced by using a custom filter for each request type? 28my $block_size = 4096; 29 30sub start_rrdtool { 31 my ($kernel, $heap, %args) = @_[KERNEL, HEAP, ARG0 .. $#_]; 32 33 $kernel->alias_set('rrdtool'); 34 $heap->{alias} = $args{alias}; 35 $heap->{error_event} = $args{'error_event'} || 'rrd_error', 36 $heap->{status_event} = $args{'status_event'} || 'rrd_status', 37 $heap->{state} = IDLE; 38 39 my $program = [ $args{rrdtool}, '-' ]; 40 41 $heap->{rrdtool} = POE::Wheel::Run->new( 42 Program => $program, 43 ErrorEvent => 'rrd_error', 44 CloseEvent => 'rrd_close', 45 StdinEvent => 'rrd_stdin', 46 StdoutEvent => 'rrd_stdout', 47 StderrEvent => 'rrd_stderr', 48 StdioDriver => POE::Driver::SysRW->new(BlockSize => $block_size), 49 StdinFilter => POE::Filter::Line->new(), 50 StdoutFilter => POE::Filter::Stream->new(), 51 StderrFilter => POE::Filter::Line->new(), 52 ); 53} 54 55sub stop_rrdtool { 56 my ($kernel, $heap, $state) = @_[KERNEL, HEAP, STATE]; 57 58 if($state eq "stop") { 59 if ($heap->{state} == BUSY) { 60 $kernel->delay('stop', 1); 61 } 62 else { 63 $kernel->alias_remove('rrdtool'); 64 my $alias = delete $heap->{alias}; 65 my $rrdtool = delete $heap->{rrdtool}; 66 $rrdtool->kill(); 67 sleep(1); 68 $rrdtool->kill( -9 ); 69 $kernel->post($alias, 'rrd_stopped'); 70 } 71 } 72} 73 74sub rrd_default_handler { 75 my ($heap, $state, @cmd_args) = @_[HEAP, STATE, ARG0 .. $#_]; 76 my $command = join(' ', $state, @cmd_args); 77 $heap->{rrdtool}->put($command); 78 $heap->{state} = BUSY; 79} 80 81sub rrd_output_handler { 82 my ($heap, $state, @cmd_args) = @_[HEAP, STATE, ARG0 .. $#_]; 83 84 # enqueue the callback 85 push(@{$heap->{callbacks}}, shift @cmd_args); 86 # enqueue the command state info 87 push(@{$heap->{cmd_state}}, $state); 88 89 my $command = join(' ', $state, @cmd_args); 90 $heap->{rrdtool}->put($command); 91 $heap->{state} = BUSY; 92} 93 94sub rrd_error { 95 carp( $_[ARG0] ); 96} 97 98sub rrd_output { 99 my ($kernel, $heap, $output) = @_[KERNEL, HEAP, ARG0]; 100 my $alias = $heap->{alias}; 101 # figure out what RRDtool sent to STDOUT 102 if ($output =~ /Usage:/) { 103 $kernel->post($alias, $heap->{'error_event'}, $output); 104 } 105 elsif ($output =~ /ERROR:\s(.*)/) { 106 $kernel->post($alias, $heap->{'error_event'}, $1); 107 } 108 else { 109 my $output = $output; 110 $output =~ s/OK .*$//ms; 111 if($output) { 112 # parse the data section and post a data structure to represent the output 113 114 # $response contains a reference to the data structure that will be used as an 115 # argument to the callback. Each RRDtool command has a different output so they 116 # need their own representation 117 my $response; 118 119 # each RRD command that returns data will add it's name to the cmd_state queue 120 # so that we can tell which RRD command output that needs to be parsed 121 my $command_output = pop @{$heap->{cmd_state}}; 122 if($command_output eq 'fetch') { 123 my @data = split(/\n/, $output); 124 my $header = shift @data; # the header contains the RRD data source names 125 shift @data; # remove blank line after the header 126 127 my @names = $header =~ m/(\S)+/; 128 129 # get first two timestamps to calculate the time between each data point 130 my ($time1) = $data[0] =~ m/^(\d+):/; 131 my ($time2) = $data[1] =~ m/^(\d+):/; 132 133 my %fetch_results = ( 134 start_time => $time1, 135 step => $time2 - $time1, 136 names => [ @names ], 137 data => [], 138 ); 139 140 foreach (@data) { 141 my ($timestamp, @rawdata) = split(/[:\s]+/); 142 push @{$fetch_results{data}}, [ @rawdata ]; 143 } 144 145 $response = \%fetch_results; 146 } 147 elsif($command_output eq 'graph') { 148 my @GRAPH_output = (); 149 my @output = split /\n/, $output; 150 151 # get rrdtool graph's GRAPH output if any 152 foreach (reverse @output) { 153 if (/^(?:NaN|[\-\+\.\d]+)$/) { 154 push @GRAPH_output, $_; 155 } 156 else { 157 last; 158 } 159 } 160 161 # get the image size 162 my ($x,$y) = $output =~ m/(\d{2,3})x(\d{2,3})/; 163 164 my %graph_results = ( 165 xsize => $x, 166 ysize => $y, 167 output => [ @GRAPH_output ], 168 ); 169 $response = \%graph_results; 170 } 171 elsif($command_output eq 'info') { 172 my %info_results; 173 foreach my $line (split(/\n/, $output)) { 174 my ($attribute, $value) = split(/\s=\s/, $line); 175 $value =~ s/"//g; 176 $info_results{$attribute} = $value; 177 } 178 $response = \%info_results; 179 } 180 elsif($command_output eq 'xport') { 181 $response = \$output; 182 } 183 elsif($command_output eq 'dump') { 184 $response = \$output; 185 } 186 187 my $callback = (scalar @{$heap->{callbacks}}) 188 ? pop(@{$heap->{callbacks}}) 189 : 'rrd_output'; 190 191 $kernel->post($alias, $callback, $response); 192 } 193 } 194 195 # update rrdtool run times 196 if ($output =~ /OK u:(\d+\.\d\d) s:(\d+\.\d\d) r:(\d+\.\d\d)/) { 197 $kernel->post($alias, $heap->{'status_event'}, $1, $2, $3); 198 } 199 200 $heap->{state} = IDLE; 201} 202 203sub new { 204 my $class = shift; 205 my %param = @_; 206 my %args = ( 207 alias => 'rrdtool', 208 rrdtool => '__DEFAULT_RRDTOOL__', 209 ); 210 211 foreach (keys %param) { 212 if (/^-?alias$/i) { $args{alias} = $param{$_} } 213 elsif (/^-?rrdtool$/i) { $args{rrdtool} = $param{$_} } 214 elsif (/^-?errorevent$/i) { $args{error_event} = $param{$_} } 215 elsif (/^-?statusevent$/i){ $args{status_event}= $param{$_} } 216 } 217 218 croak "couldn't find $args{rrdtool}\n" 219 unless -e $args{rrdtool}; 220 221 POE::Session->create 222 ( inline_states => { 223 _start => \&start_rrdtool, 224 stop => \&stop_rrdtool, 225 226 # rrdtool commands 227 create => \&rrd_default_handler, 228 update => \&rrd_default_handler, 229 fetch => \&rrd_output_handler, 230 graph => \&rrd_output_handler, 231 tune => \&rrd_default_handler, 232 dump => \&rrd_default_handler, 233 restore => \&rrd_default_handler, 234 info => \&rrd_output_handler, 235 xport => \&rrd_output_handler, 236 dump => \&rrd_output_handler, 237 238 # rrdtool wheel run events 239 rrd_error => \&rrd_error, 240 rrd_closed => \&rrd_error, 241 rrd_stdout => \&rrd_output, 242 rrd_stderr => \&rrd_error, 243 244 _stop => \&stop_rrdtool, 245 }, 246 args => [ %args ], 247 ); 248} 249 2501; 251__END__ 252 253=head1 NAME 254 255POE::Component::RRDTool - POE interface to Tobias Oetiker's RRDtool 256 257=head1 SYNOPSIS 258 259 use POE qw( Component::RRDTool ); 260 261 my $alias = 'controller'; 262 263 my @create_args = qw( 264 test.rrd 265 --start now 266 --step 30 267 DS:X:GAUGE:60:0:10 268 RRA:MAX:0.5:1:1 269 ); 270 271 # start up the rrdtool component 272 POE::Component::RRDTool->new( 273 Alias => $alias, 274 RRDtool => '/usr/local/bin/rrdtool', 275 ErrorEvent => 'rrd_error', 276 StatusEvent=> 'rrd_status', 277 ); 278 279 POE::Session->create( 280 inline_states => { 281 _start => sub { 282 # set a session alias so that we can receive events from RRDtool 283 $_[KERNEL]->alias_set($_[ARG0]); 284 285 # create a round robin database 286 $_[KERNEL]->post( 'rrdtool', 'create', @create_args ); 287 288 # stop the rrdtool component 289 $_[KERNEL]->post( 'rrdtool', 'stop' ); 290 }, 291 'rrd_error' => sub { 292 print STDERR "ERROR: " . $_[ARG0] . "\n"; 293 }, 294 'rrd_status' => sub { 295 my ($user, $system, $real) = @_[ARG0 .. ARG2]; 296 print "u: $user\ts: $system\tr: $real\n"; 297 }, 298 }, 299 args => [ $alias ], 300 ); 301 302 $poe_kernel->run(); 303 304=head1 DESCRIPTION 305 306RRDtool refers to round robin database tool. Round robin databases have a fixed number of data points in them and contain a pointer to the current element. Since the databases have a fixed number of data points the database size doesn't change after creation. RRDtool allows you to define a set of archives which consolidate the primary data points in higher granularity. RRDtool is specialized for time series data and can be used to create RRD files, update RRDs, retreive data from RRDs, and generate graphs from the databases. This module provides a POE wrapper around the rrdtool command line interface. 307 308=head1 METHODS 309 310=over 4 311 312=item B<new> - creates a POE RRDTool component 313 314new() is the constructor for L<POE::Component::RRDTool>. The constructor is L<POE::Component::RRDTool>'s only public method. It has two optional named parameters B<alias> and B<rrdtool>. 315 316The B<alias> parameter is the alias of the session that the L<POE::Component::RRDTool> instance will send events to as callbacks. It defaults to B<component>. It is important to understand that an RRDTool instance ALWAYS uses the B<rrdtool> alias to reference itself. Events are posted to the rrdtool alias and callbacks are posted to the alias set via the constructor. 317 318The B<rrdtool> parameter is the name of the RRDtool command line utility. It defaults to /usr/local/bin/rrdtool or the location that was found when building and installing on your system. You can use the B<rrdtool> parameber to override this default location. 319 320In the calling convention below the C<[]>s indicate optional parameters. 321 322 POE::Component::RRDTool->new( 323 [-alias => 'controller'], 324 [-rrdtool => '/usr/local/bin/rrdtool'], 325 [-errorevent => 'error_handler'], 326 [-statusevent => 'status_handler'], 327 ); 328 329=back 330 331=head1 EVENTS 332 333L<POE::Component::RRDTool> events take the same parameters as their rrdtool counterpart. Use the RRDtool manual as a reference for rrdtool command parameters. 334 335The following events can be posted to an RRDtool component. 336 337=over 4 338 339=item B<create> - create a round robin database 340 341 my @create_args = qw( 342 test.rrd 343 --start now 344 --step 30 345 DS:X:GAUGE:60:0:10 346 RRA:MAX:0.5:1:1 347 ); 348 349 $_[KERNEL]->post( qw( rrdtool create ), @create_args); 350 351=item B<update> - update a round robin database 352 353 $_[KERNEL]->post( qw( rrdtool update test.rrd N:1 ) ); 354 355=item B<fetch> - fetch data from a RRD 356 357 my $callback = 'rrd_fetch_handler'; 358 359 my @fetch_args = qw( 360 test.rrd 361 MAX 362 --start -1s 363 ); 364 365 $_[KERNEL]->post( qw( rrdtool fetch ), $callback, @fetch_args ); 366 367=item B<graph> - generate a graph image from RRDs 368 369 my $callback = 'rrd_graph_handler'; 370 371 my @graph_args = ( 372 'graph.png', 373 '--start', -86400, 374 '--imgformat', 'PNG', 375 'DEF:x=test.rrd:X:MAX', 376 'CDEF:y=1,x,+', 377 'PRINT:y:MAX:%lf', 378 'AREA:x#00FF00:test_data', 379 ); 380 381 $_[KERNEL]->post( qw( rrdtool udpate ), $callback, @graph_args ); 382 383 sub rrd_graph_handler { 384 my $graph = $_[ARG0]; 385 printf("Image Size: %dx%d\n", $graph->{xsize}, $graph->{ysize}); 386 printf("PRINT output: %s\n", join('\n', @$graph->{output}) if @$graph; 387 print "graph.png was created" if -e "graph.png"; 388 warn "no image was created" unless -e "graph.png"; 389 } 390 391=item B<info> - get information about a RRD 392 393 my $callback = 'rrd_info_handler'; 394 395 $_[KERNEL]->post( qw( rrdtool info ), $callback, 'test.rrd' ); 396 397=item B<xport> - generate xml reports from RRDs 398 399 my $callback = 'rrd_xport_handler'; 400 401 my @xport_args = ( 402 '--start', -300, 403 '--step', 300, 404 'DEF:x=test.rrd:X:MAX', 405 'XPORT:x:foobar', 406 ); 407 408 $_[KERNEL]->post( qw( rrdtool xport ), $callback, @xport_args ); 409 410=item B<dump> - dump a RRD in XML format 411 412 my $callback = 'rrd_dump_handler'; 413 414 $_[KERNEL]->post( qw( rrdtool dump ), $callback, 'test.rrd' ); 415 416=item B<stop> - stop an RRDTool component 417 418 $_[KERNEL]->post( qw( rrdtool stop ) ); 419 420=back 421 422=head1 CALLBACKS 423 424The callbacks listed below are sent by the RRDTool component to the session alias passed to it's constructor. You can provide event handlers for them in the controlling session's constructor. However it is not required to handle any of the callbacks. 425 426=over 4 427 428=item B<rrd_status> - notification of rrdtool runtimes 429 430Returns the user, system, and real time of the rrdtool process in ARG0, ARG1, and ARG2 respectively. This event name can be overriden by using the StatusEvent parameter to POE::Component::RRDTool->new(); 431 432 POE::Session->create( 433 inline_states => { 434 'rrd_status' => sub { 435 my ($user, $system, $real) = @_[ARG0 .. ARG2]; 436 print "u: $user\ts: $system\tr: $real\n"; 437 }, 438 ...., 439 } 440 ); 441 442=item B<rrd_error> - rrdtool error notification 443 444Returns error messages returned from rrdtool in ARG0. 445 446 POE::Session->create( 447 inline_states => { 448 'rrd_error' => sub { 449 my $error = $_[ARG0]; 450 print "Error: $error\n"; 451 }, 452 ...., 453 } 454 ); 455 456=item B<rrd_stopped> - rrdtool process stopped 457 458This callback provides a hook to do something when the rrdtool process is stopped. 459 460 POE::Session->create( 461 inline_states => { 462 'rrd_stopped' => sub { 463 print "rrdtool stopped\n"; 464 }, 465 ...., 466 } 467 ); 468 469=back 470 471=head1 AUTHOR 472 473Todd Caine <todd@pobox.com> 474 475=head1 SEE ALSO 476 477An RRDtool Tutorial 478http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/tutorial/rrdtutorial.html 479 480The Main RRDtool Website 481http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/index.html 482 483The RRDtool Manual 484http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/manual/index.html 485 486=head1 TROUBLESHOOTING 487 488The rrdtool command line utility does not support the xport subcommand until version 1.0.38. If you try to use the xport event using an older version of rrdtool you will receive an rrdtool usage message as an rrd_error callback. 489 490=head1 BUGS 491 492The rrdtool command line utility is being controlled by POE::Wheel::Run. I'm increasing the block size on the POE::Driver::SysRW instance used for the rrdtool output so that each command generates only one event. This should probably be fixed by using the default block size and a custom filter instead. 493 494If you notice that more than one event is being generated from a single rrdtool command you may need to increase the blocksize used. 495 496=head1 COPYRIGHT AND LICENSE 497 498Copyright (c) 2003 Todd Caine. All rights reserved. This program is free 499software; you can redistribute it and/or modify it under the same terms 500as Perl itself. 501 502=cut 503