contrib/collection3: Use “{plugin_inst}/{type_inst}” for “{instance}”
[collectd.git] / contrib / collection3 / lib / Collectd / Graph / Type.pm
1 package Collectd::Graph::Type;
2
3 =head1 NAME
4
5 Collectd::Graph::Type - Base class for the collectd graphing infrastructure
6
7 =cut
8
9 # Copyright (C) 2008  Florian octo Forster <octo at verplant.org>
10 #
11 # This program is free software; you can redistribute it and/or modify it under
12 # the terms of the GNU General Public License as published by the Free Software
13 # Foundation; only version 2 of the License is applicable.
14 #
15 # This program is distributed in the hope that it will be useful, but WITHOUT
16 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
18 # details.
19 #
20 # You should have received a copy of the GNU General Public License along with
21 # this program; if not, write to the Free Software Foundation, Inc.,
22 # 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
23
24 use strict;
25 use warnings;
26
27 use Carp (qw(confess cluck));
28 use RRDs ();
29 use URI::Escape (qw(uri_escape));
30
31 use Collectd::Graph::Common (qw($ColorCanvas $ColorFullBlue $ColorHalfBlue
32   ident_to_filename
33   ident_to_string
34   get_faded_color));
35
36 return (1);
37
38 =head1 DESCRIPTION
39
40 This module serves as base class for more specialized classes realizing
41 specific "types".
42
43 =head1 MEMBER VARIABLES
44
45 As typical in Perl, a Collectd::Graph::Type object is a blessed hash reference.
46 Member variables are entries in that hash. Inheriting classes are free to add
47 additional entries. To set the member variable B<foo> to B<42>, do:
48
49  $obj->{'foo'} = 42;
50
51 The following members control the behavior of Collectd::Graph::Type.
52
53 =over 4
54
55 =item B<files> (array reference)
56
57 List of RRD files. Each file is passed as "ident", i.E<nbsp>e. broken up into
58 "hostname", "plugin", "type" and optionally "plugin_instance" and
59 "type_instance". Use the B<addFiles> method rather than setting this directly.
60
61 =item B<data_sources> (array reference)
62
63 List of data sources in the RRD files. If this is not given, the default
64 implementation of B<getDataSources> will use B<RRDs::info> to find out which
65 data sources are contained in the files.
66
67 =item B<ds_names> (array reference)
68
69 Names of the data sources as printed in the graph. Should be in the same order
70 as the data sources are returned by B<getDataSources>.
71
72 =item B<rrd_title> (string)
73
74 Title of the RRD graph. The title can contain "{hostname}", "{plugin}" and so
75 on which are replaced with their actual value. See the B<getTitle> method
76 below.
77
78 =item B<rrd_opts> (array reference)
79
80 List of options directly passed to B<RRDs::graph>.
81
82 =item B<rrd_format> (string)
83
84 Format to use with B<GPRINT>. Defaults to C<%5.1lf>.
85
86 =item B<rrd_colors> (hash reference)
87
88 Mapping of data source names to colors, used when graphing the different data
89 sources.  Colors are given in the typical hexadecimal RGB form, but without
90 leading "#", e.E<nbsp>g.:
91
92  $obj->{'rrd_colors'} = {foo => 'ff0000', bar => '00ff00'};
93
94 =back
95
96 =head1 METHODS
97
98 The following methods are used by the graphing front end and may be overwritten
99 to customize their behavior.
100
101 =over 4
102
103 =cut
104
105 sub _get_ds_from_file
106 {
107   my $file = shift;
108   my $info = RRDs::info ($file);
109   my %ds = ();
110   my @ds = ();
111
112   if (!$info || (ref ($info) ne 'HASH'))
113   {
114     return;
115   }
116
117   for (keys %$info)
118   {
119     if (m/^ds\[([^\]]+)\]/)
120     {
121       $ds{$1} = 1;
122     }
123   }
124
125   @ds = (keys %ds);
126   if (wantarray ())
127   {
128     return (@ds);
129   }
130   elsif (@ds)
131   {
132     return (\@ds);
133   }
134   else
135   {
136     return;
137   }
138 } # _get_ds_from_file
139
140 sub new
141 {
142   my $pkg = shift;
143   my $obj = bless ({files => []}, $pkg);
144
145   if (@_)
146   {
147     $obj->addFiles (@_);
148   }
149
150   return ($obj);
151 }
152
153 =item B<addFiles> ({ I<ident> }, [...])
154
155 Adds the given idents (which are hash references) to the B<files> member
156 variable, see above.
157
158 =cut
159
160 sub addFiles
161 {
162   my $obj = shift;
163   push (@{$obj->{'files'}}, @_);
164 }
165
166 =item B<getGraphsNum> ()
167
168 Returns the number of graphs that can be generated from the added files. By
169 default this number equals the number of files.
170
171 =cut
172
173 sub getGraphsNum
174 {
175   my $obj = shift;
176   return (scalar @{$obj->{'files'}});
177 }
178
179 =item B<getDataSources> ()
180
181 Returns the names of the data sources. If the B<data_sources> member variable
182 is unset B<RRDs::info> is used to read that information from the first file.
183 Set the B<data_sources> member variable instead of overloading this method!
184
185 =cut
186
187 sub getDataSources
188 {
189   my $obj = shift;
190
191   if (!defined $obj->{'data_sources'})
192   {
193     my $ident;
194     my $filename;
195
196     if (!@{$obj->{'files'}})
197     {
198       return;
199     }
200
201     $ident = $obj->{'files'}[0];
202     $filename = ident_to_filename ($ident);
203
204     $obj->{'data_sources'} = _get_ds_from_file ($filename);
205     if (!$obj->{'data_sources'})
206     {
207       cluck ("_get_ds_from_file ($filename) failed.");
208     }
209   }
210
211   if (!defined $obj->{'data_sources'})
212   {
213     return;
214   }
215   elsif (wantarray ())
216   {
217     return (@{$obj->{'data_sources'}})
218   }
219   else
220   {
221     $obj->{'data_sources'};
222   }
223 } # getDataSources
224
225
226 =item B<getTitle> (I<$index>)
227
228 Returns the title of the I<$index>th B<graph> (not necessarily file!). If the
229 B<rrd_title> member variable is unset, a generic title is generated from the
230 ident. Otherwise the substrings "{hostname}", "{plugin}", "{plugin_instance}",
231 "{type}", and "{type_instance}" are replaced by their respective values.
232
233 =cut
234
235 sub getTitle
236 {
237   my $obj = shift;
238   my $ident = shift;
239   my $title = $obj->{'rrd_title'};
240
241   if (!$title)
242   {
243     return (ident_to_string ($ident));
244   }
245
246   my $hostname = $ident->{'hostname'};
247   my $plugin = $ident->{'plugin'};
248   my $plugin_instance = $ident->{'plugin_instance'};
249   my $type = $ident->{'type'};
250   my $type_instance = $ident->{'type_instance'};
251   my $instance;
252
253   if ((defined $type_instance) && (defined $plugin_instance))
254   {
255     $instance = "$plugin_instance/$type_instance";
256   }
257   elsif (defined $type_instance)
258   {
259     $instance = $type_instance;
260   }
261   elsif (defined $plugin_instance)
262   {
263     $instance = $plugin_instance;
264   }
265   else
266   {
267     $instance = 'no instance';
268   }
269
270   if (!defined $plugin_instance)
271   {
272     $plugin_instance = 'no instance';
273   }
274
275   if (!defined $type_instance)
276   {
277     $type_instance = 'no instance';
278   }
279
280   $title =~ s#{hostname}#$hostname#g;
281   $title =~ s#{plugin}#$plugin#g;
282   $title =~ s#{plugin_instance}#$plugin_instance#g;
283   $title =~ s#{type}#$type#g;
284   $title =~ s#{type_instance}#$type_instance#g;
285   $title =~ s#{instance}#$instance#g;
286
287   return ($title);
288 }
289
290 =item B<getRRDArgs> (I<$index>)
291
292 Return the arguments needed to generate the graph from the RRD file(s). If the
293 file has only one data source, this default implementation will generate that
294 typical min, average, max graph you probably know from temperatures and such.
295 If the RRD files have multiple data sources, the average of each data source is
296 printes as simple line.
297
298 =cut
299
300 sub getRRDArgs
301 {
302   my $obj = shift;
303   my $index = shift;
304
305   my $ident = $obj->{'files'}[$index];
306   if (!$ident)
307   {
308     cluck ("Invalid index: $index");
309     return;
310   }
311   my $filename = ident_to_filename ($ident);
312
313   my $rrd_opts = $obj->{'rrd_opts'} || [];
314   my $rrd_title = $obj->getTitle ($ident);
315   my $format = $obj->{'rrd_format'} || '%5.1lf';
316
317   my $rrd_colors = $obj->{'rrd_colors'};
318   my @ret = ('-t', $rrd_title, @$rrd_opts);
319
320   if (defined $obj->{'rrd_vertical'})
321   {
322     push (@ret, '-v', $obj->{'rrd_vertical'});
323   }
324
325   my $ds_names = $obj->{'ds_names'};
326   if (!$ds_names)
327   {
328     $ds_names = {};
329   }
330
331   my $ds = $obj->getDataSources ();
332   if (!$ds)
333   {
334     confess ("obj->getDataSources failed.");
335   }
336
337   if (!$rrd_colors)
338   {
339     my @tmp = ('0000ff', 'ff0000', '00ff00', 'ff00ff', '00ffff', 'ffff00');
340
341     for (my $i = 0; $i < @$ds; $i++)
342     {
343       $rrd_colors->{$ds->[$i]} = $tmp[$i % @tmp];
344     }
345   }
346
347   for (my $i = 0; $i < @$ds; $i++)
348   {
349     my $f = $filename;
350     my $ds_name = $ds->[$i];
351
352     # We need to escape colons for RRDTool..
353     $f =~ s#:#\\:#g;
354     $ds_name =~ s#:#\\:#g;
355
356     if (exists ($obj->{'scale'}))
357     {
358       my $scale = 0.0 + $obj->{'scale'};
359       push (@ret,
360         "DEF:min${i}_raw=${f}:${ds_name}:MIN",
361         "DEF:avg${i}_raw=${f}:${ds_name}:AVERAGE",
362         "DEF:max${i}_raw=${f}:${ds_name}:MAX",
363         "CDEF:max${i}=max${i}_raw,$scale,*",
364         "CDEF:avg${i}=avg${i}_raw,$scale,*",
365         "CDEF:min${i}=min${i}_raw,$scale,*");
366     }
367     else
368     {
369       push (@ret,
370         "DEF:min${i}=${f}:${ds_name}:MIN",
371         "DEF:avg${i}=${f}:${ds_name}:AVERAGE",
372         "DEF:max${i}=${f}:${ds_name}:MAX");
373     }
374   }
375
376   if (@$ds == 1)
377   {
378     my $ds_name = $ds->[0];
379     my $color_fg = $rrd_colors->{$ds_name} || '000000';
380     my $color_bg = get_faded_color ($color_fg);
381
382     if ($ds_names->{$ds_name})
383     {
384       $ds_name = $ds_names->{$ds_name};
385     }
386     $ds_name =~ s#:#\\:#g;
387
388     push (@ret, 
389       "AREA:max0#${color_bg}",
390       "AREA:min0#${ColorCanvas}",
391       "LINE1:avg0#${color_fg}:${ds_name}",
392       "GPRINT:min0:MIN:${format} Min,",
393       "GPRINT:avg0:AVERAGE:${format} Avg,",
394       "GPRINT:max0:MAX:${format} Max,",
395       "GPRINT:avg0:LAST:${format} Last\\l");
396   }
397   else
398   {
399     for (my $i = 0; $i < @$ds; $i++)
400     {
401       my $ds_name = $ds->[$i];
402       my $color = $rrd_colors->{$ds_name} || '000000';
403
404       if ($ds_names->{$ds_name})
405       {
406         $ds_name = $ds_names->{$ds_name};
407       }
408
409       push (@ret, 
410         "LINE1:avg${i}#${color}:${ds_name}",
411         "GPRINT:min${i}:MIN:${format} Min,",
412         "GPRINT:avg${i}:AVERAGE:${format} Avg,",
413         "GPRINT:max${i}:MAX:${format} Max,",
414         "GPRINT:avg${i}:LAST:${format} Last\\l");
415     }
416   }
417
418   return (\@ret);
419 } # getRRDArgs
420
421 =item B<getGraphArgs> (I<$index>)
422
423 Returns the parameters that should be passed to the CGI script to generate the
424 I<$index>th graph. The returned string is already URI-encoded and will possibly
425 set the "hostname", "plugin", "plugin_instance", "type", and "type_instance"
426 parameters.
427
428 The default implementation simply uses the ident of the I<$index>th file to
429 fill this.
430
431 =cut
432
433 sub getGraphArgs
434 {
435   my $obj = shift;
436   my $index = shift;
437   my $ident = $obj->{'files'}[$index];
438
439   my @args = ();
440   for (qw(hostname plugin plugin_instance type type_instance))
441   {
442     if (defined ($ident->{$_}))
443     {
444       push (@args, uri_escape ($_) . '=' . uri_escape ($ident->{$_}));
445     }
446   }
447
448   return (join (';', @args));
449 }
450
451 =item B<getLastModified> ([I<$index>])
452
453 If I<$index> is not given, the modification time of all files is scanned and the most recent modification is returned. If I<$index> is given, only the files belonging to the I<$index>th graph will be considered.
454
455 =cut
456
457 sub getLastModified
458 {
459   my $obj = shift;
460   my $index = @_ ? shift : -1;
461
462   my $mtime = 0;
463
464   if ($index == -1)
465   {
466     for (@{$obj->{'files'}})
467     {
468       my $ident = $_;
469       my $filename = ident_to_filename ($ident);
470       my @statbuf = stat ($filename);
471
472       if (!@statbuf)
473       {
474         next;
475       }
476
477       if ($mtime < $statbuf[9])
478       {
479         $mtime = $statbuf[9];
480       }
481     }
482   }
483   else
484   {
485     my $ident = $obj->{'files'}[$index];
486     my $filename = ident_to_filename ($ident);
487     my @statbuf = stat ($filename);
488
489     $mtime = $statbuf[9];
490   }
491
492   if (!$mtime)
493   {
494     return;
495   }
496   return ($mtime);
497 } # getLastModified
498
499 =back
500
501 =head1 SEE ALSO
502
503 L<Collectd::Graph::Type::GenericStacked>
504
505 =head1 AUTHOR AND LICENSE
506
507 Copyright (c) 2008 by Florian Forster
508 E<lt>octoE<nbsp>atE<nbsp>verplant.orgE<gt>. Licensed under the terms of the GNU
509 General Public License, VersionE<nbsp>2 (GPLv2).
510
511 =cut
512
513 # vim: set shiftwidth=2 softtabstop=2 tabstop=8 :