Posts mit dem Label CGI werden angezeigt. Alle Posts anzeigen
Posts mit dem Label CGI werden angezeigt. Alle Posts anzeigen

18.01.2014

Erbsen zählen - Perl und CGI - Dritter Teil

Im dritten Teil beschreibe ich nun das Interessanteste: die Auswertung der Daten in Text- und Grafikform, deren Prinzip ich im ersten Teil und Sammlung im zweiten Teil beschrieben habe.

Genau wie schon früher beschrieben, verwende ich das Grafikmodul GD für Perl. Dort kann man ein paar Daten hineinstecken und dann mit einem Aufruf unterschiedliche Schaubilder erzeugen lassen, als "line"-Graph, "bar"-Graph und viele andere. Der Output ist dann HTTP-konform eine Grafikdatei im gewünschten Format, z.B. PNG oder GIF, zusammen mit einer Headerzeile und dem passenden MIME-Typ.

Beim Design gibt es zwei grundsätzliche Überlegungen:
  1. Die Logdateien sammeln Daten pro überwachtem Hostname, ich muss also noch eine Summe bilden, um den Verbrauch pro Host im Verhältnis zum Rest beurteilen zu können.
  2. Ich würde gern sowohl eine Tabelle mit den aktuellen Daten sehen als auch einen Graphen, aus dem man Trends ablesen kann. Ich brauche also im CGI-Skript eine Fallunterscheidung - Text oder Grafik.
Hier kommt das komplette CGI-Skript. An ein paar Stellen füge ich Kommentare in rot oder blau ein, wenn es etwas zu erklären gibt.

Eine grundsätzliche Bemerkung vorneweg: die Datenübergabe erfolgt immer in Form von Referenzen (in C wären das Zeiger auf Datenstrukturen), deshalb verwende ich im Skript i.a. auch gleich Variablen, Arrays und Hashes, die Referenzen enthalten, damit die Übergabe an GD nicht noch unnötig Datenformate umwandeln muss.

Um das CGI-Skript bequem testen zu können, habe ich einen "Testmodus" eingebaut. Man kann generell Skripte, die das CGI-Modul verwenden, auch auf der Kommandozeile aufrufen. Die Argumente, die normalerweise in der Request-URL nach dem "?" folgen, schreibt man einfach als Pärchen mit "name=wert" hinter den Skriptnamen in die Kommandozeile, wie man hier am Beispiel sieht.
# ./accounting.pl scale=1000 testmode=99 xmax=400 ymax=300
Bei diesem Testmodus ist noch zu bedenken, dass natürlich die Umgebungsvariablen des Webservers nicht gesetzt  sind (QUERY_STRING, PATH_INFO, die SSL_*-Variablen, wenn das Skript mit https aufgerufen wurde, usw.). Ggfs. müsste man diese Variablen manuell mit passenden gefälschten Inhalten setzen, damit das Skript an den entsprechenden Stellen sinnvolle Werte bekommt.

Dem Skript kann man einen Parameter "filter" übergeben, um nur einen ganz bestimmten Hostnamen auszufiltern. Deshalb gibt es eine etwas unübersichtliche Fallunterscheidung, ob ein Filter gesetzt ist oder nicht.

Mit "mode=1" wird eine Grafik in voller Größe mit mehreren Graphen und der Gesamtsumme angezeigt; "mode=16" erzeugt eine Tabelle in Textform mit einer Zeile pro Hostname in der Logdatei und hinter jedem Hostnamen in einer Extraspalte eine wönzig kleine Grafik mit der Gesamtsumme und dem Verbrauch dieses Hosts.

#!/usr/bin/perl -w

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

use GD::Graph;
use GD::Graph::lines;
use GD::Graph::bars;
use GD::Graph::hbars;

# Die GD-Module zeichnen die verschiedenen Typen von Graphen
use CGI qw(:standard);

#use lib "/usr/local/bin";

my @data;

# Array mit Beschriftung, x-Skala und ein oder mehreren y-Datenpunkten
my %hosts;

