01.03.2013

perl erzeugt einen Graphen aus Messdaten

Mal ein schönes Beispiel dafür, wie man mit wenig Aufwand und den richtigen perl-Modulen eine hübsche Grafik zaubern kann ...

Als Beispiel dient hier meine Logdatei mit den "load average"-Werten. Die Daten lasse ich minütlich mit einem cron-Job speichern, und wenn ich mit einem Browser das CGI-Skript aufrufe, bekomme ich als Ergebnis ein hübsches PNG oder JPG.


Zur Erinnerung: die "load average" gibt an, wieviele Prozesse durchschnittlich während der Probezeit auf die Zuteilung von CPU-Zeit warten mussten. Werte für den Zeitraum "1 min", "5 min" und "15 min" werden erfasst. Diese Angaben erhält man auf der Kommandozeile am bequemsten mit dem Befehl "uptime" (so gut wie alle Unixe, AIX, Solaris, BSD etc.) oder mit dem /proc-Pseudo-Filesystem (nur Linux).

So funktioniert der Aufruf des CGI-Skripts im Browser:


Der Parameter "what" gibt an, welche "load average" gemeint ist (1, 5, 15), "xmax" und "ymax" überschreiben die Defaultwerte im Skript mit anderen Größenangaben. Weiterhin sind noch "hours" (anderer Zeitraum als 24h) und "single" möglich. Im Normalfall werden in einen Graphen alle drei Kurven gemalt; wenn "what" und "single" verwendet werden, ist in der Grafik nur diese eine Kurve enthalten.

http://meinserver.domain/cgi-bin/loadavg.pl?what=1&xmax=550&ymax=150

Hier ist der cron-Job:

# load avg
* * * * * /usr/local/bin/loadavg.pl -d 7 >/dev/null 2>&1

... und das Skript, das aufgerufen wird:

Das Skript speichert in der Datendatei maximal soviele Tage, wie im Parameter -d angegeben sind (default 1 Tag). Auf diese Weise hat das Skript seinen eigenen logrotate.
#!/usr/bin/perl -w
use Getopt::Std;
use strict 'vars';
use strict 'refs';
use vars qw( $opt_d );
$opt_d=1;
getopts("d:");
my $loadavg="/proc/loadavg";
my $outfile="/var/spool/loadavg";
sub out {
  my ($what,$val)=@_;
  my @val=();
  if (open(IN,"<","$outfile.$what")) {
    @val=<IN>;
    close(IN);
  }
  splice(@val,0,-1440*$opt_d);
  push(@val,$val);
  if (open(OUT,">","$outfile.$what")) {
    print OUT @val,"\n";
    close(OUT);
  }
}
if (open(F,"<",$loadavg)) {
  my ($l1,$l5,$l15)=(split(/\s+/,<F>))[0..2];
  out("1",$l1);
  out("5",$l5);
  out("15",$l15);
  close(F);
}

 ... und das hier ist das CGI-Skript, das die Grafik erzeugt: 

(Meine Erklärungen zu einzelnen Funktionen sind in blauer Schrift dazwischen)

Die Grafik wird eigentlich nur mit einem einzigen Befehl erzeugt, nämlich dem
print $graph->plot(\@data)->$format();
ganz am Ende. Der ganze Krams vorher dient dazu, eine schöne Beschriftung herzustellen und eine Skalierung zu berechnen, damit das hübsch aussieht. Und natürlich der CGI-Kram, um den Parameter aus der "Kommandozeile" auszuwerten.

#!/usr/bin/perl -w

use strict 'vars';
use strict 'refs';

use GD::Graph;
use GD::Graph::lines;
use GD::Graph::linespoints;
use GD::Graph::points;
use GD::Graph::bars;
use CGI qw(:standard);

my @data;
my $graph;
my $format;
my $maxscale;
# zeitabstand in sek.
my $index=60;
my ($xmax,$ymax)=(600,400); # die gewünschte Größe der Grafikdatei
my $hours=1;
my $dir="/var/spool";
my $file1 ="$dir/loadavg.1";
my $file5 ="$dir/loadavg.5";
my $file15="$dir/loadavg.15";
my $query;
# default 5 min.
my $req=5;
my $file;
my $single=0;

# X-Beschriftung
# werte1
# werte2
# ...
@data = (
[""],
[],
[],
[],
);
# loadread lädt aus der Datendatei $file die neuesten $min Einträge
sub loadread {
  my ($file,$min)=@_;
  my @val=();

  if (open(IN,"<","$file")) {
    @val=<IN>;
    chomp(@val);
    close(IN);
    if (scalar(@val)>$min) {
      splice(@val,0,-$min);
    }
  }
  return \@val;
}
# arraymax sucht aus einer Liste von Arrayrefs das maximale Element
sub arraymax {
  my $m=0;

  foreach my $a (@_) {
    foreach my $i (@{$a}) {
      $m=$i if ($i>$m);
    }
  }
  return $m;
}
# xtext erzeugt die Beschriftung der X-Achse
sub xtext {
  my ($ref,$start,$diff,$gap)=@_;
  my $cnt=scalar(@$ref);
  my $s=$diff/$gap;
  my $i;
  my $time;
  my @t;

  $start-=($cnt*$diff);
  for ($i=0; $i<$cnt; ++$i) {
    @t=(localtime($start+$i*$diff))[1..2];
    $time=sprintf("%02d:%02d",$t[1],$t[0]);
    if ($i % $s) {
      $data[0]->[$i]="";
    }
    else {
      $data[0]->[$i]=$time;
    }
  }
}

$query=new CGI();
$req=$query->param( 'what' ) || 1;
$xmax=$query->param( 'xmax' ) || 800;
$ymax=$query->param( 'ymax' ) || 600;
$hours=$query->param( 'hours' ) || 24;
$single=$query->param( 'single' ) || 0;
$single=0;

$graph = GD::Graph::lines->new($xmax, $ymax);
$format = $graph->export_format;

# calc minutes
$data[1]=loadread($file1, $hours*60);
$data[2]=loadread($file5, $hours*60);
$data[3]=loadread($file15,$hours*60);
$file=$file1;

$maxscale=arraymax($data[1],$data[2],$data[3]);
$maxscale=int($maxscale*100)/100;
xtext($data[1],(stat($file))[9],$index,$req);

unless ($single) {
  $data[1]=$data[2] if ($req==5);
  $data[1]=$data[3] if ($req==15);
  delete $data[2];
  delete $data[3];
}

$graph->set(
  x_label => 'Time',
  y_label => 'Load',
  title => 'Load avg '.$req,
  y_max_value => $maxscale,
  y_tick_number => 10,
  y_label_skip => 2,
  x_label_skip => 2*$index,
) or die $graph->error;
$graph->set(
  marker_size => 1,
) or die $graph->error;

print header("image/$format");
binmode STDOUT;
print $graph->plot(\@data)->$format();