Skip to content

Instantly share code, notes, and snippets.

@tobert
Created July 3, 2018 21:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tobert/380c95878c9e59667f61816df21dbd24 to your computer and use it in GitHub Desktop.
Save tobert/380c95878c9e59667f61816df21dbd24 to your computer and use it in GitHub Desktop.
An old perl hack for generating a colored grid of cluster load from load averages collected by collectd in RRD files.
#!/usr/bin/perl
# this needs a serious cleanup/rewrite - it was hacked together
# very quickly for a completely different purpose many years ago
# and has grown in the worst organic way since
# but ... it does work fairly well so it persists
# Copyright 2007-2011 A Tobey <tobert@gmail.com>
use strict;
use warnings;
use RRDs ();
use JSON;
use GD;
use CGI qw(param);
use Data::Dumper;
our $rrddir = '/data/collectd/rrd';
# compute the load gradients
# notice how they meet up - low's ending is middles beginning color, etc.
# this is what makes the transitions smooth
# The color is chosen by simply multiplying the load by 100 then subtracting
# whatever the offset of low/middle/high is.
our @low = mkramp( 100, 0, 255, 0, 255, 255, 0 );
our @middle = mkramp( 200, 255, 255, 0, 0, 0, 255 );
our @high = mkramp( 200, 0, 0, 255, 255, 0, 0 );
# adjust to fit the cluster
our $grid_width = 10;
our $grid_height = 2;
our $graph_width = 497;
our $graph_height = 207;
our $nodecount = 0;
our @dhosts;
our @grid = ();
opendir my $rrdfh, "/data/collectd/rrd";
while (my $name = readdir $rrdfh) {
next if ($name =~ /^\./);
push @dhosts, $name;
$nodecount++;
}
closedir $rrdfh;
if ( param('ajax') ) {
my $hostname = param('hostname');
my $data = getdata( $hostname );
push @$data, getcolor( $data->[1] );
my @cell = mkcell($data);
$data->[3] = 'Cluster Average';
print "X-json: ", JSON::to_json( $data );
print "\nContent-type: text/html\n\n";
print $cell[1];
exit 0;
}
elsif ( param('legend') ) {
my $width = $#low + $#middle + $#high;
my $height = 10;
my $im = GD::Image->new( $width, $height, 1 );
$im->setThickness( 1 );
my $count = 0;
foreach my $color ( @low, @middle, @high ) {
my $imc = $im->colorAllocate( @$color );
$im->line( $count, 0, $count, $height, $imc );
$count++;
}
print "Content-type: image/png\n\n";
binmode(STDOUT);
print $im->png;
exit(0);
}
while (@dhosts) {
my @nodes = ();
for (my $i=0; $i<$grid_width; $i++) {
last unless (@dhosts > 0);
push @nodes, getdata(shift(@dhosts));
}
push @grid, \@nodes;
}
# output the page header & beginning of the table
# - not using CGI.pm to make this as lightweight as possible since 99.9% of its functionality isn't needed
print <<EOHTML;
Content-type: text/html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>Cluster Load</title>
<meta http-equiv="Author" content="A Tobey <tobert\@gmail.com>" />
<meta http-equiv="Generated-By" content="gradient.cgi" />
<script type="text/javascript" src="/js/prototype.js"></script>
<script type="text/javascript" src="/js/datadumper.js"></script>
<style type="text/css" media="screen">
table {
border-width: 0px;
border-style: solid;
border-collapse: separate;
}
td {
text-align: center;
width: 34px;
height: 34px;
font-size: 14px;
text-decoration: none;
font-family: monospace;
border-width: 1px;
margin: 1px;
padding: 1px;
border-color: gray;
-moz-border-radius: 8px;
}
img {
border: 0px;
padding: 0px;
margin: 0px;
}
</style>
<script type="text/javascript">
var graph_hostname = '';
function showgraph ( hostname ) {
graph_hostname = hostname;
\$('graph1').innerHTML = graphlink( hostname, 'hour' );
\$('graph2').innerHTML = graphlink( hostname, 'day' );
}
function graphlink ( hostname, period ) {
var now = new Date();
return '<img height="$graph_height" width="$graph_width" src="/cgi-bin/collection.cgi?action=show_graph;plugin=load;type=load;timespan='+period+';host='+hostname+';width=$graph_width;height=$graph_height;request_time='+now.getTime()+'"/>';
}
function noop () { }
</script>
</head>
<body>
<div>|
<a href="/cgi-bin/gradient.cgi">Real-Time HUD</a> |
<a href="/cgi-bin/collection.cgi">Browse</a> |
<a href="/cgi-bin/allcpu.cgi">All CPU0</a> |
<a href="http://hadoop-jobtracker:50030/jobtracker.jsp">JobTracker</a> |
<a href="http://hadoop-namenode:50070/dfshealth.jsp">NameNode</a> |
</div>
<hr/>
<div style="float: left; margin-right: 40px;">
<table>
<caption>load averages</caption>
<tr>
EOHTML
# build the load table
my $cl_avg_total = 0;
foreach my $row ( @grid ) {
foreach my $col ( @$row ) {
print join("\n", mkcell( $col ));
$cl_avg_total += $col->[1];
}
print "\t\t</tr>\n\t\t<tr>\n";
}
print join("\n", mkcell( [ 'Cluster Average', $cl_avg_total / $nodecount, time, undef, 'colspan="10"', 1 ] ));
# tack on a wide cell with the total average across all nodes, then move on to the next table
print <<EOHTML;
</tr>
</table>
</div>
<div style="float: left; width: 512px;" id="graphbox">
<div id="graph1" class="graphcell"></div>
<br/>
<div id="graph2" class="graphcell"></div>
</div>
<div style="clear: both;">
<table style="margin-left: 20px;">
<tr>
<td colspan="3">
<img src="/cgi-bin/gradient.cgi?legend=1"/>
</td>
</tr>
<tr>
<td style="text-align: left;">low (0.00 - 1.00)</td>
<td style="text-align: center;">medium (1.01 - 3.00)</td>
<td style="text-align: right;">high (3.01 - 5.00)</td>
</tr>
</table>
<!-- p style="text-align: center; width: 100%">
<a href="/xen.html">Historical Data</a>
</p -->
</div>
</body>
<script type="text/javascript">
var load_cells = document.getElementsByClassName("loadcell");
var node_avg = new Array();
var load_cell;
for (var i=0; i<load_cells.length; i++) {
load_cell = load_cells[i];
if ( load_cell.id == undefined )
continue;
new Ajax.PeriodicalUpdater( load_cell, '/cgi-bin/gradient.cgi', {
frequency : 10,
parameters: {
ajax: "loadcell",
hostname: load_cell.id
},
onSuccess: function(transport, json) {
//console.log( "Target: " + json[0] );
var target = \$( json[0] );
target.style.backgroundColor = json[4];
target.style.color = json[5];
node_avg.push( json[1] );
if ( node_avg.length == $nodecount ) {
var total = 0;
node_avg.map( function ( value ) { total += parseFloat(value); } );
//console.log( "Total: " + total );
//console.log( "Nodecount: $nodecount" );
//console.log( "Average: " + (total / $nodecount.00) );
\$(json[3]).innerHTML = (total / $nodecount.00).toPrecision(4);
}
}
});
}
new PeriodicalExecuter( function (p) {
if ( graph_hostname != '' ) {
\$('graph1').innerHTML = graphlink( graph_hostname, 'hour' );
}
}, 30)
new PeriodicalExecuter( function (p) {
if ( graph_hostname != '' ) {
\$('graph2').innerHTML = graphlink( graph_hostname, 'day' );
}
}, 120)
</script>
</html>
EOHTML
sub mkcell {
# 0: load, 1: hostname/title, 2: last update, 3:
my $col = shift;
my( $bgcolor, $fgcolor ) = getcolor( $col->[1] );
my @lutime = localtime( $col->[2] );
my $gfunc = sprintf "showgraph('%s')", $col->[0]; # for showing a graph on the right
my $title = sprintf '%s -- Last Update: %02d:%02d:%02d', $col->[0], $lutime[2], $lutime[1], $lutime[0];
my $load = sprintf '%0.02f', $col->[1];
my $tdec = 'none'; # font style, e.g. none/italic
my $tdtxt = ''; # to add attributes to the cell
my $tclass = 'loadcell';
if ( $col->[3] ) {
$tdec = 'italic';
}
if ( $col->[4] ) {
$tdtxt = $col->[4];
}
if ( $col->[0] =~ /Cluster/ ) {
$gfunc = "noop()";
$tclass = 'fake_loadcell';
}
return(
sprintf( '<td %s style="background-color: %s; text-decoration: %s;" title="%s" id="%s" class="%s">',
$tdtxt, $bgcolor, $tdec, $title, $col->[0], $tclass ),
sprintf( '<a href="javascript:%s" style="color: %s; text-decoration: %s;">%s</a>',
$gfunc, $fgcolor, $tdec, $load ),
'</td>'
);
}
# uses the gradient arrays to choose a background color for the given load
# - also returns a foreground color based on the luminosity of the background color
sub getcolor {
my $load = shift;
my $color = [0,0,0];
if ( $load > 5.00 ) {
$color = [ 255, 0, 0 ];
}
elsif ( $load > 1.00 && $load < 3.00 ) {
$color = $middle[ int($load * 100) - 100 ];
}
elsif ( $load >= 3.00 ) {
$color = $high[ int($load * 100) - 300 ];
}
else {
$color = $low[ int($load * 100) ];
}
# now make a fg color based on luminance (perlmonks node 305198)
my $lum = ( $color->[0] * 0.212671 ) + ( $color->[1] * 0.715160 ) + ( $color->[2] * 0.072169 );
my $fgcolor = [ 0, 0, 0 ]; # black
if ( $lum < 128 ) {
$fgcolor = [ 255, 255, 255 ]; # white
}
return( sprintf( '#%02x%02x%02x', @$color ), sprintf( '#%02x%02x%02x', @$fgcolor ) );
}
# build a gradient in an array
# http://michal.guerquin.com/gradient.html
sub mkramp {
my( $steps, $r0, $g0, $b0, $r1, $g1, $b1 ) = @_;
# delta = end color - start color / number of steps in gradient
my $dr = ($r1 - $r0) / $steps;
my $dg = ($g1 - $g0) / $steps;
my $db = ($b1 - $b0) / $steps;
my @ramp;
my( $r, $g, $b ) = ( $r0, $g0, $b0 );
for ( my $i=0; $i<=$steps; $i++ ) {
push @ramp, [int($r),int($g),int($b)];
$r += $dr;
$g += $dg;
$b += $db;
}
return @ramp;
}
sub getdata {
my( $hostname ) = @_;
#warn "$rrddir/$hostname/load/load.rrd";
my $data = RRDs::info( "$rrddir/$hostname/load/load.rrd" );
return [
$hostname,
sprintf( '%0.02f', $data->{ 'ds[shortterm].last_ds' }),
$data->{ 'last_update' },
# true/false telling whether the data is stale or not
($data->{last_update} + ($data->{step} * 2)) < time ? 1 : undef
];
}
sub total_array_load {
my $ldata = shift;
my $total = 0;
foreach my $row ( @$ldata ) {
$total += $row->[1];
}
return $total;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment