#!/usr/bin/perl -w # -*- cperl -*- # # Copyright (C) 2002-2008 Jimmy Olsen, Audun Ytterdal, Nicolai Langfeldt # # 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 1525 2008-03-03 21:34:42Z jo $ $|=1; use strict; use IO::Socket; use RRDs; use Munin; use POSIX qw(strftime); use Digest::MD5; use Getopt::Long; use Time::HiRes; if ($RRDs::VERSION >= 1.3) { use Encode; } my $graph_time= Time::HiRes::time; my $DEBUG = 0; my $VERSION = '1.3.4'; # 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 ? '' : '\\'; # And RRDtool 1.2 and later has draws lines with crayons so we hack # the LINE* options a bit. my $LINEkluge=0; if ($RRDs::VERSION >= 1.2) { $LINEkluge=1; } # 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 = "/etc/munin/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. [/etc/munin/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 <<"EOT"; munin-graph version $VERSION. Written by Audun Ytterdal, Jimmy Olsen, Tore Anderson / Linpro AS Copyright (C) 2002-2005 This is free software released under the GNU General Public License. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, please refer to the file COPYING that is included with this software or refer to http://www.fsf.org/licensing/licenses/gpl.txt EOT exit 0; } if ($> == 0 and !$force_root) { print "You are running this program as root, which is dumb and will cause many problems later. Please run it as user 'munin'. If you really want to run it as root, use the --force-root option. 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 %PALETTE; # Hash of available palettes my @COLOUR; # Array of actuall colours to use { no warnings; $PALETTE{'old'} = [ # This is the old munin palette. It lacks contrast. qw(#22ff22 #0022ff #ff0000 #00aaaa #ff00ff #ffa500 #cc0000 #0000cc #0080C0 #8080C0 #FF0080 #800080 #688e23 #408080 #808000 #000000 #00FF00 #0080FF #FF8000 #800000 #FB31FB )]; $PALETTE{'default'} = [ # New default palette.Better contrast,more colours #Greens Blues Oranges Dk yel Dk blu Purple lime Reds Gray qw(#00CC00 #0066B3 #FF8000 #FFCC00 #330099 #990099 #CCFF00 #FF0000 #808080 #008F00 #00487D #B35A00 #B38F00 #6B006B #8FB300 #B30000 #BEBEBE #80FF80 #80C9FF #FFC080 #FFE680 #AA80FF #EE00CC #FF8080 #666600 #FFBFFF #00FFCC #CC6699 #999900 )]; # Line variations: Pure, earthy, dark pastel, misc colours } my $palette = &munin_get ($config, "palette", "default"); if (defined($PALETTE{$palette})) { @COLOUR=@{$PALETTE{$palette}}; } else { die "Unknown palette named by 'palette' keyword: $palette\n"; } 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] ); my $watermark = "Munin $VERSION"; # Make array of what is probably needed to graph my $work_array = []; if (@limit_hosts) { # Limit what to update if needed foreach my $nodename (@limit_hosts) { push @$work_array, map { @{munin_find_field ($_->{$nodename}, "graph_title")} } @{munin_find_field($config, $nodename)}; } } else { # ...else just search for all adresses to update push @$work_array, @{munin_find_field($config, "graph_title")}; } for my $service (@$work_array) { process_service ($service); } $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; munin_removelock("$config->{rundir}/munin-graph.lock") unless $skip_locking; # ### The End sub get_title { my $service = shift; my $scale = shift; return (munin_get ($service, "graph_title", $service) . " - by $scale"); } sub get_custom_graph_args { my $service = shift; my $result = []; my $args = munin_get ($service, "graph_args"); if (defined $args) { push @$result, split /\s/,$args; return $result; } else { return undef; } } sub get_vlabel { my $service = shift; my $scale = munin_get ($service, "graph_period", "second"); my $res = munin_get ($service, "graph_vlabel", munin_get ($service, "graph_vtitle")); if (defined $res) { $res =~ s/\$\{graph_period\}/$scale/g; } return $res; } sub should_scale { my $service = shift; my $ret; if (!defined ($ret = munin_get_bool ($service, "graph_scale"))) { $ret = !munin_get_bool ($service, "graph_noscale", 0); } return $ret; } sub get_header { my $service = shift; my $scale = shift; my $sum = shift; my $result = []; my $tmp_field; # Picture filename push @$result, munin_get_picture_filename ($service, $scale, $sum||undef); # Title push @$result, ("--title", get_title ($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 ($service))) { push (@$result, @{$tmp_field}); } if (defined ($tmp_field = get_vlabel ($service))) { push @$result, ("--vertical-label", $tmp_field); } push @$result,"--height", munin_get ($service, "graph_height", "175"); push @$result,"--width", munin_get ($service, "graph_width", "400"); push @$result,"--imgformat", "PNG"; push @$result,"--lazy" if ($force_lazy); push (@$result, "--units-exponent", "0") if (! should_scale ($service)); return $result; } sub get_sum_command { my $field = shift; if (defined $field->{"special_sum"}) { # Deprecated return $field->{"special_sum"}; } return munin_get ($field, "sum"); } sub get_stack_command { my $field = shift; if (defined $field->{"special_stack"}) { # Deprecated return $field->{"special_stack"}; } return munin_get ($field, "stack"); } sub expand_specials { my $service = shift; my $preproc = shift; my $order = shift; my $single = shift; my $result = []; my $fieldnum = 0; for my $field (@$order) { # Search for 'specials'... my $tmp_field; if ($field =~ /^-(.+)$/) { # Invisible field $field = $1; munin_set_var_loc ($service, [$field, "graph"], "no"); } $fieldnum++; if ($field =~ /^([^=]+)=(.+)$/) { # Aliased in graph_order my $fname = $1; my $spath = $2; my $src = munin_get_node_partialpath ($service, $spath); my $sname = munin_get_node_name ($src); next unless defined $src; logger ("Debug: Copying settings from $sname to $fname.") if $DEBUG; foreach my $foption ("draw", "type", "rrdfile", "fieldname", "info") { if (!defined $service->{$fname}->{$foption}) { if (defined $src->{$foption}) { munin_set_var_loc ($service, [$fname, $foption], $src->{$foption}); } } } # cdef is special... if (!defined $service->{$fname}->{"cdef"}) { if (defined $src->{"cdef"}) { (my $tmpcdef = $src->{"cdef"}) =~ s/([,=])$sname([,=]|$)/$1$fname$2/g; munin_set_var_loc ($service, [$fname, "cdef"], $tmpcdef); } } if (!defined $service->{$fname}->{"label"}) { munin_set_var_loc ($service, [$fname, "label"], $fname); } munin_set_var_loc ($service, [$fname, "filename"], munin_get_rrd_filename ($src)); } elsif (defined ($tmp_field = get_stack_command ($service->{$field}))) { logger ("DEBUG: expand_specials ($tmp_field): Doing stack...") if $DEBUG; my @spc_stack = (); foreach my $pre (split (/\s+/, $tmp_field)) { (my $name = $pre) =~ s/=.+//; if (!@spc_stack) { munin_set_var_loc ($service, [$name, "draw"], munin_get ($service->{$field}, "draw", "LINE2")); munin_set_var_loc ($service, [$field, "process"], "no"); } else { munin_set_var_loc ($service, [$name, "draw"], "STACK"); } push (@spc_stack, $name); push (@$preproc, $pre); push @$result, "$name.label"; push @$result, "$name.draw"; push @$result, "$name.cdef"; munin_set_var_loc ($service, [$name, "label"], $name); munin_set_var_loc ($service, [$name, "cdef"], "$name,UN,0,$name,IF"); if (munin_get ($service->{$field}, "cdef") and !munin_get_bool ($service->{$name}, "onlynullcdef", 0)) { logger ("NotOnlynullcdef ($field)...") if $DEBUG; $service->{$name}->{"cdef"} .= "," . $service->{$field}->{"cdef"}; $service->{$name}->{"cdef"} =~ s/\b$field\b/$name/g; } else { logger ("Onlynullcdef ($field)...") if $DEBUG; munin_set_var_loc ($service, [$name, "onlynullcdef"], 1); push @$result, "$name.onlynullcdef"; } } } elsif (defined ($tmp_field = get_sum_command ($service->{$field}))) { my @spc_stack = (); my $last_name = ""; logger ("DEBUG: expand_specials ($tmp_field): Doing sum...") if $DEBUG; if (@$order == 1 or (@$order == 2 and munin_get {$field, "negative", 0})) { $single = 1; } foreach my $pre (split (/\s+/, $tmp_field)) { (my $path = $pre) =~ s/.+=//; my $name = "z".$fieldnum."_".scalar (@spc_stack); $last_name = $name; munin_set_var_loc ($service, [$name, "cdef"], "$name,UN,0,$name,IF"); munin_set_var_loc ($service, [$name, "graph"], "no"); munin_set_var_loc ($service, [$name, "label"], $name); push @$result, "$name.cdef"; push @$result, "$name.graph"; push @$result, "$name.label"; push (@spc_stack, $name); push (@$preproc, "$name=$pre"); } $service->{$last_name}->{"cdef"} .= "," . join (',+,', @spc_stack[0 .. @spc_stack-2]) . ',+'; if (my $tc = munin_get ($service->{$field}, "cdef", 0)) { # Oh bugger... print "Oh bugger...($field)...\n" if $DEBUG; $tc =~ s/\b$field\b/$service->{$last_name}->{"cdef"}/; $service->{$last_name}->{"cdef"} = $tc; } munin_set_var_loc ($service, [$field, "process"], "no"); munin_set_var_loc ($service, [$last_name, "draw"], munin_get ($service->{$field}, "draw")); munin_set_var_loc ($service, [$last_name, "label"], munin_get ($service->{$field}, "label")); munin_set_var_loc ($service, [$last_name, "graph"], munin_get ($service->{$field}, "graph", "yes")); if (my $tmp = munin_get($service->{$field}, "negative")) { munin_set_var_loc ($service, [$last_name, "negative"], $tmp); } munin_set_var_loc ($service, [$field, "realname"], $last_name); } elsif (my $nf = munin_get ($service->{$field}, "negative", 0)) { if (!munin_get_bool ($service->{$nf}, "graph", 1) or munin_get_bool ($service->{$nf}, "skipdraw", 0)) { munin_set_var_loc ($service, [$nf, "graph"], "no"); } } } return $result; } sub single_value { my $service = shift; my $graphable = munin_get ($service, "graphable", 0);; if (!$graphable) { foreach my $field (@{munin_get_field_order ($service)}) { logger ("DEBUG: single_value: Checking field \"$field\"."); $graphable++ if munin_draw_field ($service->{$field}); } munin_set_var_loc ($service, ["graphable"], $graphable); } logger ("Debug: service ". join (' :: ', @{munin_get_node_loc ($service)}) ." has $graphable elements."); return ($graphable == 1); } 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 $field = shift; return munin_get_bool ($field, "process", 1); } sub process_service { my ($service) = @_; # Make my graphs my $sname = munin_get_node_name ($service); 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 return if (skip_service ($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($service)}; # Array to keep 'preprocess'ed fields. my @rrd_preprocess = (); logger ("Debug: Expanding specials for $sname: \"" . join("\",\"", @field_order) . "\".") if $DEBUG; @added = @{&expand_specials ($service, \@rrd_preprocess, \@field_order, \$force_single_value)}; @field_order = (@rrd_preprocess, @field_order); logger ("DEBUG: Checking field lengths for $sname: \"" . join("\",\"", @rrd_preprocess) . "\".") if $DEBUG; # Get max label length $max_field_len = munin_get_max_label_length ($service, \@field_order); # Global headers makes the value tables easier to read no matter how # wide the labels are. my $global_headers = 1; # Default format for printing under graph. my $avgformat; my $rrdformat=$avgformat="%6.2lf"; if (munin_get ($service, "graph_args", "") =~ /--base\s+1024/) { # If the base unit is 1024 then 1012.56 is a valid # number to show. That's 7 positions, not 6. $rrdformat=$avgformat="%7.2lf"; } # Plugin specified complete printf format $rrdformat = munin_get ($service, "graph_printf", $rrdformat); my $rrdscale = ''; if (munin_get_bool ($service, "graph_scale", 1)) { $rrdscale = '%s'; } # Array to keep negative data until we're finished with positive. my @rrd_negatives = (); my $filename = "unknown"; my %total_pos; my %total_neg; my $autostacking=0; logger ("DEBUG: Treating fields \"" . join ("\",\"", @field_order) . "\".") if $DEBUG; for my $fname (@field_order) { my $path = undef; my $field = undef; if ($fname =~ s/=(.+)//) { $path = $1; } $field = munin_get_node ($service, [$fname]); next if (!defined $field or !$field or !process_field ($field)); logger ("DEBUG: Processing field \"$fname\" [".munin_get_node_name($field)."].") if $DEBUG; my $fielddraw = munin_get ($field, "draw", "LINE2"); if ($field_count == 0 and $fielddraw eq 'STACK') { # Illegal -- first field is a STACK logger ("ERROR: First field (\"$fname\") of graph " . join (' :: ', munin_get_node_loc ($service)) . " is STACK. STACK can only be drawn after a LINEx or AREA."); $fielddraw = "LINE2"; } if ($fielddraw eq 'AREASTACK') { if ($autostacking==0) { $fielddraw='AREA'; $autostacking=1; } else { $fielddraw='STACK'; } } if ($fielddraw =~ /LINESTACK(\d+(?:.\d+)?)/ ) { if ($autostacking==0) { $fielddraw="LINE$1"; $autostacking=1; } else { $fielddraw='STACK'; } } # Getting name of rrd file $filename = munin_get_rrd_filename ($field, $path); my $update = RRDs::last ($filename); $update = 0 if ! defined $update; if ($update > $lastupdate) { $lastupdate = $update; } # It does not look like $fieldname.rrdfield is possible to set my $rrdfield = munin_get ($field, "rrdfield", "42"); my $single_value = $force_single_value || single_value ($service); my $has_negative = munin_get ($field, "negative"); # Trim the fieldname to make room for other field names. $rrdname = &get_field_name ($fname); if ($rrdname ne $fname) { # A change was made munin_set ($field, "cdef_name", $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 (munin_get_bool ($field, "onlynullcdef", 0)) { push (@rrd, "CDEF:c$rrdname=g$rrdname" . (($now-$update)>900 ? ",POP,UNKN" : "")); } if (munin_get ($field, "type", "GAUGE") ne "GAUGE" and graph_by_minute ($service)) { push (@rrd, expand_cdef($service, \$rrdname, "$fname,60,*")); } if (my $tmpcdef = munin_get ($field, "cdef")) { push (@rrd,expand_cdef($service, \$rrdname, $tmpcdef)); push (@rrd, "CDEF:c$rrdname=g$rrdname"); logger ("DEBUG: Field name after cdef set to $rrdname") if $DEBUG; } elsif (!munin_get_bool ($field, "onlynullcdef", 0)) { push (@rrd, "CDEF:c$rrdname=g$rrdname" . (($now-$update)>900 ? ",POP,UNKN" : "")); } next if !munin_draw_field ($field); logger ("DEBUG: Drawing field \"$fname\".") 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,-") if !munin_get ($field, "negative"); push (@rrd, "AREA:i$rrdname#ffffff"); push (@rrd, "STACK:min_max_diff$range_colour"); push (@rrd, "LINE2:re_zero#000000") if !munin_get ($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 $colour; if (my $tmpcol = munin_get ($field, "colour")) { $colour = "#" . $tmpcol; } elsif ($single_value) { $colour = $single_colour; } else { $colour = $COLOUR[$field_count%@COLOUR]; } $field_count++; my $tmplabel = munin_get ($field, "label", $fname); push (@rrd, $fielddraw . ":g$rrdname" . $colour . ":" . escape ($tmplabel) . (" " x ($max_field_len + 1 - length $tmplabel))); # Check for negative fields (typically network (or disk) traffic) if ($has_negative) { my $negfieldname = orig_to_cdef ($service, munin_get ($field, "negative")); my $negfield = $service->{$negfieldname}; if (my $tmpneg = munin_get ($negfield, "realname")) { $negfieldname = $tmpneg; $negfield = $service->{$negfieldname}; } if (!@rrd_negatives) { # zero-line, to redraw zero afterwards. push (@rrd_negatives, "CDEF:re_zero=g$negfieldname,UN,0,0,IF"); } push (@rrd_negatives, "CDEF:ng$negfieldname=g$negfieldname,-1,*"); if ($single_value) { # Only one field. Do min/max range. push (@rrd, "CDEF:neg_min_max_diff=i$negfieldname,a$negfieldname,-"); push (@rrd, "CDEF:ni$negfieldname=i$negfieldname,-1,*"); push (@rrd, "AREA:ni$negfieldname#ffffff"); push (@rrd, "STACK:neg_min_max_diff$range_colour"); } push (@rrd_negatives, $fielddraw . ":ng$negfieldname" . $colour ); # Draw HRULEs my $linedef = munin_get ($negfield, "line"); if ($linedef) { my ($number, $ldcolour, $label) = split (/:/, $linedef, 3); push (@rrd_negatives, "HRULE:".$number. ($ldcolour ? "#$ldcolour" : $colour)); } elsif (my $tmpwarn = munin_get ($negfield, "warn")) { push (@rrd_negatives, "HRULE:".$negfieldname. (defined $single_value and $single_value) ? "#ff0000" : $colour); } push (@rrd, "GPRINT:c$negfieldname:LAST:$rrdformat" . $rrdscale . "/\\g"); push (@rrd, "GPRINT:c$rrdname:LAST:$rrdformat" . $rrdscale . ""); push (@rrd, "GPRINT:i$negfieldname:MIN:$rrdformat" . $rrdscale . "/\\g"); push (@rrd, "GPRINT:i$rrdname:MIN:$rrdformat" . $rrdscale . ""); push (@rrd, "GPRINT:g$negfieldname:AVERAGE:$avgformat" . $rrdscale . "/\\g"); push (@rrd, "GPRINT:g$rrdname:AVERAGE:$avgformat" . $rrdscale . ""); push (@rrd, "GPRINT:a$negfieldname:MAX:$rrdformat" . $rrdscale . "/\\g"); push (@rrd, "GPRINT:a$rrdname:MAX:$rrdformat" . $rrdscale . "\\j"); push (@{$total_pos{'min'}}, "i$rrdname"); push (@{$total_pos{'avg'}}, "g$rrdname"); push (@{$total_pos{'max'}}, "a$rrdname"); push (@{$total_neg{'min'}}, "i$negfieldname"); push (@{$total_neg{'avg'}}, "g$negfieldname"); push (@{$total_neg{'max'}}, "a$negfieldname"); } else { push (@rrd, "COMMENT: Cur$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:c$rrdname:LAST:$rrdformat" . $rrdscale . ""); push (@rrd, "COMMENT: Min$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:i$rrdname:MIN:$rrdformat" . $rrdscale . ""); push (@rrd, "COMMENT: Avg$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:g$rrdname:AVERAGE:$avgformat" . $rrdscale . ""); push (@rrd, "COMMENT: Max$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:a$rrdname:MAX:$rrdformat" . $rrdscale . "\\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 ($field, "line"); if ($linedef) { my ($number, $ldcolour, $label) = split (/:/, $linedef, 3); $label =~ s/:/\\:/g if defined $label; push (@rrd, "HRULE:".$number. ($ldcolour ? "#$ldcolour" : ((defined $single_value and $single_value) ? "#ff0000" : $colour)). ((defined $label and length ($label)) ? ":$label" : ""), "COMMENT: \\j" ); } elsif (my $tmpwarn = munin_get ($field, "warn")) { push (@rrd,"HRULE:".$tmpwarn.($single_value ? "#ff0000" : $colour)); } } my $graphtotal = munin_get ($service, "graph_total"); if (@rrd_negatives) { push (@rrd, @rrd_negatives); push (@rrd, "LINE2:re_zero#000000"); # Redraw zero. if (defined $graphtotal 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:$graphtotal" . (" " x ($max_field_len - length ($graphtotal) + 1))); push (@rrd, "GPRINT:gnegtotal:LAST:$rrdformat" . $rrdscale . "/\\g"); push (@rrd, "GPRINT:gpostotal:LAST:$rrdformat" . $rrdscale . ""); push (@rrd, "GPRINT:inegtotal:MIN:$rrdformat" . $rrdscale . "/\\g"); push (@rrd, "GPRINT:ipostotal:MIN:$rrdformat" . $rrdscale . ""); push (@rrd, "GPRINT:gnegtotal:AVERAGE:$avgformat" . $rrdscale . "/\\g"); push (@rrd, "GPRINT:gpostotal:AVERAGE:$avgformat" . $rrdscale . ""); push (@rrd, "GPRINT:anegtotal:MAX:$rrdformat" . $rrdscale . "/\\g"); push (@rrd, "GPRINT:apostotal:MAX:$rrdformat" . $rrdscale . "\\j"); } } elsif (defined $graphtotal 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:$graphtotal" . (" " x ($max_field_len - length ($graphtotal) + 1))); push (@rrd, "COMMENT: Cur$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:gpostotal:LAST:$rrdformat" . $rrdscale . ""); push (@rrd, "COMMENT: Min$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:ipostotal:MIN:$rrdformat" . $rrdscale . ""); push (@rrd, "COMMENT: Avg$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:gpostotal:AVERAGE:$avgformat" . $rrdscale .""); push (@rrd, "COMMENT: Max$RRDkludge:") unless $global_headers; push (@rrd, "GPRINT:apostotal:MAX:$rrdformat" . $rrdscale . "\\j"); } for my $time (keys %times) { next unless ($draw{$time}); my $picfilename = munin_get_picture_filename ($service, $time); (my $picdirname = $picfilename) =~ s/\/[^\/]+$//; my @complete = (); if ($RRDkludge) { #push (@complete, #'--font' ,'LEGEND:7:/usr/libexec/munin/VeraMono.ttf', # since rrdtool 1.3 with libpango the LEGEND column alignment # only works with monospace fonts if ( $RRDs::VERSION >= 1.3 ) { push (@complete, '--font' ,'LEGEND:7:/usr/libexec/munin/VeraMono.ttf'); } else { push (@complete, '--font' ,'LEGEND:7:@@LIBDIR@@/VeraMono.ttf'); } push (@complete, '--font' ,'UNIT:7:/usr/libexec/munin/VeraMono.ttf', '--font' ,'AXIS:7:/usr/libexec/munin/VeraMono.ttf'); } push(@complete,'-W', $watermark) if $RRDs::VERSION >= 1.2; # Do the header (title, vtitle, size, etc...) push @complete, @{get_header ($service, $time)}; if ($LINEkluge) { @rrd = map { s/LINE3:/LINE2.2:/; $_; } @rrd; @rrd = map { s/LINE2:/LINE1.6:/; $_; } @rrd; # LINE1 is thin enough. } 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; # Since version 1.3 rrdtool uses libpango which needs its input # as utf8 string. So we assume that every input is in latin1 # and decode it to perl's internal representation and then to utf8. if ( $RRDs::VERSION >= 1.3 ) { @complete = map { $_ = encode("utf8", (decode("latin1", $_))); } @complete; } # Make sure directory exists munin_mkdir_p ($picdirname, 0777); RRDs::graph (@complete); if (my $ERROR = RRDs::error) { logger ("Unable to graph ". munin_get_picture_filename ($service, $time) . ": $ERROR"); } elsif ($list_images) { # Command-line option to list images created print munin_get_picture_filename ($service, $time),"\n"; } } if (munin_get_bool ($service, "graph_sums", 0)) { foreach my $time (keys %sumtimes) { my $picfilename = munin_get_picture_filename ($service, $time, 1); (my $picdirname = $picfilename) =~ s/\/[^\/]+$//; next unless ($draw{"sum".$time}); my @rrd_sum; push @rrd_sum, @{get_header ($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 = munin_get ($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 (munin_get ($service->{$fname}, "type", "GAUGE") 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 ($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 ($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 = munin_get ($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; # Make sure directory exists munin_mkdir_p ($picdirname, 0777); RRDs::graph (@rrd_sum); if (my $ERROR = RRDs::error) { logger ("Unable to graph ". munin_get_picture_filename ($service, $time) . ": $ERROR"); } elsif ($list_images) { # Command-line option to list images created print munin_get_picture_filename ($service, $time, 1),"\n"; } } } $service_time = sprintf ("%.2f",(Time::HiRes::time - $service_time)); logger ("Graphed service : $sname ($service_time sec * 4)"); print STATS "GS|$service_time\n" unless $skip_stats; foreach (@added) { delete $service->{$_} if exists $service->{$_}; } @added = (); } sub graph_by_minute { my $service = shift; return (munin_get ($service, "graph_period", "second") eq "minute"); } sub orig_to_cdef { my $service = shift; my $fieldname = shift; return undef unless ref ($service) eq "HASH"; if (defined $service->{$fieldname}->{"cdef_name"}) { return orig_to_cdef ($service, $service->{$fieldname}->{"cdef_name"}); } return $fieldname; } sub skip_service { my $service = shift; my $sname = munin_get_node_name ($service); # Skip if we've limited services with cli options return 1 if (@limit_services and !grep /^$sname$/, @limit_services); # Always graph if --force is present return 0 if $force_graphing; # See if we should skip it because of conf-options return 1 if (munin_get ($service, "graph", "yes") eq "on-demand" or !munin_get_bool ($service, "graph", 1)); # 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 (@{munin_find_field ($service, "label")}) { my $fieldname = munin_get_node_name ($field); 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; } } munin_set_var_loc ($service, [$$cfield_ref, "cdef_name"], $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: $!"; } } else { close (STDERR); *STDERR = \$log; } } 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"; $log->flush; close (STDERR); open (STDERR, ">&", $log); } 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. [/etc/munin/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. If munin.conf sets "graph_strategy cgi" then munin-graph does no work, instead munin-html generates references to the graphing CGI. Please see http://munin.projects.linpro.no/wiki/CgiHowto for more information about CGI grpahing. 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 /etc/munin/munin.conf /var/lib/munin/* /var/log/munin/munin-graph /var/run/munin/* =head1 AUTHORS Audun Ytterdal, Jimmy Olsen, Tore Anderson, Nicolai Langfeldt. =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 (C) 2002-2006 Audun Ytterdal, Jimmy Olsen, Tore Anderson, and Nicolai Langfeldt 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 =head1 SEE ALSO For information on configuration options, please refer to the man page for F. =cut # vim: syntax=perl ts=8