# Array zum Speichern der Daten pro Hostname
my $graph;
my $format;
my ($xmax,$ymax);

# Größe des auszugebenden Bilds
my $query;
my $range;
my $filter;

# Regexfilter für Hostnamen
# mode=0 full graph mode all hosts
# mode=9 sum only graph
# mode=16 text mode
# mode=99 debug text mode
my $mode;
my @x;
my @y1;

# y1=summierte Daten IN
my @y2;

# y2=summierte Daten OUT
my $scale;
#my $offset;
my $title="LTE volume statistics";

# X-Beschriftung
# werte1
# werte2
# ...
@data=(
);

%hosts=(
# "dummy.moeller-seeling.local" => { "IN " => [ 0 ], "OUT" => [ 0 ] }
);

my $max=0;
my $lines=0;

# logfile inbound, outbound
my ($login,$logout);


$query=new CGI();
$mode=$query->param( 'mode' ) || 0;
$range=$query->param( 'range' ) || "60";
#$offset=$query->param( 'offset' ) || "0";
$scale=$query->param( 'scale' ) || "2500";
$filter=$query->param( 'filter' ) || "";
$xmax=$query->param( 'xmax' ) || "800";
$ymax=$query->param( 'ymax' ) || "600";
$login=$query->param( 'infile' ) || "/var/log/lte-acct.in";
$logout=$query->param( 'outfile' ) || "/var/log/lte-acct.out";
$graph = GD::Graph::lines->new($xmax, $ymax);
$format = $graph->export_format;

# 20140108-110202 IN  i9100.moeller-seeling.local              91M
# 20140108-110202 IN  lifetab.moeller-seeling.local            42M
# 20140108-110202 IN  nexus.moeller-seeling.local              8748K
# 20140108-110202 IN  vettie68.moeller-seeling.local           143M

#my $d;

sub readfile {
  my ($file,$tag,$y)=@_;

  if (open(F,"<",$file)) {
    my ($dt,$old)=("","");

      while (<F>) {
      my ($name,$num,$unit,$hm,$m);

      chomp;
# 20140107-184506 IN  lifetab.moeller-seeling.local            0
# dieser Regex zerlegt eine Zeile, $tag ist IN oder OUT      if ($filter) {
        ($name,$num,$unit,$dt,$hm,$m)=($filter,$7,$8,"$1$2$3-$4$5$6","$4$5",$5)
          if (/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2}) $tag\s+$filter.*?\s*(\d+)([KMG]?$)/);
      }
      else {
        ($name,$num,$unit,$dt,$hm,$m)=($7,$8,$9,"$1$2$3-$4$5$6","$4$5",$5)
          if (/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2}) $tag\s+([a-z.-][0-9a-z.-]+)\s*(\d+)([KMG]?)/);
      }
      if ($name && defined($num) && defined($unit)) {

        ++$lines;
        if ($unit eq "")  { $num>>=10; }
#       if ($unit eq "K") { $num<<=10; }
        if ($unit eq "M") { $num<<=10; }
        if ($unit eq "G") { $num<<=20; }
# alle Zahlenwerte sind in KB        if ($num>1000) {
          if ($ymax<100) { push(@x,$m); }
          else { push(@x,$hm); }
          $num>>=10;
          push(@{$hosts{$name}->{$tag}},$num);
if ($mode==99) { print STDERR "# <$_>\n# $hm $tag $filter $num $unit | $max\n"; }

# in den Logfiles stehen kumulierte Daten pro Host
# alle Werte vom selben Timestamp addieren für Gesamtsumme
          if ($dt eq $old) { $num+=pop(@{$y}); }
#         else { $num+=$offset; }
          push(@{$y},$num);
          $old=$dt;

# Timestamp aufheben für Vergleich mit nächster Zeile
          if ($max<$num) { $max=$num; }

# Maximum merken für y-Skalierung
if ($mode==99) { print STDERR "# @{$y}\n"; }
        }
      }

    }
    close(F);
  }
}

readfile($login, "IN ",\@y1);
readfile($logout,"OUT",\@y2);

