diff options
-rw-r--r-- | lib/Smokeping/Graphs.pm | 349 |
1 files changed, 349 insertions, 0 deletions
diff --git a/lib/Smokeping/Graphs.pm b/lib/Smokeping/Graphs.pm new file mode 100644 index 0000000..3050c92 --- /dev/null +++ b/lib/Smokeping/Graphs.pm @@ -0,0 +1,349 @@ +# -*- 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 @dirs = @{$open}; + + return "<div>ERROR: ".(join ".", @dirs)." has no probe defined</div>" + unless $tree->{probe}; + + return "<div>ERROR: ".(join ".", @dirs)." $tree->{probe} is not known</div>" + unless $cfg->{__probes}{$tree->{probe}}; + + return "<div>ERROR: ".(join ".", @dirs)." ist no multi host</div>" + unless $tree->{host} =~ m|^/|; + + return "<div>ERROR: unknown displaymode $mode</div>" + 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 (<HG>){ + 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 = $q->param('serial'); + $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 "<div>RRDtool did not understand your input: $ERROR.</div>" 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"; + 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); + $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:%.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 "<div>INFO:".join("<br/>",@task)."</div>"; + my $ERROR = RRDs::error(); + if ($ERROR) { + return "<div>ERROR: $ERROR</div><div>".join("<br/>",@task)."</div>"; + }; + + + if ($mode eq 'a'){ # ajax mode + open my $img, "${imgbase}_${end}_${start}.png"; + binmode $img; + print "Content-Type: image/png\n\n"; + my $data; + read($img,$data,(stat($img))[7]); + close $img; + print $data; + unlink "${imgbase}_${end}_${start}.png"; + exit; + } + + elsif ($mode eq 'n'){ # navigator mode +# $page .= qq|<div class="zoom" style="cursor: crosshair;">|; + $page .= qq|<IMG id="zoom" border="0" width="$xs" height="$ys" SRC="${imghref}_${end}_${start}.png">| ; +# $page .= "</div>"; + + $page .= $q->start_form(-method=>'GET') + . "<p>Time range: " + . $q->textfield(-name=>'start',-default=>$startstr) + . " to ".$q->textfield(-name=>'end',-default=>$endstr) + . $q->hidden(-name=>'target' ) + . $q->hidden(-name=>'displaymode',-default=>$mode ) + . " " + . $q->submit(-name=>'Generate!') + . "</p>" + . $q->end_form(); + } elsif ($mode eq 's') { # classic mode + $startstr =~ s/\s/%20/g; + $endstr =~ s/\s/%20/g; + $page .= "<div>"; +# $page .= (time-$timer_start)."<br/>"; +# $page .= join " ",map {"'$_'"} @task; + $page .= "<br/>"; + $page .= ( qq{<a href="?displaymode=n;start=$startstr;end=now;}."target=".$q->param('target').'">' + . qq{<IMG BORDER="0" SRC="${imghref}_${end}_${start}.png">}."</a>" ); #" + $page .= "</div>"; + } else { # chart mode + $page .= "<div>"; + $page .= ( qq{<a href="}.lnk($q, (join ".", @$open)).qq{">} + . qq{<IMG BORDER="0" SRC="${imghref}_${end}_${start}.png">}."</a>" ); #" + $page .= "</div>"; + + } + + } + 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 E<lt>tobi@oetiker.chE<gt> + +=cut |