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:
- 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.
- 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.
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=300Bei 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'> Σ </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]
Keine Kommentare:
Kommentar veröffentlichen