if ($mode==99) {
  local($,=", ");
  foreach my $h (keys(%hosts)) {
    print STDERR "# h $h\n";
    print STDERR "# i @{$hosts{$h}->{'IN '}}\n";
    print STDERR "# o @{$hosts{$h}->{'OUT'}}\n";
  }
  print STDERR "# Sum\n";
  print STDERR "# i @y1\n";
  print STDERR "# o @y2\n";
}


# maximal die letzten $range werte aus der Datei darstellen


if ($range>0) {
  splice(@x,0,-$range);
  splice(@y1,0,-$range);
  splice(@y2,0,-$range);
  $lines=$range;
}
$data[0]=\@x;

# mode=9 nur die Summe
# mode !=9 alle Hostnamen auch einzeln zeigen
if ($mode!=9) {
  foreach my $h (keys(%hosts)) {

# inbound ist interessanter
# man könnte aber in und out anzeigen
    push(@data,$hosts{$h}->{'IN '});
#   push(@data,$hosts{$h}->{'OUT'});
  }
}
# sum of all hosts IN
push(@data,\@y1);
# sum of all hosts OUT
#push(@data,\@y2);

$max=$scale*int($max/$scale+1);

# sinnvolle obere y-Grenze ausrechnen

if ($mode==99) { print STDERR "# max=$max\n"; }

# Daten übergeben$graph->set(
  x_label       => 'Volume Date/Time, ' . $lines . " samples",
  y_label       => 'max ' . $max . ' GB',
  title         => 'LTE volume (GB)',
  y_max_value   => $max,
  y_tick_number => 20,
  y_label_skip  => ($ymax<200) ? 5: 2,
  x_labels_vertical => 1,
  x_label_skip  => $lines / 12,
) or die $graph->error;

# optional die Farben selbst festlegen
#$graph->set( dclrs => [ qw(green lred blue cyan) ] );

# gibt es überhaupt daten?

if ($lines>0) {
  if ($mode<16) {

# bilddatei erzeugen
# binmode ist nur mit windows-webserver wichtig
    print header("image/$format");
    binmode STDOUT;
    print $graph->plot(\@data)->$format();
  }

# textmodus output als tabelle
  elsif ($mode==16) {
    print CGI::header();

    print <<__HEADER__;
<HTML>
<HEAD>
<TITLE>$title</TITLE>
<META http-equiv="refresh" content="300" />
<style type="text/css">
th {
  padding : 3px;
  text-align : left;
}
td {
  padding : 3px;
  text-align : right;
}
</style>
</HEAD>
__HEADER__

print "<!-- ";
print join( " -->\n<!-- ",
           map( "$_ => $ENV{$_}", sort(keys(%ENV)))
          );
print "-->\n";

    print <<__BODY__;
<BODY>
<H1>$title</H1>
<table border="1">
<tr>
<th> Hostname </th>
<th> Download </th>
<th> Upload </th>
</tr>
__BODY__

    foreach my $h (keys(%hosts)) {
      print "<tr>\n";
      print "<th> $h</th>\n";
      print "<td>",pop(@{$hosts{$h}->{'IN '}})," MB </td>\n";
      print "<td>",pop(@{$hosts{$h}->{'OUT'}})," MB </td>\n";

# hinter jeden hostnamen einen wönzigen Detailgraphen anzeigen
if (defined($ENV{'HTTP_HOST'})) {
      print "<td><IMG SRC=http://",$ENV{'HTTP_HOST'},$ENV{'SCRIPT_NAME'},"?filter=$h&xmax=300&ymax=100&scale=",int($max/10),"</IMG></td>\n";
}
      print "</tr>\n";
    }
    print "<tr>\n";
    print "<th style='text-align:right'> &Sigma; </th>\n";
    print "<td>",pop(@y1)," MB </td>\n";
    print "<td>",pop(@y2)," MB </td>\n";
if (defined($ENV{'HTTP_HOST'})) {
      print "<td><IMG SRC=http://",$ENV{'HTTP_HOST'},$ENV{'SCRIPT_NAME'},"?mode=9&xmax=300&ymax=100&scale=",int($max/10),"</IMG></td>\n";
}
    print "</tr>\n";

    print <<__BODY__;
</table>
<DIV>
<HR/>
<ADDRESS>
ths, last changed 09.01.2014
</ADDRESS>
</DIV>
</BODY>
</HTML>
__BODY__

  }
}


