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