From: Florian Forster Date: Tue, 8 Jul 2008 11:08:13 +0000 (+0200) Subject: contrib/collection3: Add an basic, extensible, modular graphing front-end. X-Git-Tag: collectd-4.4.2~5 X-Git-Url: https://git.octo.it/?p=collectd.git;a=commitdiff_plain;h=41f141639046a261063cf346fe7f89cd4cb9cc59 contrib/collection3: Add an basic, extensible, modular graphing front-end. --- diff --git a/contrib/collection3/README b/contrib/collection3/README new file mode 100644 index 00000000..01d01bb7 --- /dev/null +++ b/contrib/collection3/README @@ -0,0 +1,42 @@ + collection3 - Web frontend for collectd +========================================= +http://collectd.org/ + +About +----- + + collection3 is a graphing front-end for the RRD files created by and filled + with collectd. It is written in Perl and should be run as an CGI-script. + Graphs are generated on-the-fly, so no cron job or similar is necessary. + +Layout +------ + + The files used by collection3 are organized in a typical UNIX fashion: The + configuration resides in etc/, executable scripts are in bin/, supplementary + Perl modules are in lib/ and static data for displaying the web page are in + share/. + + All files in all subdirectories except bin/ should NOT be executable. + Ideally, the webserver should not serve them either. Consider using + `.htaccess' files or other means to configure the web server to deny access + to these directories. + +Dependencies +------------ + + collection3 depends on the following Perl modules not included in the Perl + distribution itself: + + * Config::General + * HTML::Entities + * RRDs + +Copyright and License +--------------------- + + Copyright (C) 2008 Florian octo Forster + + collection3 is provided under the terms of the GNU General Public License, + version 2 (GPLv2). + diff --git a/contrib/collection3/bin/graph.cgi b/contrib/collection3/bin/graph.cgi new file mode 100755 index 00000000..c84199df --- /dev/null +++ b/contrib/collection3/bin/graph.cgi @@ -0,0 +1,148 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use lib ('../lib'); + +use FindBin ('$RealBin'); +use Carp (qw(confess cluck)); +use CGI (':cgi'); +use RRDs (); + +use Collectd::Graph::TypeLoader (qw(tl_read_config tl_load_type)); + +use Collectd::Graph::Common (qw(sanitize_type get_selected_files + epoch_to_rfc1123)); +use Collectd::Graph::Type (); + +our $Debug = param ('debug'); +our $Begin = param ('begin'); +our $End = param ('end'); + +if ($Debug) +{ + print < 0) && ($Begin > $End)) + { + my $temp = $End; + $End = $Begin; + $Begin = $temp; + } +} + +my $type = param ('type') or die; +my $obj; + +$obj = tl_load_type ($type); +if (!$obj) +{ + confess ("tl_load_type ($type) failed"); +} + +$type = ucfirst (lc ($type)); +$type =~ s/_([A-Za-z])/\U$1\E/g; +$type = sanitize_type ($type); + +my $files = get_selected_files (); +if ($Debug) +{ + require Data::Dumper; + print STDOUT Data::Dumper->Dump ([$files], ['files']); +} +for (@$files) +{ + $obj->addFiles ($_); +} + +my $expires = time (); +if (($End == 0) || (($Begin <= $expires) && ($End >= $expires))) +{ + # 400 == width in pixels + my $timespan = $expires - $Begin; + $expires += int ($timespan / 400); +} +elsif (($End > 0) && ($End < $expires)) +{ + $expires += 366 * 86400; +} +elsif ($Begin > $expires) +{ + $expires = $Begin; +} + +print STDOUT header (-Content_type => 'image/png', + -Last_Modified => epoch_to_rfc1123 ($obj->getLastModified ()), + -Expires => epoch_to_rfc1123 ($expires)); + +my $args = $obj->getRRDArgs (0); + +if ($Debug) +{ + require Data::Dumper; + print STDOUT Data::Dumper->Dump ([$obj], ['obj']); + print STDOUT join (",\n", @$args) . "\n"; + print STDOUT "Last-Modified: " . epoch_to_rfc1123 ($obj->getLastModified ()) . "\n"; +} +else +{ + my @timesel = (); + + if ($End) # $Begin is always true + { + @timesel = ('-s', $Begin, '-e', $End); + } + else + { + @timesel = ('-s', $Begin); # End is implicitely `now'. + } + + $| = 1; + RRDs::graph ('-', '-a', 'PNG', @timesel, @$args); + if (my $err = RRDs::error ()) + { + print STDERR "RRDs::graph failed: $err\n"; + exit (1); + } +} + +exit (0); + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 : diff --git a/contrib/collection3/bin/index.cgi b/contrib/collection3/bin/index.cgi new file mode 100755 index 00000000..b0001d26 --- /dev/null +++ b/contrib/collection3/bin/index.cgi @@ -0,0 +1,335 @@ +#!/usr/bin/perl + +# Copyright (C) 2008 Florian octo Forster +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; only version 2 of the License is applicable. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; +use warnings; +use lib ('../lib'); +use utf8; + +use FindBin ('$RealBin'); +use CGI (':cgi'); +use CGI::Carp ('fatalsToBrowser'); +use HTML::Entities ('encode_entities'); + +use Data::Dumper; + +use Collectd::Graph::TypeLoader (qw(tl_read_config tl_load_type)); +use Collectd::Graph::Common (qw(get_files_from_directory get_all_hosts + get_timespan_selection get_selected_files get_host_selection + get_plugin_selection)); +use Collectd::Graph::Type (); + +our $Debug = param ('debug') ? 1 : 0; + +our $TimeSpans = +{ + Hour => 3600, + Day => 86400, + Week => 7 * 86400, + Month => 31 * 86400, + Year => 366 * 86400 +}; + +my $action = param ('action') || 'list_hosts'; +our %Actions = +( + list_hosts => \&action_list_hosts, + show_selection => \&action_show_selection +); + +if (!exists ($Actions{$action})) +{ + print STDERR "No such action: $action\n"; + exit 1; +} + +tl_read_config ("$RealBin/../etc/collection3.conf"); + +$Actions{$action}->(); +exit (0); + +sub can_handle_xhtml +{ + my %types = (); + + if (!defined $ENV{'HTTP_ACCEPT'}) + { + return; + } + + for (split (',', $ENV{'HTTP_ACCEPT'})) + { + my $type = lc ($_); + my $q = 1.0; + + if ($type =~ m#^([^;]+);q=([0-9\.]+)$#) + { + $type = $1; + $q = 0.0 + $2; + } + $types{$type} = $q; + } + + if (!defined ($types{'application/xhtml+xml'})) + { + return; + } + elsif (!defined ($types{'text/html'})) + { + return (1); + } + elsif ($types{'application/xhtml+xml'} < $types{'text/html'}) + { + return; + } + else + { + return (1); + } +} # can_handle_xhtml + +{my $html_started; +sub start_html +{ + return if ($html_started); + + if (can_handle_xhtml ()) + { + print < + + +HTML + } + else + { + print < + +HTML + } + print < + collection.cgi, Version 3 + + + + +HTML + $html_started = 1; +}} + +sub end_html +{ + print < + +HTML +} + +sub show_selector +{ + my $timespan_selection = get_timespan_selection (); + my $host_selection = get_host_selection (); + my $plugin_selection = get_plugin_selection (); + + print < +
+ Data selection + +HTML + for (sort (keys %$plugin_selection)) + { + my $plugin = encode_entities ($_); + my $selected = $plugin_selection->{$_} + ? ' selected="selected"' + : ''; + print qq# \n#; + } + print < + + +
+ +HTML +} # show_selector + +sub action_list_hosts +{ + start_html (); + show_selector (); + + my @hosts = get_all_hosts (); + print "
    \n"; + for (sort @hosts) + { + my $url = encode_entities (script_name () . "?action=show_selection;hostname=$_"); + my $name = encode_entities ($_); + print qq#
  • $name
  • \n#; + } + print "