[Update 20140122: Links zum 1. und 2. Teil eingefügt]

15.09.2013

Portabler Apache-Webserver selbst gebaut

Heute hab ich mal wieder eine kleine Anleitung vorbereitet. Sozusagen aus aktuellem Anlass ;)

Ich habe mich vor kurzem bereit erklärt, als Aushilfe beim Förderverein der Wölfersheimer Schulen einzusteigen und den Webserver ein wenig zu betreuen, weil der edle Spender, der das aufgebaut und bisher betreut hat, beruflich viel um die Ohren hat.

Es geht aber natürlich nicht an, dass jemand direkt am produktiven System Änderungen durchführt, es muss also eine Testumgebung her, in der ich Dinge ausprobieren und auch sonst "herumspielen" kann. Idealerweise sollte es möglich sein, die Änderungen an den Vorstand des Vereins zu schicken, dort begutachten zu lassen, und dann die freigegebenen Änderungen auf den "Live"-Webserver zu überspielen.

Normalerweise ist es kein großes Problem, ein paar Webseiten zusammenzubauen und den Kollegen die Änderungen als Email zuzuschicken.

Das scheitert aber in dem Moment, wenn der Webserver aktive Inhalte ausliefert, wie z.B. Datenbankabfragen durchführt oder einfach nur aktive Seiten in Form von Python-Skripten oder JSP ausliefert. Soweit geht es zum Glück beim Förderverein nicht, es handelt sich "nur" um PHP-Skripte, die vom Apache ausgeführt werden und in HTML-Seiten resultieren, die beim Browser aufschlagen.

Also besteht die Testumgebung minimal aus einem Apache Webserver und dem PHP-Skriptinterpreter. Zusammen mit ein bißchen weiterem Kleinkram besteht der "PortableApache" aus knapp 40 MB Material. Das passt bequem auf den allerkleinsten USB-Stick, in ein Eckchen auf dem Smartphone oder MP3-Player zum bequemen Herumtragen.

Was braucht man nun? Das hängt vom Betriebssystem ab. Für Windows (weil die Vorstandskollegen Windows einsetzen) benötigt man die Installationsdateien von Apache httpd und ein PHP-Paket. Ein paar Bemerkungen zu den Windows-Paketen: es gibt verschiedene vorgefertigte Pakete, unter den Kategorien "VC9", "VC10", "VC11". Das sind Grundpakete von Microsoft (die sogenannte C-Runtime). Die Versionen 9 und 10 laufen mit Windows XP, Version 11 setzt mindestens Windows 7 voraus. Deshalb habe ich hier die Variante mit VC9 verlinkt.

Jetzt werden aus den ZIP-Dateien ein paar Dateien ausgepackt, man braucht gar nicht so viel ;)

Im Internet findet man viele Anleitungen, wie man einen portablen Apache Webserver zusammenbaut. Allen gemeinsam ist, dass sie die Verzeichnisstruktur für Apache beibehalten, es gibt also Verzeichnisse für Logfiles, Module, Konfigurationsdateien usw. Braucht man auf einem USB-Stick eigentlich nicht. Ich habe deshalb alles zusammengeworfen und ein Verzeichnis für Apache und ein Verzeichnis für PHP festgelegt. Das reicht ;)

Man beginnt mit einem Verzeichnis "PortableApache" und legt dort Unterverzeichnisse für "apache" und "php" an. Außerdem benötigt man noch ein Unterverzeichnis, in dem die Dokumente liegen, die ausgeliefert werden sollen. Im Beispiel unten ist dies das Verzeichnis "html", das parallel zu "apache" und "php" liegt. In der Konfigurationsdatei wird es dann als "../html" angesprochen. Auf diese Weise sind die Dokumente unabhängig vom Standort des Apache immer erreichbar.

