1 package Collectd::Graph::Common;
3 # Copyright (C) 2008-2011 Florian Forster
4 # Copyright (C) 2011 noris network AG
6 # This program is free software; you can redistribute it and/or modify it under
7 # the terms of the GNU General Public License as published by the Free Software
8 # Foundation; only version 2 of the License is applicable.
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
15 # You should have received a copy of the GNU General Public License along with
16 # this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 # Florian "octo" Forster <octo at collectd.org>
25 use vars (qw($ColorCanvas $ColorFullBlue $ColorHalfBlue));
27 use Collectd::Unixsock ();
28 use Carp (qw(confess cluck));
31 use Collectd::Graph::Config (qw(gc_get_scalar));
35 $ColorCanvas = 'FFFFFF';
36 $ColorFullBlue = '0000FF';
37 $ColorHalfBlue = 'B7B7F7';
39 @Collectd::Graph::Common::ISA = ('Exporter');
40 @Collectd::Graph::Common::EXPORT_OK = (qw(
46 sanitize_plugin sanitize_plugin_instance
47 sanitize_type sanitize_type_instance
48 group_files_by_plugin_instance
49 get_files_from_directory
57 get_timespan_selection
62 sort_idents_by_type_instance
68 our $DefaultDataDir = '/var/lib/collectd/rrd';
72 sub _sanitize_generic_allow_minus
79 # remove all dots and dashes at the beginning and at the end.
86 sub _sanitize_generic_no_minus
88 # Do everything the allow-minus variant does..
89 my $str = _sanitize_generic_allow_minus (@_);
91 # .. and remove the dashes, too
95 } # _sanitize_generic_no_minus
99 return (_sanitize_generic_allow_minus (@_));
104 return (_sanitize_generic_no_minus (@_));
107 sub sanitize_plugin_instance
109 return (_sanitize_generic_allow_minus (@_));
114 return (_sanitize_generic_no_minus (@_));
117 sub sanitize_type_instance
119 return (_sanitize_generic_allow_minus (@_));
122 sub group_files_by_plugin_instance
127 for (my $i = 0; $i < @files; $i++)
129 my $file = $files[$i];
130 my $key1 = $file->{'hostname'} || '';
131 my $key2 = $file->{'plugin_instance'} || '';
132 my $key = "$key1-$key2";
134 $data->{$key} ||= [];
135 push (@{$data->{$key}}, $file);
141 sub filename_to_ident
146 if ($file =~ m#([^/]+)/([^/\-]+)(?:-([^/]+))?/([^/\-]+)(?:-([^/]+))?\.rrd$#)
148 $ret = {hostname => $1, plugin => $2, type => $4};
151 $ret->{'plugin_instance'} = $3;
155 $ret->{'type_instance'} = $5;
159 $ret->{'_prefix'} = $`;
168 } # filename_to_ident
170 sub ident_to_filename
173 my $data_dir = gc_get_scalar ('DataDir', $DefaultDataDir);
177 if (defined ($ident->{'_prefix'}))
179 $ret .= $ident->{'_prefix'};
183 $ret .= "$data_dir/";
186 if (!$ident->{'hostname'})
188 cluck ("hostname is undefined")
190 if (!$ident->{'plugin'})
192 cluck ("plugin is undefined")
194 if (!$ident->{'type'})
196 cluck ("type is undefined")
199 $ret .= $ident->{'hostname'} . '/' . $ident->{'plugin'};
200 if (defined ($ident->{'plugin_instance'}))
202 $ret .= '-' . $ident->{'plugin_instance'};
205 $ret .= '/' . $ident->{'type'};
206 if (defined ($ident->{'type_instance'}))
208 $ret .= '-' . $ident->{'type_instance'};
213 } # ident_to_filename
219 if (!defined ($part))
223 if (ref ($part) eq 'ARRAY')
231 return ('(' . join (',', @$part) . ')');
246 $ret .= _part_to_string ($ident->{'hostname'})
247 . '/' . _part_to_string ($ident->{'plugin'});
248 if (defined ($ident->{'plugin_instance'}))
250 $ret .= '-' . _part_to_string ($ident->{'plugin_instance'});
253 $ret .= '/' . _part_to_string ($ident->{'type'});
254 if (defined ($ident->{'type_instance'}))
256 $ret .= '-' . _part_to_string ($ident->{'type_instance'});
262 sub get_files_from_directory
265 my $recursive = @_ ? shift : 0;
267 my @directories = ();
271 opendir ($dh, $dir) or die ("opendir ($dir): $!");
272 while (my $entry = readdir ($dh))
274 next if ($entry =~ m/^\./);
276 $entry = "$dir/$entry";
280 push (@directories, $entry);
284 push (@files, $entry);
289 push (@$ret, map { filename_to_ident ($_) } sort (@files));
295 my $temp = get_files_from_directory ($_, $recursive - 1);
298 push (@$ret, @$temp);
304 } # get_files_from_directory
310 if (defined ($Cache->{'get_all_hosts'}))
312 $ret = $Cache->{'get_all_hosts'};
317 my $data_dir = gc_get_scalar ('DataDir', $DefaultDataDir);
319 opendir ($dh, "$data_dir") or confess ("opendir ($data_dir): $!");
320 while (my $entry = readdir ($dh))
322 next if ($entry =~ m/^\./);
323 next if (!-d "$data_dir/$entry");
324 push (@$ret, sanitize_hostname ($entry));
328 $Cache->{'get_all_hosts'} = $ret;
350 my $data_dir = gc_get_scalar ('DataDir', $DefaultDataDir);
355 $cache_key = join (';', @hosts);
360 @hosts = get_all_hosts ();
363 if (defined ($Cache->{'get_all_plugins'}{$cache_key}))
365 $ret = $Cache->{'get_all_plugins'}{$cache_key};
369 return (sort (keys %$ret));
380 opendir ($dh, "$data_dir/$host") or next;
381 while (my $entry = readdir ($dh))
384 my $plugin_instance = '';
386 next if ($entry =~ m/^\./);
387 next if (!-d "$data_dir/$host/$entry");
389 if ($entry =~ m#^([^-]+)-(.+)$#)
392 $plugin_instance = $2;
394 elsif ($entry =~ m#^([^-]+)$#)
397 $plugin_instance = '';
404 $ret->{$plugin} ||= {};
405 $ret->{$plugin}{$plugin_instance} = 1;
410 $Cache->{'get_all_plugins'}{$cache_key} = $ret;
413 return (sort (keys %$ret));
421 sub get_files_for_host
423 my $host = sanitize_hostname (shift);
424 my $data_dir = gc_get_scalar ('DataDir', $DefaultDataDir);
425 return (get_files_from_directory ("$data_dir/$host", 2));
426 } # get_files_for_host
433 for (qw(hostname plugin plugin_instance type type_instance))
438 if (!defined ($filter->{$part}))
442 if (!defined ($ident->{$part}))
447 if (ref $filter->{$part})
449 if (!grep { $ident->{$part} eq $_ } (@{$filter->{$part}}))
456 if ($ident->{$part} ne $filter->{$part})
470 if (defined ($Cache->{'_get_all_files'}))
472 $ret = $Cache->{'_get_all_files'};
476 my $data_dir = gc_get_scalar ('DataDir', $DefaultDataDir);
478 $ret = get_files_from_directory ($data_dir, 3);
479 $Cache->{'_get_all_files'} = $ret;
485 sub get_files_by_ident
491 my $cache_key = ident_to_string ($ident);
492 if (defined ($Cache->{'get_files_by_ident'}{$cache_key}))
494 my $ret = $Cache->{'get_files_by_ident'}{$cache_key};
499 $all_files = _get_all_files ();
501 @ret = grep { _filter_ident ($ident, $_) == 0 } (@$all_files);
503 $Cache->{'get_files_by_ident'}{$cache_key} = \@ret;
505 } # get_files_by_ident
507 sub get_selected_files
511 for (qw(hostname plugin plugin_instance type type_instance))
514 my @temp = param ($part);
519 elsif (($part eq 'plugin') || ($part eq 'type'))
521 $ident->{$part} = [map { _sanitize_generic_no_minus ($_) } (@temp)];
525 $ident->{$part} = [map { _sanitize_generic_allow_minus ($_) } (@temp)];
529 return (get_files_by_ident ($ident));
530 } # get_selected_files
532 sub get_timespan_selection
535 if (param ('timespan'))
537 my $temp = int (param ('timespan'));
538 if ($temp && ($temp > 0))
545 } # get_timespan_selection
547 sub get_host_selection
551 for (get_all_hosts ())
556 for (param ('hostname'))
558 my $host = _sanitize_generic_allow_minus ($_);
559 if (defined ($ret{$host}))
567 return (grep { $ret{$_} > 0 } (sort (keys %ret)));
573 } # get_host_selection
575 sub get_plugin_selection
578 my @hosts = get_host_selection ();
580 for (get_all_plugins (@hosts))
585 for (param ('plugin'))
587 if (defined ($ret{$_}))
595 return (grep { $ret{$_} > 0 } (sort (keys %ret)));
601 } # get_plugin_selection
606 if ($color =~ m/([0-9A-Fa-f][0-9A-Fa-f])([0-9A-Fa-f][0-9A-Fa-f])([0-9A-Fa-f][0-9A-Fa-f])/)
608 return ([hex ($1) / 255.0, hex ($2) / 255.0, hex ($3) / 255.0]);
615 confess ("Wrong number of arguments") if (@_ != 1);
616 return (sprintf ('%02hx%02hx%02hx', map { int (255.0 * $_) } @{$_[0]}));
621 my ($r, $g, $b) = (rand (), rand ());
627 $min = 1.0 - ($r + $g);
631 $max = 2.0 - ($r + $g);
634 $b = $min + (rand () * ($max - $min));
636 return (_color_to_string ([$r, $g, $b]));
644 my $ret = [undef, undef, undef];
646 $opts{'background'} ||= [1.0, 1.0, 1.0];
647 $opts{'alpha'} ||= 0.25;
651 $fg = _string_to_color ($fg)
652 or confess ("Cannot parse foreground color $fg");
655 if (!ref ($opts{'background'}))
657 $opts{'background'} = _string_to_color ($opts{'background'})
658 or confess ("Cannot parse background color " . $opts{'background'});
660 $bg = $opts{'background'};
662 for (my $i = 0; $i < 3; $i++)
664 $ret->[$i] = ($opts{'alpha'} * $fg->[$i])
665 + ((1.0 - $opts{'alpha'}) * $bg->[$i]);
668 return (_color_to_string ($ret));
671 sub sort_idents_by_type_instance
674 my $array_sort = shift;
676 my %elements = map { $_->{'type_instance'} => $_ } (@$idents);
677 splice (@$idents, 0);
681 next if (!exists ($elements{$_}));
682 push (@$idents, $elements{$_});
683 delete ($elements{$_});
685 push (@$idents, map { $elements{$_} } (sort (keys %elements)));
686 } # sort_idents_by_type_instance
688 sub type_to_module_name
693 $ret = ucfirst (lc ($type));
695 $ret =~ s/[^A-Za-z_]//g;
696 $ret =~ s/_([A-Za-z])/\U$1\E/g;
698 return ("Collectd::Graph::Type::$ret");
699 } # type_to_module_name
703 my @days = (qw(Sun Mon Tue Wed Thu Fri Sat));
704 my @months = (qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec));
706 my $epoch = @_ ? shift : time ();
707 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
708 my $string = sprintf ('%s, %02d %s %4d %02d:%02d:%02d GMT', $days[$wday], $mday,
709 $months[$mon], 1900 + $year, $hour ,$min, $sec);
715 my $all_files = shift;
724 my $files_to_flush = [];
727 if (!defined $opts{'begin'})
729 cluck ("begin is not defined");
732 $begin = $opts{'begin'};
734 if (!defined $opts{'end'})
736 cluck ("end is not defined");
746 $interval = $opts{'interval'} || 10;
748 if (ref ($all_files) eq 'HASH')
750 my @tmp = ($all_files);
755 # Don't flush anything if the timespan is in the future.
756 if (($end > $now) && ($begin > $now))
764 my $file_name = ident_to_filename ($file_orig);
769 @statbuf = stat ($file_name);
774 $mtime = $statbuf[9];
776 # Skip if file is fresh
777 if (($now - $mtime) <= $interval)
781 # or $end is before $mtime
782 elsif (($end != 0) && (($end - $mtime) <= 0))
787 $file_copy->{'host'} = $file_orig->{'hostname'};
788 $file_copy->{'plugin'} = $file_orig->{'plugin'};
789 if (exists $file_orig->{'plugin_instance'})
791 $file_copy->{'plugin_instance'} = $file_orig->{'plugin_instance'}
793 $file_copy->{'type'} = $file_orig->{'type'};
794 if (exists $file_orig->{'type_instance'})
796 $file_copy->{'type_instance'} = $file_orig->{'type_instance'}
799 push (@$files_to_flush, $file_copy);
800 } # for (@$all_files)
802 if (!@$files_to_flush)
807 $sock = Collectd::Unixsock->new ($opts{'addr'});
813 $status = $sock->flush (plugins => ['rrdtool'], identifier => $files_to_flush);
816 cluck ("FLUSH failed: " . $sock->{'error'});
825 # vim: set shiftwidth=2 softtabstop=2 tabstop=8 :