# -*- perl -*- package Smokeping::Graphs; use strict; use Smokeping; =head1 NAME Smokeping::Graphs - Functions used in Smokeping for creating graphs =head1 OVERVIEW This module currently only contains the code for generating the 'multi target' graphs. Code for the other graphs will be moved here too in time. =head2 IMPLEMENTATION =head3 get_multi_detail A version of get_detail for multi host graphs where there is data from multiple targets shown in one graph. The look of the graph is modeld after the graphs shown in the overview page, except for the size. =cut sub get_multi_detail ($$$$;$){ # a) 's' classic with several static graphs on the page # b) 'n' navigator mode with one graph. below the graph one can specify the end time # and the length of the graph. # c) 'a' ajax mode, generate image based on given url and dump in on stdout # my $cfg = shift; my $q = shift; my $tree = shift; my $open = shift; my $mode = shift || $q->param('displaymode') || 's'; my $phys_open = $open; if ($tree->{__tree_link}){ $tree=$tree->{__tree_link}; $phys_open = $tree->{__real_path}; } my @dirs = @{$phys_open}; return "
ERROR: ".(join ".", @dirs)." has no probe defined
" unless $tree->{probe}; return "
ERROR: ".(join ".", @dirs)." $tree->{probe} is not known
" unless $cfg->{__probes}{$tree->{probe}}; return "
ERROR: ".(join ".", @dirs)." ist no multi host
" unless $tree->{host} =~ m|^/|; return "
ERROR: unknown displaymode $mode
" unless $mode =~ /^[snca]$/; my $dir = ""; for (@dirs) { $dir .= "/$_"; mkdir $cfg->{General}{imgcache}.$dir, 0755 unless -d $cfg->{General}{imgcache}.$dir; die "ERROR: creating $cfg->{General}{imgcache}$dir: $!\n" unless -d $cfg->{General}{imgcache}.$dir; } my $page; my $file = pop @dirs; my @hosts = split /\s+/, $tree->{host}; my $ProbeDesc; my $ProbeUnit; my $imgbase; my $imghref; my @tasks; my %lastheight; my $max = {}; if ($mode eq 's'){ # in nav mode there is only one graph, so the height calculation # is not necessary. $imgbase = $cfg->{General}{imgcache}."/".(join "/", @dirs)."/${file}"; $imghref = $cfg->{General}{imgurl}."/".(join "/", @dirs)."/${file}"; @tasks = @{$cfg->{Presentation}{detail}{_table}}; if (open (HG,"<${imgbase}.maxheight")){ while (){ chomp; my @l = split / /; $lastheight{$l[0]} = $l[1]; } close HG; } for my $rrd (@hosts){ my $newmax = Smokeping::findmax($cfg, $cfg->{General}{datadir}.$rrd.".rrd"); map {$max->{$_} = $newmax->{$_} if not $max->{$_} or $newmax->{$_} > $max->{$_} } keys %{$newmax}; } if (open (HG,">${imgbase}.maxheight")){ foreach my $size (keys %{$max}){ print HG "$size $max->{$size}\n"; } close HG; } } elsif ($mode eq 'n' or $mode eq 'a') { if ($mode eq 'n') { $imgbase =$cfg->{General}{imgcache}."/__navcache/".time()."$$"; $imghref =$cfg->{General}{imgurl}."/__navcache/".time()."$$"; } else { my $serial = int(rand(2000)); $imgbase =$cfg->{General}{imgcache}."/__navcache/".$serial; $imghref =$cfg->{General}{imgurl}."/__navcache/".$serial; } mkdir $cfg->{General}{imgcache}."/__navcache",0755 unless -d $cfg->{General}{imgcache}."/__navcache"; # remove old images after one hour my $pattern = $cfg->{General}{imgcache}."/__navcache/*.png"; for (glob $pattern){ unlink $_ if time - (stat $_)[9] > 3600; } @tasks = (["Navigator Graph", Smokeping::parse_datetime($q->param('start')),Smokeping::parse_datetime($q->param('end'))]); } else { # chart mode mkdir $cfg->{General}{imgcache}."/__chartscache",0755 unless -d $cfg->{General}{imgcache}."/__chartscache"; # remove old images after one hour my $pattern = $cfg->{General}{imgcache}."/__chartscache/*.png"; for (glob $pattern){ unlink $_ if time - (stat $_)[9] > 3600; } my $desc = join "/",@{$open}; @tasks = ([$desc , time()-3600, time()]); $imgbase = $cfg->{General}{imgcache}."/__chartscache/".(join ".", @dirs).".${file}"; $imghref = $cfg->{General}{imgurl}."/__chartscache/".(join ".", @dirs).".${file}"; } if ($mode =~ /[anc]/){ my $val = 0; for my $host (@hosts){ my ($graphret,$xs,$ys) = RRDs::graph ("dummy", '--start', $tasks[0][1], '--end', $tasks[0][2], "DEF:maxping=$cfg->{General}{datadir}${host}.rrd:median:AVERAGE", 'PRINT:maxping:MAX:%le' ); my $ERROR = RRDs::error(); return "
RRDtool did not understand your input: $ERROR.
" if $ERROR; $val = $graphret->[0] if $val < $graphret->[0]; } $val = 1e-6 if $val =~ /nan/i; $max = { $tasks[0][1] => $val * 1.5 }; } for (@tasks) { my ($desc,$start,$end) = @{$_}; my $xs; my $ys; my $sigtime = ($end and $end =~ /^\d+$/) ? $end : time; my $date = $cfg->{Presentation}{detail}{strftime} ? POSIX::strftime($cfg->{Presentation}{detail}{strftime}, localtime($sigtime)) : scalar localtime($sigtime); if ( $RRDs::VERSION >= 1.199908 ){ $date =~ s|:|\\:|g; } $end ||= 'last'; $start = Smokeping::exp2seconds($start) if $mode =~ /[s]/; my $startstr = $start =~ /^\d+$/ ? POSIX::strftime("%Y-%m-%d %H:%M",localtime($mode eq 'n' ? $start : time-$start)) : $start; my $endstr = $end =~ /^\d+$/ ? POSIX::strftime("%Y-%m-%d %H:%M",localtime($mode eq 'n' ? $end : time)) : $end; my $realstart = ( $mode =~ /[sc]/ ? '-'.$start : $start); my @G; my @colors = split /\s+/, $cfg->{Presentation}{multihost}{colors}; my $i = 0; for my $host (@hosts){ $i++; my $swidth = $max->{$start} / $cfg->{Presentation}{detail}{height}; my $rrd = $cfg->{General}{datadir}.$host.".rrd"; next unless -r $rrd; # skip things that do not exist; my $medc = shift @colors; my @tree_path = split /\//,$host; shift @tree_path; my ($host,$real_slave) = split /~/, $tree_path[-1]; #/ $tree_path[-1] = $host; my $tree = Smokeping::get_tree($cfg,\@tree_path); my $label = $tree->{menu}; if ($real_slave){ $label .= "<". $cfg->{Slaves}{$real_slave}{display_name}; } my $probe = $cfg->{__probes}{$tree->{probe}}; my $XProbeDesc = $probe->ProbeDesc(); if (not $ProbeDesc or $ProbeDesc eq $XProbeDesc){ $ProbeDesc = $XProbeDesc; } else { $ProbeDesc = "various probes"; } my $XProbeUnit = $probe->ProbeUnit(); if (not $ProbeUnit or $ProbeUnit eq $XProbeUnit){ $ProbeUnit = $XProbeUnit; } else { $ProbeUnit = "various units"; } my $pings = $probe->_pings($tree); $label = sprintf("%-20s",$label); push @colors, $medc; my $sdc = $medc; my $stddev = Smokeping::RRDhelpers::get_stddev($rrd,'median','AVERAGE',$realstart,$sigtime) || 0; $sdc =~ s/^(......).*/${1}30/; push @G, "DEF:median$i=${rrd}:median:AVERAGE", "DEF:loss$i=${rrd}:loss:AVERAGE", "CDEF:ploss$i=loss$i,$pings,/,100,*", "CDEF:dm$i=median$i,0,".$max->{$start}.",LIMIT", Smokeping::calc_stddev($rrd,$i,$pings), "CDEF:dmlow$i=dm$i,sdev$i,2,/,-", "CDEF:s2d$i=sdev$i", # "CDEF:dm2=median,1.5,*,0,$max,LIMIT", # "LINE1:dm2", # this is for kicking things down a bit "AREA:dmlow$i", "AREA:s2d${i}#${sdc}::STACK", "LINE1:dm$i#${medc}:${label}", "VDEF:avmed$i=median$i,AVERAGE", "VDEF:avsd$i=sdev$i,AVERAGE", "CDEF:msr$i=median$i,POP,avmed$i,avsd$i,/", "VDEF:avmsr$i=msr$i,AVERAGE", "GPRINT:avmed$i:%5.1lf %ss av md ", "GPRINT:ploss$i:AVERAGE:%5.1lf %% av ls", sprintf('COMMENT:%5.1lf ms sd',$stddev*1000.0), "GPRINT:avmsr$i:%5.1lf %s am/as\\l"; }; my @task; push @task, "--logarithmic" if $cfg->{Presentation}{detail}{logarithmic} and $cfg->{Presentation}{detail}{logarithmic} eq 'yes'; push @task, '--lazy' if $mode eq 's' and $lastheight{$start} == $max->{$start}; push @task, "${imgbase}_${end}_${start}.png", '--start',$realstart, ($end ne 'last' ? ('--end',$end) : ()), '--height',$cfg->{Presentation}{detail}{height}, '--width',$cfg->{Presentation}{detail}{width}, '--title',$desc, '--rigid','--upper-limit', $max->{$start}, '--lower-limit',($cfg->{Presentation}{detail}{logarithmic} ? ($max->{$start} > 0.01) ? '0.001' : '0.0001' : '0'), '--vertical-label',$ProbeUnit, '--imgformat','PNG', '--color', 'SHADEA#ffffff', '--color', 'SHADEB#ffffff', '--color', 'BACK#ffffff', '--color', 'CANVAS#ffffff', @G, "COMMENT:$ProbeDesc", 'COMMENT:end\: '.$date.'\j'; my $graphret; ($graphret,$xs,$ys) = RRDs::graph @task; # print "
INFO:".join("
",@task)."
"; my $ERROR = RRDs::error(); if ($ERROR) { return "
ERROR: $ERROR
".join("
",@task)."
"; }; if ($mode eq 'a'){ # ajax mode open my $img, "${imgbase}_${end}_${start}.png"; binmode $img; print "Content-Type: image/png\n"; my $data; read($img,$data,(stat($img))[7]); close $img; print "Content-Length: ".length($data)."\n\n"; print $data; unlink "${imgbase}_${end}_${start}.png"; return undef; } elsif ($mode eq 'n'){ # navigator mode # $page .= qq|
|; $page .= qq|| ; # $page .= "
"; $page .= $q->start_form(-method=>'GET', -id=>'range_form') . "