In das Verzeichnis "apache" gehört nun folgendes:
  1. mod_mime.so
  2. mod_cgi.so
  3. openssl.cfg.cnf
  4. msvcr100.dll
  5. openssl.exe
  6. pcre.dll
  7. ssleay32.dll
  8. zlib1.dll
  9. htpasswd.exe
  10. httpd.exe
  11. libapr-1.dll
  12. libapriconv-1.dll
  13. libaprutil-1.dll
  14. libeay32.dll
  15. libhttpd.dll
  16. libxml2.dll
  17. httpd-ssl.conf
  18. magic
  19. mime.types
  20. httpd.conf
  21. mod_mime_magic.so
  22. mod_rewrite.so
  23. mod_setenvif.so
  24. mod_ssl.so
  25. mod_status.so
  26. mod_substitute.so
  27. mod_userdir.so
  28. mod_version.so
  29. mod_vhost_alias.so
  30. mod_alias.so
  31. mod_authn_core.so
  32. mod_authz_core.so
  33. mod_deflate.so
  34. mod_dir.so
  35. mod_env.so
  36. mod_headers.so
  37. mod_imagemap.so
  38. mod_include.so
  39. mod_info.so
  40. mod_log_config.so
  41. mod_log_debug.so
  42. mod_log_forensic.so
  43. mod_logio.so
  44. php5apache2_4.dll
Die Datei php5apache2_4.dll nimmt man aus dem PHP-Paket.

Im Verzeichnis "php" braucht man ein bißchen mehr Kram, je nachdem, welche  PHP-Funktionen den Skripten zur Verfügung stehen sollen. Wer sich mit PHP auskennt, kann hier natürlich brutal ausmisten, für Grundbedürfnisse kann man meinen Vorschlag noch deutlich verschlanken.

  1. glib-2.dll
  2. gmodule-2.dll
  3. icudt49.dll
  4. icuin49.dll
  5. icuio49.dll
  6. icule49.dll
  7. iculx49.dll
  8. icutest49.dll
  9. icutu49.dll
  10. icuuc49.dll
  11. libeay32.dll
  12. libenchant.dll
  13. libenchant_ispell.dll
  14. libenchant_myspell.dll
  15. libpq.dll
  16. libsasl.dll
  17. php-cgi.exe
  18. php-win.exe
  19. php.exe
  20. php.gif
  21. php.ini-development
  22. php.ini-production
  23. php5apache2_4.dll
  24. php5nsapi.dll
  25. php5ts.dll
  26. ssleay32.dll
Das Unterverzeichnis "php/ext" enthält folgende Dateien:
  1. php_bz2.dll
  2. php_curl.dll
  3. php_enchant.dll
  4. php_exif.dll
  5. php_fileinfo.dll
  6. php_gd2.dll
  7. php_gettext.dll
  8. php_gmp.dll
  9. php_intl.dll
  10. php_mbstring.dll
  11. php_openssl.dll
  12. php_shmop.dll
  13. php_sockets.dll
Um sicherzustellen, dass PHP funktioniert, sollte man nun an dieser Stelle eine Kommandozeile öffnen (Eingabeaufforderung, cmd.exe), in das Verzeichnis "PortableApache/php" wechseln und testen, ob der PHP-Interpreter sich meldet und seine Versionsnummer bekannt gibt:
H:\PortableApache\php>.\php -v
PHP 5.4.19 (cli) (built: Aug 21 2013 01:12:03)
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2013 Zend Technologies


Für diese veränderte Umgebung  muss man dann natürlich auf die Konfigurationsdateien beträchtlich anpassen, damit das Apache-httpd-Programm weiß, wo alle weiteren Module und Dateien zu finden sind.

