commit
This commit is contained in:
403
lib/Smokeping/Graphs.pm
Normal file
403
lib/Smokeping/Graphs.pm
Normal file
@@ -0,0 +1,403 @@
|
||||
# -*- 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_colors ($){
|
||||
my $cfg = shift;
|
||||
my @colorList = ();
|
||||
my $colorText = $cfg->{Presentation}{colortext};
|
||||
my $colorBorder = $cfg->{Presentation}{colorborder};
|
||||
my $colorBackground = $cfg->{Presentation}{colorbackground};
|
||||
|
||||
# If graphborders set to no, and no color override, then return default colors as before
|
||||
if (($cfg->{Presentation}{graphborders} eq 'no') && !($colorText||$colorBorder||$colorBackground)) {
|
||||
return '--border', '0',
|
||||
'--color', 'BACK#ffffff00',
|
||||
'--color', 'CANVAS#ffffff00';
|
||||
};
|
||||
|
||||
# If there are any overrides, use them
|
||||
if ($cfg->{Presentation}{graphborders} eq 'no') {
|
||||
push(@colorList, '--border', '0');
|
||||
};
|
||||
if ($colorText) {
|
||||
push(@colorList, '--color', "FONT#${colorText}");
|
||||
};
|
||||
if ($colorBorder) {
|
||||
push(@colorList, '--color', "FRAME#${colorBorder}");
|
||||
};
|
||||
if ($colorBackground) {
|
||||
push(@colorList, '--color', "SHADEA#${colorBackground}");
|
||||
push(@colorList, '--color', "SHADEB#${colorBackground}");
|
||||
push(@colorList, '--color', "BACK#${colorBackground}");
|
||||
push(@colorList, '--color', "CANVAS#${colorBackground}");
|
||||
};
|
||||
|
||||
if (@colorList) { return @colorList[0..$#colorList] };
|
||||
|
||||
# Otherwise use rrdtool defaults
|
||||
return
|
||||
}
|
||||
|
||||
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 "<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 = 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 "<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";
|
||||
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);
|
||||
$label =~ s/:/\\:/g;
|
||||
|
||||
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',$cfg->{Presentation}{htmltitle} ne 'yes' ? $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',
|
||||
Smokeping::Graphs::get_colors($cfg),
|
||||
@G,
|
||||
"COMMENT:$ProbeDesc",
|
||||
"COMMENT:$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";
|
||||
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 .= "<div class=\"panel\">";
|
||||
$page .= "<div class=\"panel-heading\"><h2>$desc</h2></div>"
|
||||
if $cfg->{Presentation}{htmltitle} eq 'yes';
|
||||
$page .= "<div class=\"panel-body\">";
|
||||
|
||||
$page .= qq|<IMG id="zoom" alt="" width="$xs" height="$ys" SRC="${imghref}_${end}_${start}.png">| ;
|
||||
|
||||
$page .= $q->start_form(-method=>'GET', -id=>'range_form')
|
||||
. "<p>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!')
|
||||
. "</p>"
|
||||
. $q->end_form();
|
||||
|
||||
$page .= "</div></div>\n";
|
||||
} elsif ($mode eq 's') { # classic mode
|
||||
$startstr =~ s/\s/%20/g;
|
||||
$endstr =~ s/\s/%20/g;
|
||||
$page .= "<div class=\"panel\">";
|
||||
# $page .= (time-$timer_start)."<br/>";
|
||||
# $page .= join " ",map {"'$_'"} @task;
|
||||
$page .= "<div class=\"panel-heading\"><h2>$desc</h2></div>"
|
||||
if $cfg->{Presentation}{htmltitle} eq 'yes';
|
||||
$page .= "<div class=\"panel-body\">";
|
||||
$page .= ( qq{<a href="?displaymode=n;start=$startstr;end=now;}."target=".$q->param('target').'">'
|
||||
. qq{<IMG ALT="" SRC="${imghref}_${end}_${start}.png" class="img-responsive">}."</a>" ); #"
|
||||
$page .= "</div></div>\n";
|
||||
} else { # chart mode
|
||||
$page .= "<div class=\"panel\">";
|
||||
$page .= "<div class=\"panel-heading\"><h2>$desc</h2></div>"
|
||||
if $cfg->{Presentation}{htmltitle} eq 'yes';
|
||||
$page .= "<div class=\"panel-body\">";
|
||||
$page .= ( qq{<a href="}.lnk($q, (join ".", @$open)).qq{">}
|
||||
. qq{<IMG ALT="" SRC="${imghref}_${end}_${start}.png" class="img-responsive">}."</a>" ); #"
|
||||
$page .= "</div></div>\n";
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
Reference in New Issue
Block a user