Time range: " . $q->textfield(-name=>'start',-default=>$startstr) . "  to  ".$q->textfield(-name=>'end',-default=>$endstr) . $q->hidden(-name=>'epoch_start',-id=>'epoch_start',-default=>$start) . $q->hidden(-name=>'epoch_end',-id=>'epoch_end',-default=>time()) . $q->hidden(-name=>'target',-id=>'target' ) . $q->hidden(-name=>'hierarchy',-id=>'hierarchy' ) . $q->hidden(-name=>'displaymode',-default=>$mode ) . " " . $q->submit(-name=>'Generate!') . "

" . $q->end_form(); } elsif ($mode eq 's') { # classic mode $startstr =~ s/\s/%20/g; $endstr =~ s/\s/%20/g; $page .= "
"; # $page .= (time-$timer_start)."
"; # $page .= join " ",map {"'$_'"} @task; $page .= "
"; $page .= ( qq{' . qq{}."" ); #" $page .= "
"; } else { # chart mode $page .= "
"; $page .= ( qq{} . qq{}."" ); #" $page .= "
"; } } return $page; } 1; __END__ =head1 COPYRIGHT Copyright 2007 by Tobias Oetiker =head1 LICENSE 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; either version 2 of the License, or (at your option) any later version. 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., 675 Mass Ave, Cambridge, MA 02139, USA. =head1 AUTHOR Tobias Oetiker Etobi@oetiker.chE =cut