#!@@PERL@@ -w # # Copyright (C) 2002-2004 Jimmy Olsen, Audun Ytterdal # # 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; version 2 dated June, # 1991. # # 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # # $Id: munin-graph.in 1142 2006-10-17 12:27:35Z tore $ $|=1; use strict; use IO::Socket; use RRDs; use Munin; use POSIX qw(strftime); use Digest::MD5; use Getopt::Long; use Time::HiRes; my $graph_time= Time::HiRes::time; my $DEBUG = 0; my $VERSION = "@@VERSION@@"; # Limit graphing to certain hosts and/or services my @limit_hosts = (); my @limit_services = (); # RRDtool 1.2 requires \\: in comments my $RRDkludge = $RRDs::VERSION < 1.2 ? '' : '\\'; # Force drawing of "graph no". my $force_graphing = 0; my $force_lazy = 1; my $force_root = 0; my $do_usage = 0; my $do_version = 0; my $cron = 0; my $list_images = 0; my $skip_locking = 0; my $skip_stats = 0; my $stdout = 0; my $conffile = "@@CONFDIR@@/munin.conf"; my %draw = ("day" => 1, "week" => 1, "month" => 1, "year" => 1, "sumyear" => 1, "sumweek" => 1); my $log = new IO::Handle; # Get options $do_usage=1 unless GetOptions ( "force!" => \$force_graphing, "lazy!" => \$force_lazy, "force-root!" => \$force_root, "host=s" => \@limit_hosts, "service=s" => \@limit_services, "config=s" => \$conffile, "stdout!" => \$stdout, "day!" => \$draw{'day'}, "week!" => \$draw{'week'}, "month!" => \$draw{'month'}, "year!" => \$draw{'year'}, "sumweek!" => \$draw{'sumweek'}, "sumyear!" => \$draw{'sumyear'}, "list-images!" => \$list_images, "skip-locking!"=> \$skip_locking, "skip-stats!" => \$skip_stats, "debug!" => \$DEBUG, "version!" => \$do_version, "cron!" => \$cron, "help" => \$do_usage ); if ($do_usage) { print "Usage: $0 [options] Options: --[no]force Force drawing of graphs that are not usually drawn due to options in the config file. [--noforce] --[no]force-root Force running, even as root. [--noforce-root] --[no]lazy Only redraw graphs when needed. [--lazy] --help View this message. --version View version information. --debug View debug messages. --[no]cron Behave as expected when run from cron. (Used internally in Munin.) --service Limit graphed services to . Multiple --service options may be supplied. --host Limit graphed hosts to . Multiple --host options may be supplied. --config Use as configuration file. [@@CONFDIR@@/munin.conf] --[no]list-images List the filenames of the images created. [--nolist-images] --[no]day Create day-graphs. [--day] --[no]week Create week-graphs. [--week] --[no]month Create month-graphs. [--month] --[no]year Create year-graphs. [--year] --[no]sumweek Create summarised week-graphs. [--summweek] --[no]sumyear Create summarised year-graphs. [--sumyear] "; exit 0; } if ($do_version) { print "munin-graph version $VERSION.\n"; print "Written by Audun Ytterdal, Jimmy Olsen, Tore Anderson / Linpro AS\n"; print "\n"; print "Copyright (C) 2002-2004\n"; print "This is free software released under the GNU Public License. There is NO\n"; print "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"; exit 0; } if ($> == 0 and !$force_root) { print "You are running this program as root, which is neither smart nor necessary. If you really want to run it as root, use the --force-root option. Else, run it as the user \"@@USER@@\". Aborting.\n\n"; exit (1); } my $config= &munin_config ($conffile); if (&munin_get ($config, "graph_strategy", "cron") ne "cron" and $cron) { # We're run from cron, but munin.conf says we use dynamic graph generation exit 0; } munin_runlock("$config->{rundir}/munin-graph.lock") unless $skip_locking; unless ($skip_stats) { open (STATS,">$config->{dbdir}/munin-graph.stats.tmp") or logger("Unable to open $config->{dbdir}/munin-graph.stats.tmp"); } logger("Starting munin-graph"); my @COLOUR = ("#22ff22", "#0022ff", "#ff0000", "#00aaaa", "#ff00ff", "#ffa500", "#cc0000", "#0000cc", "#0080C0", "#8080C0", "#FF0080", "#800080", "#688e23", "#408080", "#808000", "#000000", "#00FF00", "#0080FF", "#FF8000", "#800000", "#FB31FB"); my $range_colour = "#22ff22"; my $single_colour = "#00aa00"; my %times = ( "day" => "-30h", "week" => "-8d", "month" => "-33d", "year" => "-400d"); my %resolutions = ( "day" => "300", "week" => "1500", "month" => "7200", "year" => "86400"); my %sumtimes = ( # time => [ label, seconds-in-period ] "week" => ["hour", 12], "year" => ["day", 288] ); for my $key ( keys %{$config->{domain}}) { my $domain_time= Time::HiRes::time; mkdir "$config->{htmldir}/$key",0777; logger("Processing domain: $key"); &process_domain($key); $domain_time = sprintf ("%.2f",(Time::HiRes::time - $domain_time)); logger("Processed domain: $key ($domain_time sec)"); print STATS "GD|$key|$domain_time\n" unless $skip_stats; } $graph_time = sprintf ("%.2f",(Time::HiRes::time - $graph_time)); logger("Munin-graph finished ($graph_time sec)"); print STATS "GT|total|$graph_time\n" unless $skip_stats; rename ("$config->{dbdir}/munin-graph.stats.tmp", "$config->{dbdir}/munin-graph.stats"); close STATS unless $skip_stats; close $log; sub process_domain { my ($domain) = @_; for my $key ( keys %{$config->{domain}->{$domain}->{node}}) { my $node_time= Time::HiRes::time; process_node($domain,$key ,$config->{domain}->{$domain}->{node}->{$key} ); $node_time = sprintf ("%.2f",(Time::HiRes::time - $node_time)); logger ("Processed node: $key ($node_time sec)"); print STATS "GN|$domain|$key|$node_time\n" unless $skip_stats; } } sub get_title { my $node = shift; my $service = shift; my $scale = shift; return ($node->{client}->{$service}->{'graph_title'}? $node->{client}->{$service}->{'graph_title'}:$service) . " - by $scale"; } sub get_custom_graph_args { my $node = shift; my $service = shift; my $result = []; if ($node->{client}->{$service}->{graph_args}) { push @$result, split /\s/,$node->{client}->{$service}->{graph_args}; return $result; } else { return undef; } } sub get_vlabel { my $node = shift; my $service = shift; my $scale = shift; if ($node->{client}->{$service}->{graph_vlabel}) { (my $res = $node->{client}->{$service}->{graph_vlabel}) =~ s/\$\{graph_period\}/$scale/g; return $res; } elsif ($node->{client}->{$service}->{graph_vtitle}) { return $node->{client}->{$service}->{graph_vtitle}; } return undef; } sub should_scale { my $node = shift; my $service = shift; if (defined $node->{client}->{$service}->{graph_scale}) { return &munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1); } elsif (defined $node->{client}->{$service}->{graph_noscale}) { return ! &munin_get_bool_val ($node->{client}->{$service}->{graph_noscale}, 0); } return 1; } sub get_header { my $node = shift; my $config = shift; my $domain = shift; my $host = shift; my $service = shift; my $scale = shift; my $sum = shift; my $result = []; my $tmp_field; # Picture filename push @$result, &munin_get_picture_filename ($config, $domain, $host, $service, $scale, $sum||undef); # Title push @$result, ("--title", &get_title ($node, $service, $scale)); # When to start the graph push @$result, "--start",$times{$scale}; # Custom graph args, vlabel and graph title if (defined ($tmp_field = &get_custom_graph_args ($node, $service))) { push (@$result, @{$tmp_field}); } if (defined ($tmp_field = &get_vlabel ($node, $service, munin_get ($config, "graph_period", "second", $domain, $host, $service)))) { push @$result, ("--vertical-label", $tmp_field); } push @$result,"--height", &munin_get ($config, "graph_height", "175", $domain, $host, $service); push @$result,"--width", &munin_get ($config, "graph_width", "400", $domain, $host, $service); push @$result,"--imgformat", "PNG"; push @$result,"--lazy" if ($force_lazy); push (@$result, "--units-exponent", "0") if (! &should_scale ($node, $service)); return $result; } sub get_sum_command { my $node = shift; my $service = shift; my $field = shift; if (defined $node->{client}->{$service}->{$field.".special_sum"}) { return $node->{client}->{$service}->{$field.".special_sum"}; } elsif (defined $node->{client}->{$service}->{$field.".sum"}) { return $node->{client}->{$service}->{$field.".sum"}; } return undef; } sub get_stack_command { my $node = shift; my $service = shift; my $field = shift; if (defined $node->{client}->{$service}->{$field.".special_stack"}) { return $node->{client}->{$service}->{$field.".special_stack"}; } elsif (defined $node->{client}->{$service}->{$field.".stack"}) { return $node->{client}->{$service}->{$field.".stack"}; } return undef; } sub expand_specials { my $node = shift; my $config = shift; my $domain = shift; my $host = shift; my $service = shift; my $preproc = shift; my $order = shift; my $single = shift; my $result = []; my $fieldnum = 0; for my $field (@$order) { # Search for 'specials'... if ($field =~ /^-(.+)$/) { $field = $1; unless (defined $node->{client}->{$service}->{$field.".graph"} or defined $node->{client}->{$service}->{$field.".skipdraw"}) { $node->{client}->{$service}->{$field.".graph"} = "no"; } } $fieldnum++; my $tmp_field; if (defined ($tmp_field = &get_stack_command ($node, $service, $field))) { print "DEBUG: Doing special_stack...\n" if $DEBUG; my @spc_stack = (); foreach my $pre (split (/\s+/, $tmp_field)) { (my $name = $pre) =~ s/=.+//; if (!@spc_stack) { $node->{client}->{$service}->{$name.".draw"} = $node->{client}->{$service}->{$field.".draw"}; $node->{client}->{$service}->{$field.".process"} = "no"; } else { $node->{client}->{$service}->{$name.".draw"} = "STACK"; } push (@spc_stack, $name); push (@$preproc, $pre); push @$result, "$name.label"; push @$result, "$name.draw"; push @$result, "$name.cdef"; $node->{client}->{$service}->{$name.".label"} = $name; $node->{client}->{$service}->{$name.".cdef"} = "$name,UN,0,$name,IF"; if (exists $node->{client}->{$service}->{$field.".cdef"} and !exists $node->{client}->{$service}->{$name.".onlynullcdef"}) { print "NotOnlynullcdef ($field)...\n" if $DEBUG; $node->{client}->{$service}->{$name.".cdef"} .= "," . $node->{client}->{$service}->{$field.".cdef"}; $node->{client}->{$service}->{$name.".cdef"} =~ s/\b$field\b/$name/g; } else { print "Onlynullcdef ($field)...\n" if $DEBUG; $node->{client}->{$service}->{$name.".onlynullcdef"} = 1; push @$result, "$name.onlynullcdef"; } } } elsif (defined ($tmp_field = &get_sum_command ($node, $service, $field))) { my @spc_stack = (); my $last_name = ""; print "DEBUG: Doing special_sum...\n" if $DEBUG; if (@$order == 1 or @$order == 2 && $node->{client}->{$service}->{$field.".negative"}) { $single = 1; } foreach my $pre (split (/\s+/, $tmp_field)) { (my $path = $pre) =~ s/.+=//; my $name = "z".$fieldnum."_".scalar (@spc_stack); $last_name = $name; $node->{client}->{$service}->{$name.".cdef"} = "$name,UN,0,$name,IF"; $node->{client}->{$service}->{$name.".graph"} = "no"; $node->{client}->{$service}->{$name.".label"} = $name; push @$result, "$name.cdef"; push @$result, "$name.graph"; push @$result, "$name.label"; push (@spc_stack, $name); push (@$preproc, "$name=$pre"); } $node->{client}->{$service}->{$last_name.".cdef"} .= "," . join (',+,', @spc_stack[0 .. @spc_stack-2]) . ',+'; if (exists $node->{client}->{$service}->{$field.".cdef"} and length $node->{client}->{$service}->{$field.".cdef"}) { # Oh bugger... my $tc = $node->{client}->{$service}->{$field.".cdef"}; print "Oh bugger...($field)...\n" if $DEBUG; $tc =~ s/\b$field\b/$node->{client}->{$service}->{$last_name.".cdef"}/; $node->{client}->{$service}->{$last_name.".cdef"} = $tc; } $node->{client}->{$service}->{$field.".process"} = "no"; $node->{client}->{$service}->{$last_name.".draw"} = $node->{client}->{$service}->{$field.".draw"}; $node->{client}->{$service}->{$last_name.".label"} = $node->{client}->{$service}->{$field.".label"}; if (defined $node->{client}->{$service}->{$field.".graph"}) { $node->{client}->{$service}->{$last_name.".graph"} = $node->{client}->{$service}->{$field.".graph"}; } else { $node->{client}->{$service}->{$last_name.".graph"} = "yes"; } if (defined $node->{client}->{$service}->{$field.".negative"}) { $node->{client}->{$service}->{$last_name.".negative"} = $node->{client}->{$service}->{$field.".negative"};; } $node->{client}->{$service}->{$field.".realname"} = $last_name; print "Setting node->{client}->{$service}->{$field} -> realname = $last_name...\n" if $DEBUG; } elsif (defined $node->{client}->{$service}->{$field.".negative"}) { my $nf = $node->{client}->{$service}->{$field.".negative"}; unless (defined $node->{client}->{$service}->{$nf.".graph"} or defined $node->{client}->{$service}->{$nf.".skipdraw"}) { $node->{client}->{$service}->{$nf.".graph"} = "no"; } } } return $result; } sub single_value { my $node = shift; my $config = shift; my $domain = shift; my $host = shift; my $service = shift; my $field = shift; my $order = shift; return 1 if @$order == 1; return 1 if (@$order == 2 and $node->{client}->{$service}->{$field.".negative"}); my $graphable = 0; if (!defined $node->{client}->{$service}->{"graphable"}) { # foreach my $field (keys %{$node->{client}->{$service}}) foreach my $field (&munin_get_field_order ($node, $config, $domain, $host, $service)) { print "DEBUG: single_value: Checking field \"$field\".\n" if $DEBUG; if ($field =~ /^([^\.]+)\.label/ or $field =~ /=/) { $graphable++ if &munin_draw_field ($node, $service, $1); } } $node->{client}->{$service}->{"graphable"} = $graphable; } return 1 if ($node->{client}->{$service}->{"graphable"} == 1); return 0; } sub get_field_name { my $name = shift; $name = substr (Digest::MD5::md5_hex ($name), -15) if (length $name > 15); return $name; } sub process_field { my $node = shift; my $service = shift; my $field = shift; return (&munin_get_bool_val ($node->{client}->{$service}->{$field.".process"}, 1)); } sub process_node { my ($domain,$name,$node) = @_; # See if we should skip it because of command-line arguments return if (@limit_hosts and not grep (/^$name$/, @limit_hosts)); # Make my graphs logger ("Processing $name") if $DEBUG; for my $service (keys %{$node->{client}}) { my $service_time= Time::HiRes::time; my $lastupdate = 0; my $now = time; my $fnum = 0; my @rrd; my @added = (); # See if we should skip the service next if (&skip_service ($node, $service)); my $field_count = 0; my $max_field_len = 0; my @field_order = (); my $rrdname; my $force_single_value; @field_order = @{&munin_get_field_order ($node, $config, $domain, $name, $service, \$force_single_value)}; # Array to keep 'preprocess'ed fields. my @rrd_preprocess = (); print "DEBUG: Expanding specials \"", join "\",\"", @field_order, "\".\n" if $DEBUG; @added = @{&expand_specials ($node, $config, $domain, $name, $service, \@rrd_preprocess, \@field_order)}; @field_order = (@rrd_preprocess, @field_order); print "DEBUG: Checking field lengths \"", join "\",\"", (@rrd_preprocess, @field_order), "\".\n" if $DEBUG; # Get max label length $max_field_len = &munin_get_max_label_length ($node, $config, $domain, $name, $service, \@field_order); my $global_headers = ($max_field_len > 16); # Array to keep negative data until we're finished with positive. my @rrd_negatives = (); my $filename = "unknown"; my %total_pos; my %total_neg; print "DEBUG: Treating fields \"", join "\",\"", @field_order, "\".\n" if $DEBUG; for my $field (@field_order) { my $path = undef; if ($field =~ s/=(.+)//) { $path = $1; } next unless &process_field ($node, $service, $field); print "DEBUG: Processing field \"$field\".\n" if $DEBUG; if ($field_count == 0 and munin_get ($config, "draw", "LINE2", $domain, $name, $service, $field) eq "STACK") { # Illegal -- first field is a STACK logger ("ERROR: First field (\"$field\") of graph \"$domain\" :: \"$name\" :: \"$service\" is STACK. STACK can only be drawn after a LINEx or AREA."); } # Getting name of rrd file $filename = &munin_get_rrd_filename ($node, $config, $domain, $name, $service, $field, $path); my $update = RRDs::last ($filename); $update = 0 if ! defined $update; if ($update > $lastupdate) { $lastupdate = $update; } my $rrdfield = ($node->{client}->{$service}->{$field.".rrdfield"} || "42"); my $single_value = $force_single_value || &single_value ($node, $config, $domain, $name, $service, $field, \@field_order); my $has_negative = exists $node->{client}->{$service}->{$field.".negative"}; # Trim the fieldname to make room for other field names. $rrdname = &get_field_name ($field); if ($rrdname ne $field) # A change was made { set_cdef_name ($node->{client}->{$service}, $field, $rrdname); } push (@rrd, "DEF:g$rrdname=" . $filename . ":" . $rrdfield . ":AVERAGE"); push (@rrd, "DEF:i$rrdname=" . $filename . ":" . $rrdfield . ":MIN"); push (@rrd, "DEF:a$rrdname=" . $filename . ":" . $rrdfield . ":MAX"); if (exists $node->{client}->{$service}->{$field.".onlynullcdef"} and $node->{client}->{$service}->{$field.".onlynullcdef"}) { push (@rrd, "CDEF:c$rrdname=g$rrdname" . (($now-$update)>900 ? ",POP,UNKN" : "")); } if (($node->{client}->{$service}->{$field.".type"}||"GAUGE") ne "GAUGE" and graph_by_minute ($config, $domain, $name, $service)) { push (@rrd, &expand_cdef($node->{client}->{$service}, \$rrdname, "$field,60,*")); } if ($node->{client}->{$service}->{$field.".cdef"}) { push (@rrd, &expand_cdef($node->{client}->{$service}, \$rrdname, $node->{client}->{$service}->{$field.".cdef"})); push (@rrd, "CDEF:c$rrdname=g$rrdname"); print "DEBUG: Field name after cdef set to $rrdname\n" if $DEBUG; } elsif (!(exists $node->{client}->{$service}->{$field.".onlynullcdef"} and $node->{client}->{$service}->{$field.".onlynullcdef"})) { push (@rrd, "CDEF:c$rrdname=g$rrdname" . (($now-$update)>900 ? ",POP,UNKN" : "")); } next unless &munin_draw_field ($node, $service, $field); print "DEBUG: Drawing field \"$field\".\n" if $DEBUG; if ($single_value) # Only one field. Do min/max range. { push (@rrd, "CDEF:min_max_diff=a$rrdname,i$rrdname,-"); push (@rrd, "CDEF:re_zero=min_max_diff,min_max_diff,-") unless ($node->{client}->{$service}->{$field.".negative"}); push (@rrd, "AREA:i$rrdname#ffffff"); push (@rrd, "STACK:min_max_diff$range_colour"); push (@rrd, "LINE2:re_zero#000000") unless ($node->{client}->{$service}->{$field.".negative"}); } if ($has_negative and !@rrd_negatives) # Push "global" headers... { push (@rrd, "COMMENT:" . (" " x $max_field_len)); push (@rrd, "COMMENT:Cur (-/+)"); push (@rrd, "COMMENT:Min (-/+)"); push (@rrd, "COMMENT:Avg (-/+)"); push (@rrd, "COMMENT:Max (-/+) \\j"); } elsif ($global_headers == 1) { push (@rrd, "COMMENT:" . (" " x $max_field_len)); push (@rrd, "COMMENT: Cur$RRDkludge:"); push (@rrd, "COMMENT:Min$RRDkludge:"); push (@rrd, "COMMENT:Avg$RRDkludge:"); push (@rrd, "COMMENT:Max$RRDkludge: \\j"); $global_headers++; } my $custom_colour = $node->{client}->{$service}->{$field.".colour"}; $custom_colour = "#" . $custom_colour if $custom_colour; push (@rrd, ($node->{client}->{$service}->{$field.".draw"} || "LINE2") . ":g$rrdname" . ($custom_colour || ($single_value ? $single_colour : $COLOUR[$field_count++%@COLOUR])) . ":" . (escape ($node->{client}->{$service}->{"$field.label"}) || escape ($field)) . (" " x ($max_field_len + 1 - length ($node->{client}->{$service}->{"$field.label"} || $field)))); # Check for negative fields (typically network traffic) if ($has_negative) { my $negfield = &orig_to_cdef ($node->{client}->{$service}, $node->{client}->{$service}->{$field.".negative"}); print "DEBUG: negfield = $negfield\n" if $DEBUG; if (exists $node->{client}->{$service}->{$negfield.".realname"}) { $negfield = $node->{client}->{$service}->{$negfield.".realname"}; } if (!@rrd_negatives) # zero-line, to redraw zero afterwards. { push (@rrd_negatives, "CDEF:re_zero=g$negfield,UN,0,0,IF"); } push (@rrd_negatives, "CDEF:ng$negfield=g$negfield,-1,*"); if ($single_value) # Only one field. Do min/max range. { push (@rrd, "CDEF:neg_min_max_diff=i$negfield,a$negfield,-"); push (@rrd, "CDEF:ni$negfield=i$negfield,-1,*"); push (@rrd, "AREA:ni$negfield#ffffff"); push (@rrd, "STACK:neg_min_max_diff$range_colour"); } push (@rrd_negatives, ($node->{client}->{$service}->{$negfield.".draw"} || "LINE2") . ":ng$negfield" . ((defined $single_value and $single_value) ? $single_colour : $COLOUR[($field_count-1)%@COLOUR])); # Draw HRULEs my $linedef = munin_get ($config, "line", undef, $domain, $name, $service, $node->{client}->{$service}->{$field.".negative"}); if ($linedef) { my ($number, $colour, $label) = split (/:/, $linedef, 3); push (@rrd_negatives, "HRULE:".$number. ($colour ? "#$colour" : ((defined $single_value and $single_value) ? "#ff0000" : $COLOUR[($field_count-1)%@COLOUR])) ); } elsif ($node->{client}->{$service}->{"$negfield.warn"}) { push (@rrd_negatives, "HRULE:".$node->{client}->{$service}->{$node->{client}->{$service}->{$field.".negative"}.".warn"}. ((defined $single_value and $single_value) ? "#ff0000" : $COLOUR[($field_count-1)%@COLOUR])); } push (@rrd, "GPRINT:c$negfield:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g"); push (@rrd, "GPRINT:c$rrdname:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "GPRINT:i$negfield:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g"); push (@rrd, "GPRINT:i$rrdname:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "GPRINT:g$negfield:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g"); push (@rrd, "GPRINT:g$rrdname:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "GPRINT:a$negfield:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g"); push (@rrd, "GPRINT:a$rrdname:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "\\j"); push (@{$total_pos{'min'}}, "i$rrdname"); push (@{$total_pos{'avg'}}, "g$rrdname"); push (@{$total_pos{'max'}}, "a$rrdname"); push (@{$total_neg{'min'}}, "i$negfield"); push (@{$total_neg{'avg'}}, "g$negfield"); push (@{$total_neg{'max'}}, "a$negfield"); } else { push (@rrd, "COMMENT: Cur$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:c$rrdname:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, "yes")?"%s":"") . ""); push (@rrd, "COMMENT: Min$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:i$rrdname:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "COMMENT: Avg$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:g$rrdname:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "COMMENT: Max$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:a$rrdname:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "\\j"); push (@{$total_pos{'min'}}, "i$rrdname"); push (@{$total_pos{'avg'}}, "g$rrdname"); push (@{$total_pos{'max'}}, "a$rrdname"); } # Draw HRULEs my $linedef = munin_get ($config, "line", undef, $domain, $name, $service, $field); if ($linedef) { my ($number, $colour, $label) = split (/:/, $linedef, 3); $label =~ s/:/\\:/g if defined $label; push (@rrd, "HRULE:".$number. ($colour ? "#$colour" : ((defined $single_value and $single_value) ? "#ff0000" : $COLOUR[($field_count-1)%@COLOUR])) . ((defined $label and length ($label)) ? ":$label" : ""), "COMMENT: \\j" ); } elsif ($node->{client}->{$service}->{"$field.warn"}) { push (@rrd, "HRULE:".$node->{client}->{$service}->{"$field.warn"}.($single_value ? "#ff0000" : $COLOUR[($field_count-1)%@COLOUR])); } } if (@rrd_negatives) { push (@rrd, @rrd_negatives); push (@rrd, "LINE2:re_zero#000000"); # Redraw zero. if (exists $node->{client}->{$service}->{graph_total} and exists $total_pos{'min'} and exists $total_neg{'min'} and @{$total_pos{'min'}} and @{$total_neg{'min'}}) { push (@rrd, "CDEF:ipostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'min'}}).(",+" x (@{$total_pos{'min'}}-1))); push (@rrd, "CDEF:gpostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'avg'}}).(",+" x (@{$total_pos{'avg'}}-1))); push (@rrd, "CDEF:apostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'max'}}).(",+" x (@{$total_pos{'max'}}-1))); push (@rrd, "CDEF:inegtotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_neg{'min'}}).(",+" x (@{$total_neg{'min'}}-1))); push (@rrd, "CDEF:gnegtotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_neg{'avg'}}).(",+" x (@{$total_neg{'avg'}}-1))); push (@rrd, "CDEF:anegtotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_neg{'max'}}).(",+" x (@{$total_neg{'max'}}-1))); push (@rrd, "CDEF:dpostotal=ipostotal,UN,ipostotal,UNKN,IF"); push (@rrd, "LINE1:dpostotal#000000:" . $node->{client}->{$service}->{graph_total} . (" " x ($max_field_len - length ($node->{client}->{$service}->{graph_total}) + 1))); push (@rrd, "GPRINT:gnegtotal:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g"); push (@rrd, "GPRINT:gpostotal:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "GPRINT:inegtotal:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g"); push (@rrd, "GPRINT:ipostotal:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "GPRINT:gnegtotal:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g"); push (@rrd, "GPRINT:gpostotal:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "GPRINT:anegtotal:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "/\\g"); push (@rrd, "GPRINT:apostotal:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "\\j"); } } elsif (exists $node->{client}->{$service}->{graph_total} and exists $total_pos{'min'} and @{$total_pos{'min'}}) { push (@rrd, "CDEF:ipostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'min'}}).(",+" x (@{$total_pos{'min'}}-1))); push (@rrd, "CDEF:gpostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'avg'}}).(",+" x (@{$total_pos{'avg'}}-1))); push (@rrd, "CDEF:apostotal=".join (",", map { "$_,UN,0,$_,IF" } @{$total_pos{'max'}}).(",+" x (@{$total_pos{'max'}}-1))); push (@rrd, "CDEF:dpostotal=ipostotal,UN,ipostotal,UNKN,IF"); push (@rrd, "LINE1:dpostotal#000000:" . $node->{client}->{$service}->{graph_total} . (" " x ($max_field_len - length ($node->{client}->{$service}->{graph_total}) + 1))); push (@rrd, "COMMENT: Cur$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:gpostotal:LAST:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "COMMENT: Min$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:ipostotal:MIN:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "COMMENT: Avg$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:gpostotal:AVERAGE:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . ""); push (@rrd, "COMMENT: Max$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:apostotal:MAX:%6.2lf" . (munin_get_bool_val ($node->{client}->{$service}->{graph_scale}, 1)?"%s":"") . "\\j"); } for my $time (keys %times) { next unless ($draw{$time}); my @complete = (); if ($RRDkludge) { push (@complete, '--font' ,'LEGEND:7:@@LIBDIR@@/VeraMono.ttf', '--font' ,'UNIT:7:@@LIBDIR@@/VeraMono.ttf', '--font' ,'AXIS:7:@@LIBDIR@@/VeraMono.ttf'); } logger ("Processing $name -> $time") if $DEBUG; # Do the header (title, vtitle, size, etc...) push @complete, @{&get_header ($node, $config, $domain, $name, $service, $time)}; push @complete, @rrd; push (@complete, "COMMENT:Last update$RRDkludge: " . RRDescape(scalar localtime($lastupdate)) . "\\r"); if (time - 300 < $lastupdate) { push @complete, "--end",(int($lastupdate/$resolutions{$time}))*$resolutions{$time}; } print "\n\nrrdtool \"graph\" \"", join ("\"\n\t\"",@complete), "\"\n" if $DEBUG; RRDs::graph (@complete); if (my $ERROR = RRDs::error) { logger ("Unable to graph $filename: $ERROR"); } elsif ($list_images) # Command-line option to list images created { print &munin_get_picture_filename ($config, $domain, $name, $service, $time), "\n"; } } if (&munin_get_bool_val ($node->{client}->{$service}->{"graph_sums"}, 0)) { foreach my $time (keys %sumtimes) { next unless ($draw{"sum".$time}); my @rrd_sum; push @rrd_sum, @{&get_header ($node, $config, $domain, $name, $service, $time, 1)}; if (time - 300 < $lastupdate) { push @rrd_sum, "--end",(int($lastupdate/$resolutions{$time}))*$resolutions{$time}; } push @rrd_sum, @rrd; push (@rrd_sum, "COMMENT:Last update$RRDkludge: " . RRDescape(scalar localtime($lastupdate)) . "\\r"); my $labelled = 0; my @defined = (); for (my $index = 0; $index <= $#rrd_sum; $index++) { if ($rrd_sum[$index] =~ /^(--vertical-label|-v)$/) { (my $label = $node->{client}->{$service}->{graph_vlabel}) =~ s/\$\{graph_period\}/$sumtimes{$time}[0]/g; splice (@rrd_sum, $index, 2, ("--vertical-label", $label)); $index++; $labelled++; } elsif ($rrd_sum[$index] =~ /^(LINE[123]|STACK|AREA|GPRINT):([^#:]+)([#:].+)$/) { my ($pre, $fname, $post) = ($1, $2, $3); next if $fname eq "re_zero"; if ($post =~ /^:AVERAGE/) { splice (@rrd_sum, $index, 1, $pre . ":x$fname" . $post); $index++; next; } next if grep /^x$fname$/, @defined; push @defined, "x$fname"; my @replace; if (!defined ($node->{client}->{$service}->{$fname.".type"}) or $node->{client}->{$service}->{$fname.".type"} ne "GAUGE") { if ($time eq "week") { # Every plot is half an hour. Add two plots and multiply, to get per hour if (graph_by_minute ($config, $domain, $name, $service)) { # Already multiplied by 60 push @replace, "CDEF:x$fname=PREV($fname),UN,0,PREV($fname),IF,$fname,+,5,*,6,*"; } else { push @replace, "CDEF:x$fname=PREV($fname),UN,0,PREV($fname),IF,$fname,+,300,*,6,*"; } } else { # Every plot is one day exactly. Just multiply. if (graph_by_minute ($config, $domain, $name, $service)) { # Already multiplied by 60 push @replace, "CDEF:x$fname=$fname,5,*,288,*"; } else { push @replace, "CDEF:x$fname=$fname,300,*,288,*"; } } } push @replace, $pre . ":x$fname" . $post; splice (@rrd_sum, $index, 1, @replace); $index++; } elsif ($rrd_sum[$index] =~ /^(--lower-limit|--upper-limit|-l|-u)$/) { $index++; $rrd_sum[$index] = $rrd_sum[$index] * 300 * $sumtimes{$time}->[1]; } } unless ($labelled) { my $label = $node->{client}->{$service}->{"graph_vlabel_sum_$time"} || $sumtimes{$time}->[0]; unshift @rrd_sum, "--vertical-label", $label; } print "\n\nrrdtool \"graph\" \"", join ("\"\n\t\"",@rrd_sum), "\"\n" if $DEBUG; RRDs::graph (@rrd_sum); if (my $ERROR = RRDs::error) { logger ("Unable to graph $filename: $ERROR"); } elsif ($list_images) # Command-line option to list images created { print &munin_get_picture_filename ($config, $domain, $name, $service, $time, 1), "\n"; } } } $service_time = sprintf ("%.2f",(Time::HiRes::time - $service_time)); logger ("Graphed service : $service ($service_time sec * 4)"); print STATS "GS|$domain|$name|$service|$service_time\n" unless $skip_stats; foreach (@added) { delete $node->{client}->{$service}->{$_} if exists $node->{client}->{$service}->{$_}; } @added = (); } } sub graph_by_minute { my $config = shift; my $domain = shift; my $name = shift; my $service = shift; return (munin_get ($config, "graph_period", "second", $domain, $name, $service) eq "minute"); } sub orig_to_cdef { my $service = shift; my $field = shift; if (defined $service->{$field.".cdef_name"}) { return &orig_to_cdef ($service, $service->{$field.".cdef_name"}); } return $field; } sub set_cdef_name { my $service = shift; my $field = shift; my $new = shift; $service->{$field.".cdef_name"} = $new; print "DEBUG: set_cdef_name from $field to $new.\n" if $DEBUG; } sub skip_service { my $node = shift; my $service = shift; # Check to make sure that service exists return 1 unless (ref $node->{client}->{$service}); # See if we should skip it because of conf-options return 1 if ($node->{client}->{$service}->{'graph'} and ($node->{client}->{$service}->{'graph'} eq "no" || ($node->{client}->{$service}->{'graph'} eq "on-demand") && !$force_graphing)); # See if we should skip it because of command-line arguments return 1 if (@limit_services and not grep (/^$service$/, @limit_services)); # Don't skip return 0; } sub expand_cdef { my $service = shift; my $cfield_ref = shift; my $cdef = shift; my $new_field = &get_field_name ("cdef$$cfield_ref"); my ($max, $min, $avg) = ("CDEF:a$new_field=$cdef", "CDEF:i$new_field=$cdef", "CDEF:g$new_field=$cdef"); foreach my $field (keys %$service) { next unless ($field =~ /^(.+)\.label$/); my $fieldname = $1; my $rrdname = &orig_to_cdef ($service, $fieldname); if ($cdef =~ /\b$fieldname\b/) { $max =~ s/([,=])$fieldname([,=]|$)/$1a$rrdname$2/g; $min =~ s/([,=])$fieldname([,=]|$)/$1i$rrdname$2/g; $avg =~ s/([,=])$fieldname([,=]|$)/$1g$rrdname$2/g; } } &set_cdef_name ($service, $$cfield_ref, $new_field); $$cfield_ref = $new_field; return ($max, $min, $avg); } sub logger_open { my $dirname = shift; if (!$log->opened) { unless (open ($log, ">>$dirname/munin-graph.log")) { print STDERR "Warning: Could not open log file \"$dirname/munin-graph.log\" for writing: $!"; } } } sub logger { my ($comment) = @_; my $now = strftime "%b %d %H:%M:%S", localtime; print "$now - $comment\n" if $stdout; if ($log->opened) { print $log "$now - $comment\n"; } else { if (defined $config->{logdir}) { if (open ($log, ">>$config->{logdir}/munin-graph.log")) { print $log "$now - $comment\n"; } else { print STDERR "Warning: Could not open log file \"$config->{logdir}/munin-graph.log\" for writing: $!"; print STDERR "$now - $comment\n"; } } else { print STDERR "$now - $comment\n"; } } } sub parse_path { my ($path, $domain, $node, $service, $field) = @_; my $filename = "unknown"; if ($path =~ /^\s*([^:]*):([^:]*):([^:]*):([^:]*)\s*$/) { $filename = munin_get_filename ($config, $1, $2, $3, $4); } elsif ($path =~ /^\s*([^:]*):([^:]*):([^:]*)\s*$/) { $filename = munin_get_filename ($config, $domain, $1, $2, $3); } elsif ($path =~ /^\s*([^:]*):([^:]*)\s*$/) { $filename = munin_get_filename ($config, $domain, $node, $1, $2); } elsif ($path =~ /^\s*([^:]*)\s*$/) { $filename = munin_get_filename ($config, $domain, $node, $service, $1); } return $filename; } sub escape { my $text = shift; return undef if not defined $text; $text =~ s/\\/\\\\/g; $text =~ s/:/\\:/g; return $text; } sub RRDescape { my $text = shift; return $RRDs::VERSION < 1.2 ? $text : escape($text); } 1; =head1 NAME munin-graph - A program to create graphs from data contained in rrd-files. =head1 SYNOPSIS munin-graph [--options] =head1 OPTIONS =over 5 =item B<< --[no]force >> If set, force drawing of graphs that are not usually drawn due to options in the config file. [--noforce] =item B<< --[no]lazy >> If set, only redraw graphs when it would look different from the existing one. [--lazy] =item B<< --help >> View help. =item B<< --[no]force-root >> Force running as root (stupid and unnecessary). [--noforce-root] =item B<< --[no]debug >> If set, view debug messages. [--nodebug] =item B<< --service >> Limit graphed services to EserviceE. Multiple --service options may be supplied. [unset] =item B<< --host >> Limit graphed hosts to EhostE. Multiple --host options may be supplied. [unset] =item B<< --config >> Use EfileE as configuration file. [@@CONFDIR@@/munin.conf] =item B<< --[no]list-images >> If set, list the filenames of the images created. [--nolist-images] =item B<< --[no]day >> If set, create day-based graphs. [--day] =item B<< --[no]week >> If set, create week-based graphs. [--week] =item B<< --[no]month >> If set, create month-based graphs. [--month] =item B<< --[no]year >> If set, create year-based graphs. [--year] =back =head1 DESCRIPTION Munin-graph is a part of the package Munin, which is used in combination with Munin's node. Munin is a group of programs to gather data from Munin's nodes, graph them, create html-pages, and optionally warn Nagios about any off-limit values. munin-graph does the graphing. It is usually only used from within munin-cron. It checks the rrd-files for updated values, and redraws the graphs if needed. To force redrawing of graphs (after setup-changes et alia), use '--nolazy'. =head1 FILES @@CONFDIR@@/munin.conf @@DBDIR@@/* @@LOGDIR@@/munin-graph @@STATEDIR@@/* =head1 VERSION This is munin-graph version 0.9.2-3 =head1 AUTHORS Audun Ytterdal and Jimmy Olsen. =head1 BUGS munin-graph does, as of now, not check the syntax of the configuration file. Please report other bugs in the bug tracker at L. =head1 COPYRIGHT Copyright © 2002-2004 Audun Ytterdal, Jimmy Olsen, and Tore Anderson / Linpro AS. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. This program is released under the GNU General Public License =cut # vim: syntax=perl ts=8