\n"; + + end_html (); +} # action_list_hosts + +=head1 MODULE LOADING + +This script makes use of the various B modules. If a +file like C is encountered it tries to load the +B module and, if that fails, falls back to the +B base class. + +If you want to create a specialized graph for a certain type, you have to +create a new module which inherits from the B base +class. A description of provided (and used) methods can be found in the inline +documentation of the B module. + +There are other, more specialized, "abstract" classes that possibly better fit +your need. Unfortunately they are not yet documented. + +=over 4 + +=item B + +Specialized class that groups files by their plugin instance and stacks them on +top of each other. Example types that inherit from this class are +B and B. + +=item B + +Specialized class for input/output graphs. This class can only handle files +with exactly two data sources, input and output. Example types that inherit +from this class are B and +B. + +=back + +=cut + +sub action_show_selection +{ + start_html (); + show_selector (); + + my $ident = {}; + + my $all_files; + my $types = {}; + + $all_files = get_selected_files (); + + if ($Debug) + { + print "
", Data::Dumper->Dump ([$all_files], ['all_files']), "
\n"; + } + + for (@$all_files) + { + my $file = $_; + my $type = ucfirst (lc ($file->{'type'})); + + $type =~ s/[^A-Za-z_]//g; + $type =~ s/_([A-Za-z])/\U$1\E/g; + + if (!defined ($types->{$type})) + { + $types->{$type} = tl_load_type ($file->{'type'}); + if (!$types->{$type}) + { + cluck ("tl_load_type (" . $file->{'type'} . ") failed"); + next; + } + } + + $types->{$type}->addFiles ($file); + } +#print STDOUT Data::Dumper->Dump ([$types], ['types']); + + print qq# \n#; + for (sort (keys %$types)) + { + my $type = $_; + my $graphs_num = $types->{$type}->getGraphsNum (); + + my $timespan = get_timespan_selection (); + + for (my $i = 0; $i < $graphs_num; $i++) + { + my $args = $types->{$type}->getGraphArgs ($i); + my $url = encode_entities ("graph.cgi?$args;begin=-$timespan"); + + print " \n"; + print " \n" if ($i == 0); + + print qq# \n#; + print " \n"; + } + } + + print "
$type
\n"; + end_html (); +} + +=head1 SEE ALSO + +L + +=head1 AUTHOR AND LICENSE + +Copyright (c) 2008 by Florian Forster +EoctoEatEverplant.orgE. Licensed under the terms of the GNU +General Public License, VersionE2 (GPLv2). + +=cut + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 : diff --git a/contrib/collection3/bin/json.cgi b/contrib/collection3/bin/json.cgi new file mode 100755 index 00000000..0dceb628 --- /dev/null +++ b/contrib/collection3/bin/json.cgi @@ -0,0 +1,120 @@ +#!/usr/bin/perl + +# Copyright (C) 2008 Florian octo Forster +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; only version 2 of the License is applicable. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; +use warnings; +use lib ('../lib'); +use utf8; + +use FindBin ('$RealBin'); +use CGI (':cgi'); +use CGI::Carp ('fatalsToBrowser'); +use URI::Escape ('uri_escape'); + +use Data::Dumper; + +use Collectd::Graph::TypeLoader (qw(tl_read_config tl_load_type)); +use Collectd::Graph::Common (qw(get_all_hosts get_files_for_host type_to_module_name)); +use Collectd::Graph::Type (); + +our $Debug = param ('debug') ? 1 : 0; + +tl_read_config ("$RealBin/../etc/collection3.conf"); + +if ($Debug) +{ + print "Content-Type: text/plain; charset=utf-8\n\n"; +} +else +{ + print "Content-Type: application/json; charset=utf-8\n\n"; +} + +print "{\n"; + +my @hosts = get_all_hosts (); +for (my $i = 0; $i < @hosts; $i++) +{ + my $host = $hosts[$i]; + my $files = get_files_for_host ($host); + my %graphs = (); + my @graphs = (); + + # Group files by graphs + for (@$files) + { + my $file = $_; + my $type = $file->{'type'}; + + # Create a new graph object if this is the first of this type. + if (!defined ($graphs{$type})) + { + $graphs{$type} = tl_load_type ($file->{'type'}); + if (!$graphs{$type}) + { + cluck ("tl_load_type (" . $file->{'type'} . ") failed"); + next; + } + } + + $graphs{$type}->addFiles ($file); + } # for (@$files) + + print qq( "$host":\n {\n); + + @graphs = keys %graphs; + for (my $j = 0; $j < @graphs; $j++) + { + my $type = $graphs[$j]; + my $graphs_num = $graphs{$type}->getGraphsNum (); + my @args = (); + + for (my $k = 0; $k < $graphs_num; $k++) + { + my $args = $graphs{$type}->getGraphArgs ($k); + my $url = 'http://' . $ENV{'SERVER_NAME'} . "/cgi-bin/graph.cgi?" . $args; + push (@args, $url); + } + + print qq( "$type": [ ) + . join (', ', map { qq("$_") } (@args)); + + if ($j == (@graphs - 1)) + { + print qq( ]\n); + } + else + { + print qq( ],\n); + } + } # for (keys %graphs) + + if ($i == (@hosts - 1)) + { + print qq( }\n); + } + else + { + print qq( },\n\n); + } +} # for (my $i = 0; $i < @hosts; $i++) + +print "}\n"; + +exit (0); + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 : diff --git a/contrib/collection3/etc/.htaccess b/contrib/collection3/etc/.htaccess new file mode 100644 index 00000000..3a428827 --- /dev/null +++ b/contrib/collection3/etc/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/contrib/collection3/etc/collection3.conf b/contrib/collection3/etc/collection3.conf new file mode 100644 index 00000000..583eeace --- /dev/null +++ b/contrib/collection3/etc/collection3.conf @@ -0,0 +1,255 @@ + + Module GenericStacked + DataSources value + RRDTitle "CPU {plugin_instance} usage" + RRDVerticalLabel "Jiffies" + RRDFormat "%5.2lf" + DSName idle Idle + DSName nice Nice + DSName user User + DSName wait Wait-IO + DSName system System + DSName softirq SoftIRQ + DSName interrupt IRQ + DSName steal Steal + Order idle nice user wait system softirq interrupt steal + Color idle e8e8e8 + Color nice 00e000 + Color user 0000ff + Color wait ffb000 + Color system ff0000 + Color softirq ff00ff + Color interrupt a000a0 + Color steal 000000 + + + Module Df + DataSources free used + + + Module GenericIO + DataSources read write + DSName "read Read " + DSName write Written + RRDTitle "Disk Traffic ({plugin_instance})" + RRDVerticalLabel "Bytes per second" +# RRDOptions ... + RRDFormat "%5.1lf%s" + + + Module GenericIO + DataSources read write + DSName "read Read " + DSName write Written + RRDTitle "Disk Operations ({plugin_instance})" + RRDVerticalLabel "Operations per second" +# RRDOptions ... + RRDFormat "%5.1lf" + + + Module GenericIO + DataSources read write + DSName "read Read " + DSName write Written + RRDTitle "Disk Merged Operations ({plugin_instance})" + RRDVerticalLabel "Merged operations/s" +# RRDOptions ... + RRDFormat "%5.1lf" + + + Module GenericIO + DataSources read write + DSName "read Read " + DSName write Written + RRDTitle "Disk time per operation ({plugin_instance})" + RRDVerticalLabel "Avg. Time/Op" +# RRDOptions ... + RRDFormat "%5.1lf%ss" + Scale 0.001 + + + DataSources entropy + DSName entropy Entropy bits + RRDTitle "Available entropy on {hostname}" + RRDVerticalLabel "Bits" + RRDFormat "%4.0lf" + + + DataSources value + DSName value RPM + RRDTitle "Fanspeed ({type_instance})" + RRDVerticalLabel "RPM" + RRDFormat "%6.1lf" + Color value 00b000 + + + Module GenericIO + DataSources rx tx + DSName rx RX + DSName tx TX + RRDTitle "Interface Errors ({type_instance})" + RRDVerticalLabel "Errors per second" +# RRDOptions ... + RRDFormat "%.3lf" + + + Module GenericStacked + DataSources value + RRDTitle "Interface receive errors ({plugin_instance})" + RRDVerticalLabel "Erorrs/s" + RRDFormat "%.1lf" + Color length f00000 + Color over 00e0ff + Color crc 00e000 + Color frame ffb000 + Color fifo f000c0 + Color missed 0000f0 + + + Module GenericStacked + DataSources value + RRDTitle "Interface transmit errors ({plugin_instance})" + RRDVerticalLabel "Erorrs/s" + RRDFormat "%.1lf" + Color aborted f00000 + Color carrier 00e0ff + Color fifo 00e000 + Color heartbeat ffb000 + Color window f000c0 + + + Module GenericIO + DataSources rx tx + DSName rx RX + DSName tx TX + RRDTitle "Interface Traffic ({type_instance})" + RRDVerticalLabel "Bits per second" +# RRDOptions ... + RRDFormat "%5.1lf%s" + Scale 8 + + + Module GenericIO + DataSources rx tx + DSName rx RX + DSName tx TX + RRDTitle "Interface Packets ({type_instance})" + RRDVerticalLabel "Packets per second" +# RRDOptions ... + RRDFormat "%5.1lf%s" + + + DataSources value + DSName value Bytes/s + RRDTitle "Traffic ({type_instance})" + RRDVerticalLabel "Bytes per second" +# RRDOptions ... + RRDFormat "%5.1lf%s" + + + DataSources value + DSName value Packets/s + RRDTitle "Packets ({type_instance})" + RRDVerticalLabel "Packets per second" +# RRDOptions ... + RRDFormat "%5.1lf" + + + Module GenericStacked + DataSources value + RRDTitle "Interrupts on {hostname}" + RRDVerticalLabel "IRQs/s" + RRDFormat "%5.1lf" + + + Module Load + + + Module GenericStacked + DataSources value + RRDTitle "Physical memory utilization on {hostname}" + RRDVerticalLabel "Bytes" + RRDFormat "%5.1lf%s" + RRDOptions -b 1024 -l 0 + DSName "free Free " + DSName "cached Cached " + DSName "buffered Buffered" + DSName "used Used " + #Order used buffered cached free + Order free cached buffered used + Color free 00e000 + Color cached 0000ff + Color buffered ffb000 + Color used ff0000 + + + DataSources ping + DSName "ping Latency" + RRDTitle "Network latency ({type_instance})" + RRDVerticalLabel "Milliseconds" + RRDFormat "%5.2lfms" + + + Module GenericStacked + DataSources value + RRDTitle "Processes on {hostname}" + RRDVerticalLabel "Processes" + RRDFormat "%5.1lf%s" + DSName running Running + DSName sleeping Sleeping + DSName paging Paging + DSName zombies Zombies + DSName blocked Blocked + DSName stopped Stopped + Order paging blocked zombies stopped running sleeping + Color running 00e000 + Color sleeping 0000ff + Color paging ffb000 + Color zombies ff0000 + Color blocked ff00ff + Color stopped a000a0 + + + Module GenericStacked + DataSources value + RRDTitle "TCP connections ({plugin_instance})" + RRDVerticalLabel "Connections" + RRDFormat "%5.1lf" + Order LISTEN CLOSING LAST_ACK CLOSE_WAIT CLOSE TIME_WAIT FIN_WAIT2 FIN_WAIT1 SYN_RECV SYN_SENT ESTABLISHED CLOSED + Color ESTABLISHED 00e000 + Color SYN_SENT 00e0ff + Color SYN_RECV 00e0a0 + Color FIN_WAIT1 f000f0 + Color FIN_WAIT2 f000a0 + Color TIME_WAIT ffb000 + Color CLOSE 0000f0 + Color CLOSE_WAIT 0000a0 + Color LAST_ACK 000080 + Color LISTEN ff0000 + Color CLOSING 000000 + Color CLOSED 0000f0 + + + DataSources value + DSName value Temp + RRDTitle "Temperature ({type_instance})" + RRDVerticalLabel "°Celsius" + RRDFormat "%4.1lf°C" + + + DataSources users + DSName users Users + RRDTitle "Users ({type_instance}) on {hostname}" + RRDVerticalLabel "Users" + RRDFormat "%.1lf" + Color users 0000f0 + + + DataSources value + DSName value Volts + RRDTitle "Voltage ({type_instance})" + RRDVerticalLabel "Volts" + RRDFormat "%4.1lfV" + Color value f00000 + +# vim: set sw=2 sts=2 et syntax=apache : diff --git a/contrib/collection3/lib/.htaccess b/contrib/collection3/lib/.htaccess new file mode 100644 index 00000000..3a428827 --- /dev/null +++ b/contrib/collection3/lib/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/contrib/collection3/lib/Collectd/Graph/Common.pm b/contrib/collection3/lib/Collectd/Graph/Common.pm new file mode 100644 index 00000000..ec171dae --- /dev/null +++ b/contrib/collection3/lib/Collectd/Graph/Common.pm @@ -0,0 +1,585 @@ +package Collectd::Graph::Common; + +use strict; +use warnings; + +use vars (qw($ColorCanvas $ColorFullBlue $ColorHalfBlue)); + +use Carp (qw(confess cluck)); +use CGI (':cgi'); +use Exporter; + +$ColorCanvas = 'FFFFFF'; +$ColorFullBlue = '0000FF'; +$ColorHalfBlue = 'B7B7F7'; + +@Collectd::Graph::Common::ISA = ('Exporter'); +@Collectd::Graph::Common::EXPORT_OK = (qw( + $ColorCanvas + $ColorFullBlue + $ColorHalfBlue + + sanitize_hostname + sanitize_plugin sanitize_plugin_instance + sanitize_type sanitize_type_instance + group_files_by_plugin_instance + get_files_from_directory + filename_to_ident + ident_to_filename + ident_to_string + get_all_hosts + get_files_for_host + get_files_by_ident + get_selected_files + get_timespan_selection + get_host_selection + get_plugin_selection + get_faded_color + sort_idents_by_type_instance + type_to_module_name + epoch_to_rfc1123 +)); + +our $DataDir = '/var/lib/collectd/rrd'; + +return (1); + +sub _sanitize_generic_allow_minus +{ + my $str = "" . shift; + + # remove all slashes + $str =~ s#/##g; + + # remove all dots and dashes at the beginning and at the end. + $str =~ s#^[\.-]+##; + $str =~ s#[\.-]+$##; + + return ($str); +} + +sub _sanitize_generic_no_minus +{ + # Do everything the allow-minus variant does.. + my $str = _sanitize_generic_allow_minus (@_); + + # .. and remove the dashes, too + $str =~ s#/##g; + + return ($str); +} # _sanitize_generic_no_minus + +sub sanitize_hostname +{ + return (_sanitize_generic_allow_minus (@_)); +} + +sub sanitize_plugin +{ + return (_sanitize_generic_no_minus (@_)); +} + +sub sanitize_plugin_instance +{ + return (_sanitize_generic_allow_minus (@_)); +} + +sub sanitize_type +{ + return (_sanitize_generic_no_minus (@_)); +} + +sub sanitize_type_instance +{ + return (_sanitize_generic_allow_minus (@_)); +} + +sub group_files_by_plugin_instance +{ + my @files = @_; + my $data = {}; + + for (my $i = 0; $i < @files; $i++) + { + my $file = $files[$i]; + my $key = $file->{'plugin_instance'} || ''; + + $data->{$key} ||= []; + push (@{$data->{$key}}, $file); + } + + return ($data); +} + +sub filename_to_ident +{ + my $file = shift; + my $ret; + + if ($file =~ m#([^/]+)/([^/\-]+)(?:-([^/]+))?/([^/\-]+)(?:-([^/]+))?\.rrd$#) + { + $ret = {hostname => $1, plugin => $2, type => $4}; + if (defined ($3)) + { + $ret->{'plugin_instance'} = $3; + } + if (defined ($5)) + { + $ret->{'type_instance'} = $5; + } + if ($`) + { + $ret->{'_prefix'} = $`; + } + } + else + { + return; + } + + return ($ret); +} # filename_to_ident + +sub ident_to_filename +{ + my $ident = shift; + + my $ret = ''; + + if (defined ($ident->{'_prefix'})) + { + $ret .= $ident->{'_prefix'}; + } + else + { + $ret .= "$DataDir/"; + } + + if (!$ident->{'hostname'}) + { + cluck ("hostname is undefined") + } + if (!$ident->{'plugin'}) + { + cluck ("plugin is undefined") + } + if (!$ident->{'type'}) + { + cluck ("type is undefined") + } + + $ret .= $ident->{'hostname'} . '/' . $ident->{'plugin'}; + if (defined ($ident->{'plugin_instance'})) + { + $ret .= '-' . $ident->{'plugin_instance'}; + } + + $ret .= '/' . $ident->{'type'}; + if (defined ($ident->{'type_instance'})) + { + $ret .= '-' . $ident->{'type_instance'}; + } + $ret .= '.rrd'; + + return ($ret); +} # ident_to_filename + +sub ident_to_string +{ + my $ident = shift; + + my $ret = ''; + + $ret .= $ident->{'hostname'} . '/' . $ident->{'plugin'}; + if (defined ($ident->{'plugin_instance'})) + { + $ret .= '-' . $ident->{'plugin_instance'}; + } + + $ret .= '/' . $ident->{'type'}; + if (defined ($ident->{'type_instance'})) + { + $ret .= '-' . $ident->{'type_instance'}; + } + + return ($ret); +} # ident_to_string + +sub get_files_from_directory +{ + my $dir = shift; + my $recursive = @_ ? shift : 0; + my $dh; + my @directories = (); + my $ret = []; + + opendir ($dh, $dir) or die ("opendir ($dir): $!"); + while (my $entry = readdir ($dh)) + { + next if ($entry =~ m/^\./); + + $entry = "$dir/$entry"; + + if (-d $entry) + { + push (@directories, $entry); + } + elsif (-f $entry) + { + my $ident = filename_to_ident ($entry); + if ($ident) + { + push (@$ret, $ident); + } + } + } + closedir ($dh); + + if ($recursive > 0) + { + for (@directories) + { + my $temp = get_files_from_directory ($_, $recursive - 1); + if ($temp && @$temp) + { + push (@$ret, @$temp); + } + } + } + + return ($ret); +} # get_files_from_directory + +sub get_all_hosts +{ + my $dh; + my @ret = (); + + opendir ($dh, "$DataDir") or confess ("opendir ($DataDir): $!"); + while (my $entry = readdir ($dh)) + { + next if ($entry =~ m/^\./); + next if (!-d "$DataDir/$entry"); + push (@ret, sanitize_hostname ($entry)); + } + closedir ($dh); + + if (wantarray ()) + { + return (@ret); + } + elsif (@ret) + { + return (\@ret); + } + else + { + return; + } +} # get_all_hosts + +sub get_all_plugins +{ + my @hosts = @_; + my $ret = {}; + my $dh; + + if (!@hosts) + { + @hosts = get_all_hosts (); + } + + for (@hosts) + { + my $host = $_; + opendir ($dh, "$DataDir/$host") or next; + while (my $entry = readdir ($dh)) + { + my $plugin; + my $plugin_instance = ''; + + next if ($entry =~ m/^\./); + next if (!-d "$DataDir/$host/$entry"); + + if ($entry =~ m#^([^-]+)-(.+)$#) + { + $plugin = $1; + $plugin_instance = $2; + } + elsif ($entry =~ m#^([^-]+)$#) + { + $plugin = $1; + $plugin_instance = ''; + } + else + { + next; + } + + $ret->{$plugin} ||= {}; + $ret->{$plugin}{$plugin_instance} = 1; + } # while (readdir) + closedir ($dh); + } # for (@hosts) + + if (wantarray ()) + { + return (sort (keys %$ret)); + } + else + { + return ($ret); + } +} # get_all_plugins + +sub get_files_for_host +{ + my $host = sanitize_hostname (shift); + return (get_files_from_directory ("$DataDir/$host", 2)); +} # get_files_for_host + +sub _filter_ident +{ + my $filter = shift; + my $ident = shift; + + for (qw(hostname plugin plugin_instance type type_instance)) + { + my $part = $_; + my $tmp; + + if (!defined ($filter->{$part})) + { + next; + } + if (!defined ($ident->{$part})) + { + return (1); + } + + if (ref $filter->{$part}) + { + if (!grep { $ident->{$part} eq $_ } (@{$filter->{$part}})) + { + return (1); + } + } + else + { + if ($ident->{$part} ne $filter->{$part}) + { + return (1); + } + } + } + + return (0); +} # _filter_ident + +sub get_files_by_ident +{ + my $ident = shift; + my $all_files; + my @ret = (); + + #if ($ident->{'hostname'}) + #{ + #$all_files = get_files_for_host ($ident->{'hostname'}); + #} + #else + #{ + $all_files = get_files_from_directory ($DataDir, 3); + #} + + @ret = grep { _filter_ident ($ident, $_) == 0 } (@$all_files); + + return (\@ret); +} # get_files_by_ident + +sub get_selected_files +{ + my $ident = {}; + + for (qw(hostname plugin plugin_instance type type_instance)) + { + my $part = $_; + my @temp = param ($part); + if (!@temp) + { + next; + } + elsif (($part eq 'plugin') || ($part eq 'type')) + { + $ident->{$part} = [map { _sanitize_generic_no_minus ($_) } (@temp)]; + } + else + { + $ident->{$part} = [map { _sanitize_generic_allow_minus ($_) } (@temp)]; + } + } + + return (get_files_by_ident ($ident)); +} # get_selected_files + +sub get_timespan_selection +{ + my $ret = 86400; + if (param ('timespan')) + { + my $temp = int (param ('timespan')); + if ($temp && ($temp > 0)) + { + $ret = $temp; + } + } + + return ($ret); +} # get_timespan_selection + +sub get_host_selection +{ + my %ret = (); + + for (get_all_hosts ()) + { + $ret{$_} = 0; + } + + for (param ('hostname')) + { + my $host = _sanitize_generic_allow_minus ($_); + if (defined ($ret{$host})) + { + $ret{$host} = 1; + } + } + + if (wantarray ()) + { + return (grep { $ret{$_} > 0 } (sort (keys %ret))); + } + else + { + return (\%ret); + } +} # get_host_selection + +sub get_plugin_selection +{ + my %ret = (); + my @hosts = get_host_selection (); + + for (get_all_plugins (@hosts)) + { + $ret{$_} = 0; + } + + for (param ('plugin')) + { + if (defined ($ret{$_})) + { + $ret{$_} = 1; + } + } + + if (wantarray ()) + { + return (grep { $ret{$_} > 0 } (sort (keys %ret))); + } + else + { + return (\%ret); + } +} # get_plugin_selection + +sub _string_to_color +{ + my $color = shift; + 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])/) + { + return ([hex ($1) / 255.0, hex ($2) / 255.0, hex ($3) / 255.0]); + } + return; +} # _string_to_color + +sub _color_to_string +{ + confess ("Wrong number of arguments") if (@_ != 1); + return (sprintf ('%02hx%02hx%02hx', map { int (255.0 * $_) } @{$_[0]})); +} # _color_to_string + +sub get_faded_color +{ + my $fg = shift; + my $bg; + my %opts = @_; + my $ret = [undef, undef, undef]; + + $opts{'background'} ||= [1.0, 1.0, 1.0]; + $opts{'alpha'} ||= 0.25; + + if (!ref ($fg)) + { + $fg = _string_to_color ($fg) + or confess ("Cannot parse foreground color $fg"); + } + + if (!ref ($opts{'background'})) + { + $opts{'background'} = _string_to_color ($opts{'background'}) + or confess ("Cannot parse background color " . $opts{'background'}); + } + $bg = $opts{'background'}; + + for (my $i = 0; $i < 3; $i++) + { + $ret->[$i] = ($opts{'alpha'} * $fg->[$i]) + + ((1.0 - $opts{'alpha'}) * $bg->[$i]); + } + + return (_color_to_string ($ret)); +} # get_faded_color + +sub sort_idents_by_type_instance +{ + my $idents = shift; + my $array_sort = shift; + + my %elements = map { $_->{'type_instance'} => $_ } (@$idents); + splice (@$idents, 0); + + for (@$array_sort) + { + next if (!exists ($elements{$_})); + push (@$idents, $elements{$_}); + delete ($elements{$_}); + } + push (@$idents, map { $elements{$_} } (sort (keys %elements))); +} # sort_idents_by_type_instance + +sub type_to_module_name +{ + my $type = shift; + my $ret; + + $ret = ucfirst (lc ($type)); + + $ret =~ s/[^A-Za-z_]//g; + $ret =~ s/_([A-Za-z])/\U$1\E/g; + + return ("Collectd::Graph::Type::$ret"); +} # type_to_module_name + +sub epoch_to_rfc1123 +{ + my @days = (qw(Sun Mon Tue Wed Thu Fri Sat)); + my @months = (qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)); + + my $epoch = @_ ? shift : time (); + my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch); + my $string = sprintf ('%s, %02d %s %4d %02d:%02d:%02d GMT', $days[$wday], $mday, + $months[$mon], 1900 + $year, $hour ,$min, $sec); + return ($string); +} + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 : diff --git a/contrib/collection3/lib/Collectd/Graph/Type.pm b/contrib/collection3/lib/Collectd/Graph/Type.pm new file mode 100644 index 00000000..60097e5c --- /dev/null +++ b/contrib/collection3/lib/Collectd/Graph/Type.pm @@ -0,0 +1,480 @@ +package Collectd::Graph::Type; + +=head1 NAME + +Collectd::Graph::Type - Base class for the collectd graphing infrastructure + +=cut + +# Copyright (C) 2008 Florian octo Forster +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; only version 2 of the License is applicable. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +use Carp (qw(confess cluck)); +use RRDs (); +use URI::Escape (qw(uri_escape)); + +use Collectd::Graph::Common (qw($ColorCanvas $ColorFullBlue $ColorHalfBlue + ident_to_filename + ident_to_string + get_faded_color)); + +return (1); + +=head1 DESCRIPTION + +This module serves as base class for more specialized classes realizing +specific "types". + +=head1 MEMBER VARIABLES + +As typical in Perl, a Collectd::Graph::Type object is a blessed hash reference. +Member variables are entries in that hash. Inheriting classes are free to add +additional entries. To set the member variable B to B<42>, do: + + $obj->{'foo'} = 42; + +The following members control the behavior of Collectd::Graph::Type. + +=over 4 + +=item B (array reference) + +List of RRD files. Each file is passed as "ident", i.Ee. broken up into +"hostname", "plugin", "type" and optionally "plugin_instance" and +"type_instance". Use the B method rather than setting this directly. + +=item B (array reference) + +List of data sources in the RRD files. If this is not given, the default +implementation of B will use B to find out which +data sources are contained in the files. + +=item B (array reference) + +Names of the data sources as printed in the graph. Should be in the same order +as the data sources are returned by B. + +=item B (string) + +Title of the RRD graph. The title can contain "{hostname}", "{plugin}" and so +on which are replaced with their actual value. See the B method +below. + +=item B (array reference) + +List of options directly passed to B. + +=item B (string) + +Format to use with B. Defaults to C<%5.1lf>. + +=item B (hash reference) + +Mapping of data source names to colors, used when graphing the different data +sources. Colors are given in the typical hexadecimal RGB form, but without +leading "#", e.Eg.: + + $obj->{'rrd_colors'} = {foo => 'ff0000', bar => '00ff00'}; + +=back + +=head1 METHODS + +The following methods are used by the graphing front end and may be overwritten +to customize their behavior. + +=over 4 + +=cut + +sub _get_ds_from_file +{ + my $file = shift; + my $info = RRDs::info ($file); + my %ds = (); + my @ds = (); + + if (!$info || (ref ($info) ne 'HASH')) + { + return; + } + + for (keys %$info) + { + if (m/^ds\[([^\]]+)\]/) + { + $ds{$1} = 1; + } + } + + @ds = (keys %ds); + if (wantarray ()) + { + return (@ds); + } + elsif (@ds) + { + return (\@ds); + } + else + { + return; + } +} # _get_ds_from_file + +sub new +{ + my $pkg = shift; + my $obj = bless ({files => []}, $pkg); + + if (@_) + { + $obj->addFiles (@_); + } + + return ($obj); +} + +=item B ({ I }, [...]) + +Adds the given idents (which are hash references) to the B member +variable, see above. + +=cut + +sub addFiles +{ + my $obj = shift; + push (@{$obj->{'files'}}, @_); +} + +=item B () + +Returns the number of graphs that can be generated from the added files. By +default this number equals the number of files. + +=cut + +sub getGraphsNum +{ + my $obj = shift; + return (scalar @{$obj->{'files'}}); +} + +=item B () + +Returns the names of the data sources. If the B member variable +is unset B is used to read that information from the first file. +Set the B member variable instead of overloading this method! + +=cut + +sub getDataSources +{ + my $obj = shift; + + if (!defined $obj->{'data_sources'}) + { + my $ident; + my $filename; + + if (!@{$obj->{'files'}}) + { + return; + } + + $ident = $obj->{'files'}[0]; + $filename = ident_to_filename ($ident); + + $obj->{'data_sources'} = _get_ds_from_file ($filename); + if (!$obj->{'data_sources'}) + { + cluck ("_get_ds_from_file ($filename) failed."); + } + } + + if (!defined $obj->{'data_sources'}) + { + return; + } + elsif (wantarray ()) + { + return (@{$obj->{'data_sources'}}) + } + else + { + $obj->{'data_sources'}; + } +} # getDataSources + + +=item B (I<$index>) + +Returns the title of the I<$index>th B (not necessarily file!). If the +B member variable is unset, a generic title is generated from the +ident. Otherwise the substrings "{hostname}", "{plugin}", "{plugin_instance}", +"{type}", and "{type_instance}" are replaced by their respective values. + +=cut + +sub getTitle +{ + my $obj = shift; + my $ident = shift; + my $title = $obj->{'rrd_title'}; + + if (!$title) + { + return (ident_to_string ($ident)); + } + + my $hostname = $ident->{'hostname'}; + my $plugin = $ident->{'plugin'}; + my $plugin_instance = $ident->{'plugin_instance'}; + my $type = $ident->{'type'}; + my $type_instance = $ident->{'type_instance'}; + + if (!defined $plugin_instance) + { + $plugin_instance = 'no instance'; + } + + if (!defined $type_instance) + { + $type_instance = 'no instance'; + } + + $title =~ s#{hostname}#$hostname#g; + $title =~ s#{plugin}#$plugin#g; + $title =~ s#{plugin_instance}#$plugin_instance#g; + $title =~ s#{type}#$type#g; + $title =~ s#{type_instance}#$type_instance#g; + + return ($title); +} + +=item B (I<$index>) + +Return the arguments needed to generate the graph from the RRD file(s). If the +file has only one data source, this default implementation will generate that +typical min, average, max graph you probably know from temperatures and such. +If the RRD files have multiple data sources, the average of each data source is +printes as simple line. + +=cut + +sub getRRDArgs +{ + my $obj = shift; + my $index = shift; + + my $ident = $obj->{'files'}[$index]; + if (!$ident) + { + cluck ("Invalid index: $index"); + return; + } + my $filename = ident_to_filename ($ident); + + my $rrd_opts = $obj->{'rrd_opts'} || []; + my $rrd_title = $obj->getTitle ($ident); + my $format = $obj->{'rrd_format'} || '%5.1lf'; + + my $rrd_colors = $obj->{'rrd_colors'}; + my @ret = ('-t', $rrd_title, @$rrd_opts); + + if (defined $obj->{'rrd_vertical'}) + { + push (@ret, '-v', $obj->{'rrd_vertical'}); + } + + my $ds_names = $obj->{'ds_names'}; + if (!$ds_names) + { + $ds_names = {}; + } + + my $ds = $obj->getDataSources (); + if (!$ds) + { + confess ("obj->getDataSources failed."); + } + + if (!$rrd_colors) + { + my @tmp = ('0000ff', 'ff0000', '00ff00', 'ff00ff', '00ffff', 'ffff00'); + + for (my $i = 0; $i < @$ds; $i++) + { + $rrd_colors->{$ds->[$i]} = $tmp[$i % @tmp]; + } + } + + for (my $i = 0; $i < @$ds; $i++) + { + my $f = $filename; + my $ds_name = $ds->[$i]; + + # We need to escape colons for RRDTool.. + $f =~ s#:#\\:#g; + $ds_name =~ s#:#\\:#g; + + push (@ret, + "DEF:min${i}=${f}:${ds_name}:MIN", + "DEF:avg${i}=${f}:${ds_name}:AVERAGE", + "DEF:max${i}=${f}:${ds_name}:MAX"); + } + + if (@$ds == 1) + { + my $ds_name = $ds->[0]; + my $color_fg = $rrd_colors->{$ds_name} || '000000'; + my $color_bg = get_faded_color ($color_fg); + + if ($ds_names->{$ds_name}) + { + $ds_name = $ds_names->{$ds_name}; + } + $ds_name =~ s#:#\\:#g; + + push (@ret, + "AREA:max0#${color_bg}", + "AREA:min0#${ColorCanvas}", + "LINE1:avg0#${color_fg}:${ds_name}", + "GPRINT:min0:MIN:${format} Min,", + "GPRINT:avg0:AVERAGE:${format} Avg,", + "GPRINT:max0:MAX:${format} Max,", + "GPRINT:avg0:LAST:${format} Last\\l"); + } + else + { + for (my $i = 0; $i < @$ds; $i++) + { + my $ds_name = $ds->[$i]; + my $color = $rrd_colors->{$ds_name} || '000000'; + + if ($ds_names->{$ds_name}) + { + $ds_name = $ds_names->{$ds_name}; + } + + push (@ret, + "LINE1:avg${i}#${color}:${ds_name}", + "GPRINT:min${i}:MIN:${format} Min,", + "GPRINT:avg${i}:AVERAGE:${format} Avg,", + "GPRINT:max${i}:MAX:${format} Max,", + "GPRINT:avg${i}:LAST:${format} Last\\l"); + } + } + + return (\@ret); +} # getRRDArgs + +=item B (I<$index>) + +Returns the parameters that should be passed to the CGI script to generate the +I<$index>th graph. The returned string is already URI-encoded and will possibly +set the "hostname", "plugin", "plugin_instance", "type", and "type_instance" +parameters. + +The default implementation simply uses the ident of the I<$index>th file to +fill this. + +=cut + +sub getGraphArgs +{ + my $obj = shift; + my $index = shift; + my $ident = $obj->{'files'}[$index]; + + my @args = (); + for (qw(hostname plugin plugin_instance type type_instance)) + { + if (defined ($ident->{$_})) + { + push (@args, uri_escape ($_) . '=' . uri_escape ($ident->{$_})); + } + } + + return (join (';', @args)); +} + +=item B ([I<$index>]) + +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. + +=cut + +sub getLastModified +{ + my $obj = shift; + my $index = @_ ? shift : -1; + + my $mtime = 0; + + if ($index == -1) + { + for (@{$obj->{'files'}}) + { + my $ident = $_; + my $filename = ident_to_filename ($ident); + my @statbuf = stat ($filename); + + if (!@statbuf) + { + next; + } + + if ($mtime < $statbuf[9]) + { + $mtime = $statbuf[9]; + } + } + } + else + { + my $ident = $obj->{'files'}[$index]; + my $filename = ident_to_filename ($ident); + my @statbuf = stat ($filename); + + $mtime = $statbuf[9]; + } + + if (!$mtime) + { + return; + } + return ($mtime); +} # getLastModified + +=back + +=head1 SEE ALSO + +L + +=head1 AUTHOR AND LICENSE + +Copyright (c) 2008 by Florian Forster +EoctoEatEverplant.orgE. Licensed under the terms of the GNU +General Public License, VersionE2 (GPLv2). + +=cut + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 : diff --git a/contrib/collection3/lib/Collectd/Graph/Type/Df.pm b/contrib/collection3/lib/Collectd/Graph/Type/Df.pm new file mode 100644 index 00000000..b4eb8b1c --- /dev/null +++ b/contrib/collection3/lib/Collectd/Graph/Type/Df.pm @@ -0,0 +1,68 @@ +package Collectd::Graph::Type::Df; + +use strict; +use warnings; +use base ('Collectd::Graph::Type'); + +use Collectd::Graph::Common (qw(ident_to_filename get_faded_color)); + +return (1); + +sub getDataSources +{ + return ([qw(free used)]); +} # getDataSources + +sub new +{ + my $pkg = shift; + my $obj = Collectd::Graph::Type->new (@_); + $obj->{'data_sources'} = [qw(free used)]; + $obj->{'rrd_opts'} = ['-v', 'Bytes']; + $obj->{'rrd_title'} = 'Disk space ({type_instance})'; + $obj->{'rrd_format'} = '%5.1lf%sB'; + $obj->{'colors'} = [qw(00b000 ff0000)]; + + return (bless ($obj, $pkg)); +} # new + +sub getRRDArgs +{ + my $obj = shift; + my $index = shift; + + my $ident = $obj->{'files'}[$index]; + if (!$ident) + { + cluck ("Invalid index: $index"); + return; + } + my $filename = ident_to_filename ($ident); + $filename =~ s#:#\\:#g; + + my $faded_green = get_faded_color ('00ff00'); + my $faded_red = get_faded_color ('ff0000'); + + return (['-t', 'Free space (' . $ident->{'type_instance'} . ')', '-v', 'Bytes', '-l', '0', + "DEF:free_min=${filename}:free:MIN", + "DEF:free_avg=${filename}:free:AVERAGE", + "DEF:free_max=${filename}:free:MAX", + "DEF:used_min=${filename}:used:MIN", + "DEF:used_avg=${filename}:used:AVERAGE", + "DEF:used_max=${filename}:used:MAX", + "CDEF:both_avg=free_avg,used_avg,+", + "AREA:both_avg#${faded_green}", + "AREA:used_avg#${faded_red}", + 'LINE1:both_avg#00ff00:Free', + 'GPRINT:free_min:MIN:%5.1lf%sB Min,', + 'GPRINT:free_avg:AVERAGE:%5.1lf%sB Avg,', + 'GPRINT:free_max:MAX:%5.1lf%sB Max,', + 'GPRINT:free_avg:LAST:%5.1lf%sB Last\l', + 'LINE1:used_avg#ff0000:Used', + 'GPRINT:used_min:MIN:%5.1lf%sB Min,', + 'GPRINT:used_avg:AVERAGE:%5.1lf%sB Avg,', + 'GPRINT:used_max:MAX:%5.1lf%sB Max,', + 'GPRINT:used_avg:LAST:%5.1lf%sB Last\l']); +} # getRRDArgs + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 : diff --git a/contrib/collection3/lib/Collectd/Graph/Type/GenericIO.pm b/contrib/collection3/lib/Collectd/Graph/Type/GenericIO.pm new file mode 100644 index 00000000..58b7566e --- /dev/null +++ b/contrib/collection3/lib/Collectd/Graph/Type/GenericIO.pm @@ -0,0 +1,113 @@ +package Collectd::Graph::Type::GenericIO; + +use strict; +use warnings; +use base ('Collectd::Graph::Type'); + +use Carp ('confess'); + +use Collectd::Graph::Common (qw($ColorCanvas ident_to_filename get_faded_color)); + +return (1); + +sub getRRDArgs +{ + my $obj = shift; + my $index = shift; + + my $ident = $obj->{'files'}[$index] || confess ("Unknown index $index"); + my $filename = ident_to_filename ($ident); + + my $rrd_opts = $obj->{'rrd_opts'} || []; + my $rrd_title = $obj->getTitle ($ident); + my $format = $obj->{'rrd_format'} || '%5.1lf%s'; + + my $ds_list = $obj->getDataSources (); + my $ds_names = $obj->{'ds_names'}; + if (!$ds_names) + { + $ds_names = {}; + } + + my $colors = $obj->{'rrd_colors'} || {}; + my @ret = ('-t', $rrd_title, @$rrd_opts); + + if (defined $obj->{'rrd_vertical'}) + { + push (@ret, '-v', $obj->{'rrd_vertical'}); + } + + if (@$ds_list != 2) + { + my $num = 0 + @$ds_list; + confess ("Expected two data sources, but there is/are $num"); + } + + my $rx_ds = $ds_list->[0]; + my $tx_ds = $ds_list->[1]; + + my $rx_ds_name = $ds_names->{$rx_ds} || $rx_ds; + my $tx_ds_name = $ds_names->{$tx_ds} || $tx_ds; + + my $rx_color_fg = $colors->{$rx_ds} || '0000ff'; + my $tx_color_fg = $colors->{$tx_ds} || '00b000'; + + my $rx_color_bg = get_faded_color ($rx_color_fg); + my $tx_color_bg = get_faded_color ($tx_color_fg); + my $overlap_color = get_faded_color ($rx_color_bg, background => $tx_color_bg); + + $filename =~ s#:#\\:#g; + $rx_ds =~ s#:#\\:#g; + $tx_ds =~ s#:#\\:#g; + $rx_ds_name =~ s#:#\\:#g; + $tx_ds_name =~ s#:#\\:#g; + + if ($obj->{'scale'}) + { + my $factor = $obj->{'scale'}; + + push (@ret, + "DEF:min_rx_raw=${filename}:${rx_ds}:MIN", + "DEF:avg_rx_raw=${filename}:${rx_ds}:AVERAGE", + "DEF:max_rx_raw=${filename}:${rx_ds}:MAX", + "DEF:min_tx_raw=${filename}:${tx_ds}:MIN", + "DEF:avg_tx_raw=${filename}:${tx_ds}:AVERAGE", + "DEF:max_tx_raw=${filename}:${tx_ds}:MAX", + "CDEF:min_rx=min_rx_raw,${factor},*", + "CDEF:avg_rx=avg_rx_raw,${factor},*", + "CDEF:max_rx=max_rx_raw,${factor},*", + "CDEF:min_tx=min_tx_raw,${factor},*", + "CDEF:avg_tx=avg_tx_raw,${factor},*", + "CDEF:max_tx=max_tx_raw,${factor},*"); + } + else # (!$obj->{'scale'}) + { + push (@ret, + "DEF:min_rx=${filename}:${rx_ds}:MIN", + "DEF:avg_rx=${filename}:${rx_ds}:AVERAGE", + "DEF:max_rx=${filename}:${rx_ds}:MAX", + "DEF:min_tx=${filename}:${tx_ds}:MIN", + "DEF:avg_tx=${filename}:${tx_ds}:AVERAGE", + "DEF:max_tx=${filename}:${tx_ds}:MAX"); + } + + push (@ret, + "CDEF:overlap=avg_rx,avg_tx,LT,avg_rx,avg_tx,IF", + "AREA:avg_rx#${rx_color_bg}", + "AREA:avg_tx#${tx_color_bg}", + "AREA:overlap#${overlap_color}", + "LINE1:avg_rx#${rx_color_fg}:${rx_ds_name}", + "GPRINT:min_rx:MIN:${format} Min,", + "GPRINT:avg_rx:AVERAGE:${format} Avg,", + "GPRINT:max_rx:MAX:${format} Max,", + "GPRINT:avg_rx:LAST:${format} Last\\l", + "LINE1:avg_tx#${tx_color_fg}:${tx_ds_name}", + "GPRINT:min_tx:MIN:${format} Min,", + "GPRINT:avg_tx:AVERAGE:${format} Avg,", + "GPRINT:max_tx:MAX:${format} Max,", + "GPRINT:avg_tx:LAST:${format} Last\\l"); + + return (\@ret); +} # getRRDArgs + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 : diff --git a/contrib/collection3/lib/Collectd/Graph/Type/GenericStacked.pm b/contrib/collection3/lib/Collectd/Graph/Type/GenericStacked.pm new file mode 100644 index 00000000..273d89e6 --- /dev/null +++ b/contrib/collection3/lib/Collectd/Graph/Type/GenericStacked.pm @@ -0,0 +1,145 @@ +package Collectd::Graph::Type::GenericStacked; + +use strict; +use warnings; +use base ('Collectd::Graph::Type'); + +use Collectd::Graph::Common (qw($ColorCanvas $ColorFullBlue $ColorHalfBlue + group_files_by_plugin_instance ident_to_filename sanitize_type_instance + get_faded_color sort_idents_by_type_instance)); + +return (1); + +sub getGraphsNum +{ + my $obj = shift; + my $group = group_files_by_plugin_instance (@{$obj->{'files'}}); + + return (scalar (keys %$group)); +} + +sub getRRDArgs +{ + my $obj = shift; + my $index = shift; + + my $group = group_files_by_plugin_instance (@{$obj->{'files'}}); + my @group = sort (keys %$group); + + my $rrd_opts = $obj->{'rrd_opts'} || []; + my $format = $obj->{'rrd_format'} || '%5.1lf'; + + my $idents = $group->{$group[$index]}; + my $ds_name_len = 0; + + my $rrd_title = $obj->getTitle ($idents->[0]); + + my $colors = $obj->{'rrd_colors'} || {}; + my @ret = ('-t', $rrd_title, @$rrd_opts); + + if (defined $obj->{'rrd_vertical'}) + { + push (@ret, '-v', $obj->{'rrd_vertical'}); + } + + if ($obj->{'custom_order'}) + { + sort_idents_by_type_instance ($idents, $obj->{'custom_order'}); + } + + $obj->{'ds_names'} ||= {}; + my @names = map { $obj->{'ds_names'}{$_->{'type_instance'}} || $_->{'type_instance'} } (@$idents); + + for (my $i = 0; $i < @$idents; $i++) + { + my $ident = $idents->[$i]; + my $filename = ident_to_filename ($ident); + + if ($ds_name_len < length ($names[$i])) + { + $ds_name_len = length ($names[$i]); + } + + # Escape colons _after_ the length has been checked. + $names[$i] =~ s/:/\\:/g; + + push (@ret, + "DEF:min${i}=${filename}:value:MIN", + "DEF:avg${i}=${filename}:value:AVERAGE", + "DEF:max${i}=${filename}:value:MAX"); + } + + for (my $i = @$idents - 1; $i >= 0; $i--) + { + if ($i == (@$idents - 1)) + { + push (@ret, + "CDEF:cdef${i}=avg${i}"); + } + else + { + my $j = $i + 1; + push (@ret, + "CDEF:cdef${i}=cdef${j},avg${i},+"); + } + } + + for (my $i = 0; $i < @$idents; $i++) + { + my $type_instance = $idents->[$i]{'type_instance'}; + my $color = '000000'; + if (exists $colors->{$type_instance}) + { + $color = $colors->{$type_instance}; + } + + $color = get_faded_color ($color); + + push (@ret, + "AREA:cdef${i}#${color}"); + } + + for (my $i = 0; $i < @$idents; $i++) + { + my $type_instance = $idents->[$i]{'type_instance'}; + my $ds_name = sprintf ("%-*s", $ds_name_len, $names[$i]); + my $color = '000000'; + if (exists $colors->{$type_instance}) + { + $color = $colors->{$type_instance}; + } + push (@ret, + "LINE1:cdef${i}#${color}:${ds_name}", + "GPRINT:min${i}:MIN:${format} Min,", + "GPRINT:avg${i}:AVERAGE:${format} Avg,", + "GPRINT:max${i}:MAX:${format} Max,", + "GPRINT:avg${i}:LAST:${format} Last\\l"); + } + + return (\@ret); +} + +sub getGraphArgs +{ + my $obj = shift; + my $index = shift; + + my $group = group_files_by_plugin_instance (@{$obj->{'files'}}); + my @group = sort (keys %$group); + + my $idents = $group->{$group[$index]}; + + my @args = (); + for (qw(hostname plugin plugin_instance type)) + { + if (defined ($idents->[0]{$_})) + { + push (@args, $_ . '=' . $idents->[0]{$_}); + } + } + + return (join (';', @args)); +} # getGraphArgs + + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 : diff --git a/contrib/collection3/lib/Collectd/Graph/Type/Load.pm b/contrib/collection3/lib/Collectd/Graph/Type/Load.pm new file mode 100644 index 00000000..f13665e8 --- /dev/null +++ b/contrib/collection3/lib/Collectd/Graph/Type/Load.pm @@ -0,0 +1,71 @@ +package Collectd::Graph::Type::Load; + +use strict; +use warnings; +use base ('Collectd::Graph::Type'); + +use Collectd::Graph::Common (qw($ColorCanvas ident_to_filename get_faded_color)); + +return (1); + +sub new +{ + my $pkg = shift; + my $obj = Collectd::Graph::Type->new (@_); + $obj->{'data_sources'} = [qw(shortterm midterm longterm)]; + $obj->{'rrd_opts'} = ['-v', 'System load']; + $obj->{'rrd_title'} = 'System load'; + $obj->{'rrd_format'} = '%.2lf'; + $obj->{'colors'} = [qw(00ff00 0000ff ff0000)]; + + print STDERR "Hi, this is Collectd::Graph::Type::Load::new\n"; + + return (bless ($obj, $pkg)); +} # new + +sub getRRDArgs +{ + my $obj = shift; + my $index = shift; + + my $ident = $obj->{'files'}[$index]; + if (!$ident) + { + cluck ("Invalid index: $index"); + return; + } + my $filename = ident_to_filename ($ident); + $filename =~ s#:#\\:#g; + + my $faded_green = get_faded_color ('00ff00'); + + return (['-t', 'System load', '-v', 'System load', + "DEF:s_min=${filename}:shortterm:MIN", + "DEF:s_avg=${filename}:shortterm:AVERAGE", + "DEF:s_max=${filename}:shortterm:MAX", + "DEF:m_min=${filename}:midterm:MIN", + "DEF:m_avg=${filename}:midterm:AVERAGE", + "DEF:m_max=${filename}:midterm:MAX", + "DEF:l_min=${filename}:longterm:MIN", + "DEF:l_avg=${filename}:longterm:AVERAGE", + "DEF:l_max=${filename}:longterm:MAX", + "AREA:s_max#${faded_green}", + "AREA:s_min#${ColorCanvas}", + "LINE1:s_avg#00ff00: 1 min", + "GPRINT:s_min:MIN:%.2lf Min,", + "GPRINT:s_avg:AVERAGE:%.2lf Avg,", + "GPRINT:s_max:MAX:%.2lf Max,", + "GPRINT:s_avg:LAST:%.2lf Last\\l", + "LINE1:m_avg#0000ff: 5 min", + "GPRINT:m_min:MIN:%.2lf Min,", + "GPRINT:m_avg:AVERAGE:%.2lf Avg,", + "GPRINT:m_max:MAX:%.2lf Max,", + "GPRINT:m_avg:LAST:%.2lf Last\\l", + "LINE1:l_avg#ff0000:15 min", + "GPRINT:l_min:MIN:%.2lf Min,", + "GPRINT:l_avg:AVERAGE:%.2lf Avg,", + "GPRINT:l_max:MAX:%.2lf Max,", + "GPRINT:l_avg:LAST:%.2lf Last\\l"]); +} # sub getRRDArgs + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 : diff --git a/contrib/collection3/lib/Collectd/Graph/TypeLoader.pm b/contrib/collection3/lib/Collectd/Graph/TypeLoader.pm new file mode 100644 index 00000000..6223eaa0 --- /dev/null +++ b/contrib/collection3/lib/Collectd/Graph/TypeLoader.pm @@ -0,0 +1,301 @@ +package Collectd::Graph::TypeLoader; + +=head1 NAME + +Collectd::Graph::TypeLoader - Load a module according to the "type" + +=cut + +# Copyright (C) 2008 Florian octo Forster +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; only version 2 of the License is applicable. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +use Carp (qw(cluck confess)); +use Exporter (); +use Config::General ('ParseConfig'); +use Collectd::Graph::Type (); + +@Collectd::Graph::TypeLoader::ISA = ('Exporter'); +@Collectd::Graph::TypeLoader::EXPORT_OK = ('tl_read_config', 'tl_load_type'); + +our $Configuration = undef; + +our @ArrayMembers = (qw(data_sources rrd_opts custom_order)); +our @ScalarMembers = (qw(rrd_title rrd_format rrd_vertical scale)); +our @DSMappedMembers = (qw(ds_names rrd_colors)); + +our %MemberToConfigMap = +( + data_sources => 'datasources', + ds_names => 'dsname', + rrd_title => 'rrdtitle', + rrd_opts => 'rrdoptions', + rrd_format => 'rrdformat', + rrd_vertical => 'rrdverticallabel', + rrd_colors => 'color', + scale => 'scale', # GenericIO only + custom_order => 'order' # GenericStacked only +); + +return (1); + +=head1 EXPORTED FUNCTIONS + +=over 4 + +=item B (I<$file>) + +Reads the configuration from the file located at I<$file>. + +=cut + +sub tl_read_config +{ + my $file = shift; + my %conf; + + if ($Configuration) + { + return (1); + } + + %conf = ParseConfig (-ConfigFile => $file, + -LowerCaseNames => 1, + -UseApacheInclude => 1, + -IncludeDirectories => 1, + ($Config::General::VERSION >= 2.38) ? (-IncludeAgain => 0) : (), + -MergeDuplicateBlocks => 1, + -CComments => 0); + if (!%conf) + { + return; + } + + $Configuration = \%conf; + return (1); +} # tl_read_config + +sub _create_object +{ + my $module = shift; + my $obj; + + local $SIG{__WARN__} = sub {}; + local $SIG{__DIE__} = sub {}; + + eval <new (); +PERL + if (!$obj) + { + return; + } + + return ($obj); +} # _create_object + +sub _load_module_from_config +{ + my $conf = shift; + + my $module = $conf->{'module'}; + my $obj; + + if ($module && !($module =~ m/::/)) + { + $module = "Collectd::Graph::Type::$module"; + } + + if ($module) + { + print STDERR "\$module = $module;\n"; + $obj = _create_object ($module); + if (!$obj) + { + cluck ("Creating an $module object failed"); + return; + } + } + else + { + $obj = Collectd::Graph::Type->new (); + if (!$obj) + { + cluck ("Creating an Collectd::Graph::Type object failed"); + return; + } + } + + for (@ScalarMembers) # {{{ + { + my $member = $_; + my $key = $MemberToConfigMap{$member}; + my $val; + + if (!defined $conf->{$key}) + { + next; + } + $val = $conf->{$key}; + + if (ref ($val) ne '') + { + cluck ("Invalid value type for $key: " . ref ($val)); + next; + } + + $obj->{$member} = $val; + } # }}} + + for (@ArrayMembers) # {{{ + { + my $member = $_; + my $key = $MemberToConfigMap{$member}; + my $val; + + if (!defined $conf->{$key}) + { + next; + } + $val = $conf->{$key}; + + if (ref ($val) eq 'ARRAY') + { + $obj->{$member} = $val; + } + elsif (ref ($val) eq '') + { + $obj->{$member} = [split (' ', $val)]; + } + else + { + cluck ("Invalid value type for $key: " . ref ($val)); + } + } # }}} + + for (@DSMappedMembers) # {{{ + { + my $member = $_; + my $key = $MemberToConfigMap{$member}; + my @val_list; + + if (!defined $conf->{$key}) + { + next; + } + + if (ref ($conf->{$key}) eq 'ARRAY') + { + @val_list = @{$conf->{$key}}; + } + elsif (ref ($conf->{$key}) eq '') + { + @val_list = ($conf->{$key}); + } + else + { + cluck ("Invalid value type for $key: " . ref ($conf->{$key})); + next; + } + + for (@val_list) + { + my $line = $_; + my $ds; + my $val; + + if (!defined ($line) || (ref ($line) ne '')) + { + next; + } + + ($ds, $val) = split (' ', $line, 2); + if (!$ds || !$val) + { + next; + } + + $obj->{$member} ||= {}; + $obj->{$member}{$ds} = $val; + + print STDERR "\$obj->{$member}{$ds} = $val;\n"; + } # for (@val_list) + } # }}} for (@DSMappedMembers) + + return ($obj); +} # _load_module_from_config + +sub _load_module_generic +{ + my $type = shift; + my $module = ucfirst (lc ($type)); + my $obj; + + $module =~ s/[^A-Za-z_]//g; + $module =~ s/_([A-Za-z])/\U$1\E/g; + + $obj = _create_object ($module); + if (!$obj) + { + $obj = Collectd::Graph::Type->new (); + if (!$obj) + { + cluck ("Creating an Collectd::Graph::Type object failed"); + return; + } + } + + return ($obj); +} # _load_module_generic + +=item B (I<$type>) + +Does whatever is necessary to get an object with which to graph RRD files of +type I<$type>. + +=cut + +sub tl_load_type +{ + my $type = shift; + + if (defined $Configuration->{'type'}{$type}) + { + return (_load_module_from_config ($Configuration->{'type'}{$type})); + } + else + { + return (_load_module_generic ($type)); + } +} # tl_load_type + +=back + +=head1 SEE ALSO + +L + +=head1 AUTHOR AND LICENSE + +Copyright (c) 2008 by Florian Forster +EoctoEatEverplant.orgE. Licensed under the terms of the GNU +General Public License, VersionE2 (GPLv2). + +=cut + +# vim: set shiftwidth=2 softtabstop=2 tabstop=8 et fdm=marker : diff --git a/contrib/collection3/share/.htaccess b/contrib/collection3/share/.htaccess new file mode 100644 index 00000000..e139ace6 --- /dev/null +++ b/contrib/collection3/share/.htaccess @@ -0,0 +1,2 @@ +Options -ExecCGI +SetHandler none diff --git a/contrib/collection3/share/shortcut-icon.png b/contrib/collection3/share/shortcut-icon.png new file mode 100644 index 00000000..6af57e57 Binary files /dev/null and b/contrib/collection3/share/shortcut-icon.png differ diff --git a/contrib/collection3/share/style.css b/contrib/collection3/share/style.css new file mode 100644 index 00000000..a6648aaf --- /dev/null +++ b/contrib/collection3/share/style.css @@ -0,0 +1,9 @@ +table +{ + border-collapse: collapse; +} +td, th +{ + border: 1px solid gray; +} +/* vim: set sw=2 sts=2 et : */