Man braucht also eine httpd.conf, in der die Verzeichnisse alle aufgeführt sind. Wichtig ist, dass in dieser Konfigurationsdatei keine absoluten Verzeichnisnamen enthalten sind, sondern nur solche, die relativ zum Arbeitsverzeichnis des Webservers (des Prozesses httpd.exe) formuliert sind. Zur Verdeutlichung habe ich diese Stellen unten fett markiert.
Vom Standpunkt der Sicherheit aus gesehen ist diese Webserver-Datei natürlich absolut unvollständig, da fehlen noch einige Anweisungen, um Einbrüche von außen zu verhindern. Das ist aber nicht der Zweck eines "PortableApache". Diese Konfigurationsdatei da unten also bitte nicht als Vorlage für einen "echten" Webserver im Internet verwenden!

Listen 80
LoadModule alias_module mod_alias.so
LoadModule logio_module mod_logio.so
LoadModule log_config_module mod_log_config.so
LoadModule dir_module mod_dir.so
LoadModule mime_module mod_mime.so
LoadModule authn_core_module mod_authn_core.so
LoadModule authz_core_module mod_authz_core.so
LoadModule cgi_module mod_cgi.so
LoadModule env_module mod_env.so
LoadModule rewrite_module mod_rewrite.so
LoadModule setenvif_module mod_setenvif.so
LoadModule ssl_module mod_ssl.so
LoadModule php5_module php5apache2_4.dll
ServerName localhost:80
DocumentRoot "../html"
ServerAdmin you@yourdomain.de
# Include httpd-ssl.conf

ScriptAlias /cgi-bin/ "../cgi-bin/"
LogFormat "%h %l %u %t \"%r\" %>s %b" common
CustomLog access.log common
ErrorLog  error.log
PidFile   httpd.pid

<IfModule php5_module>
  AddHandler application/x-httpd-php .php
  PHPIniDir "../php"
</IfModule>

<IfModule rewrite_module>
  RewriteEngine On
</IfModule>

<IfModule php5_module>
  DirectoryIndex index.php index.htm index.html
</IfModule>

<IfModule dir_module>
  DirectoryIndex index.html index.htm
</IfModule>

<IfModule mime_module>
  TypesConfig "mime.types"
  AddType application/x-compress .Z
  AddType application/x-gzip .gz .tgz
  AddType application/x-bzip2 .bz2 .tz2
</IfModule>

<IfModule ssl_module>
  SSLRandomSeed startup builtin
  SSLRandomSeed connect builtin
</IfModule>

<Directory "../html">
  AllowOverride All
</Directory>


Das Apache-Modul für "rewrite" brauche ich, damit ich solche Spielereien mit der Datei ".htaccess" in den Datenverzeichnissen machen kann:

RewriteEngine on
Options +FollowSymlinks
RewriteBase /

RewriteRule (.*)_(.*)_(.*).html$ /_start.php?cat=$1&id=$2&edit=$3
RewriteRule (.*)_(.*).html$ /_start.php?cat=$1&id=$2
RewriteRule (.*).html$ /_start.php?cat=$1


Ich habe da oben auch schon den Anfang einer SSL-Konfiguration angedeutet, aber natürlich funktioniert das nur, wenn man sich mit dem openssl-Programm ein eigenes Zertifikat baut, also einen private key. Dazu später vielleicht mal einen eigenen Artikel, wie man sich selbst zur root-CA-Instanz macht und dann für sich selbst Keys erzeugt und signiert.

Für eine erste Prüfung, ob diese httpd.conf auch wirklich funktioniert, ruft man "httpd.exe -t" auf (-t testet, ob eine Konfigurationsdatei einigermaßen funktionsfähig aussieht). Wenn man hier die Meldung "Syntax OK" erhält, kann man fast schon loslegen ;)

Für die bequeme Nutzung ist noch ein kleines Startskript vonnöten, das den Apache aufruft. Ich habe es "start.cmd" genannt und direkt in das Verzeichnis "PortableApache" gesteckt.

cd /D "%~dp0apache"
PATH %~dp0php;%PATH%
.\httpd.exe -f httpd.conf



Der Inhalt dieses kleinen Webservers wird nun angezeigt, wenn man mit einem Browser die URL "http://localhost" oder "http://127.0.0.1" aufruft.

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();