summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/Smokeping/Graphs.pm349
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)
+ . "&nbsp;&nbsp;to&nbsp;&nbsp;".$q->textfield(-name=>'end',-default=>$endstr)
+ . $q->hidden(-name=>'target' )
+ . $q->hidden(-name=>'displaymode',-default=>$mode )
+ . "&nbsp;"
+ . $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