Apache Logs auf zentralem Loghost verwalten

Das Speichern und Verwalten von Webserver-Zugriffslogs ist trivial: Man nutzt die Directiven CustomLog und ErrorLog im Apache und alles ist erledigt. Ein besonders findiger Admin mit SLES11 ruft vielleicht noch ein zypper -install webalizer auf, und schon ist auch noch die Webstatistik zur Hand.
Doch die Sache hat einen Haken, denn so einfach funktioniert es nur, wenn man unter Linux arbeitet und nur ein paar wenige Webauftritte selbst verwaltet werden, die keine allzu hohen Zugriffszahlen aufweisen.

Die Ausgangslage an der Universität Erlangen-Nürnberg sieht etwas anders aus: Das RRZE betreibt auf fünf Webservern über 700 Webauftritte einiger hundert Kunden. Diese Webauftritte beinhalten das gesamte Spektrum von einer einfachen bis zu einer komplexen Struktur: Manche Webauftritte bestehen aus einer handvoll statischer Webseiten, andere werden mit eigenen Content-Management- und Redaktionssystemen betrieben (dabei auch Performancefresser wie Typo3 und WordPress). Auch für Webauftritte, die nicht auf Basis eines Content-Management- oder Redaktionssystems laufen, verwendet die überwiegende Mehrzahl der Webauftritte interaktive Skripten, wie beispielsweise PHP, Perl oder Python.

Mehrere hundert Gigabyte Traffi c pro Monat sind keine Seltenheit. Die AccessLog-Files sind dabei immer größer als 2 GB. Fasst man den gesamten Datenverkehr der Webauftritte an der FAU zusammen, verzeichnet die Statistik rund ein Terabyte Traffi c und 37 Millionen Zugriffe pro Monat. Da ist es kaum vorstellbar, dass tatsächlich nur fünf „nicht mehr ganz aktuelle“ Sun-Server vom Typ SunFire T2000 den Schauplatz für das gesamte Treiben abgeben. Wie kann das funktionieren?

Ziel

Die Lösung ist relativ einfach, denn hier kommt der Load Balancer ins Spiel. Als Lastverteiler kommt er überall dort zum Einsatz, wo sehr viele Clients eine hohe Anfragedichte erzeugen. Seine Aufgabe ist die Vermittlung der Clients an die einzelnen Server, so dass schließlich jeder Webauftritt von einem der fünf Server bedient wird. Dabei ist selbstverständlich vorher nicht klar, welcher Webauftritt an welchen Server gerät. Jeder Server kann den Request annehmen und bearbeiten. Beim nächsten Aufruf ist es eventuell wieder ein anderer Server. Eine lokale Speicherung der Zugriffslogs ist deshalb nicht angebracht. Sowohl ein Webmaster einer Domain, der etwas nachprüfen möchte, als auch die automatisierte Auswertung mittels Statistikprogramm (z.B. Webalizer) verlangen nach einer Datei, die alle Zugriffe auf den entsprechenden Webauftritt enthält. Jeder Webauftritt muss also über eine eigene Zugriffslog verfügen, die alle Zugriffe verzeichnet, unabhängig davon, welcher Webserver konkret den Request bearbeitet hat. Und unter Umständen wird sogar zusätzlich auch noch die ErrorLog benötigt.

Als weitere Anforderung an eine effektive Zugriffslog-Verwaltung sollen die Server nicht auf eine technologische Basis beschränkt sein. Neben den aktuellen fünf Sun-Servern müssen auch andere Server mit anderen Betriebssystemen (SLES10, SLES11, Ubuntu, u.a.) zentral verwaltete Logdateien erstellen können. Darüber hinaus soll den Bedenken des Datenschutzes Rechnung getragen werden: IP-Adressen von Rechnern, deren Benutzer auf Webseiten zugreifen, sind so weit wie möglich zu anonymisieren.

Umsetzung

Die Umsetzung der Zugriffslog-Verwaltung erfolgt mit Hilfe eines zentralen Loghosts, der alle Logmeldungen mit syslog-ng entgegen
nimmt und sie in die Zieldateien schreibt. Die Webserver spielen dabei die Rolle der „Clients“, die dann die Nachrichten über
syslog oder syslog-ng an den Loghost senden.
Nun aber noch ein paar Worte zur Konfi guration der Syslog-Daemon: Je nachdem auf welchem Betriebssystem man arbeitet, steht
auf den Servern des RRZE zur Übermittlung von Logmeldungen entweder syslog oder syslog-ng zur Verfügung. Für den Loghost
macht es keinen Unterschied, welches Netzwerkprotokoll die Clients nutzen.

Clients mit syslog

Konfiguration syslog.conf von Clients mit dem gebräuchlichen Syslog:

[shell]
local0.* @LOGHOST.HOSTNAME
local1.* @LOGHOST.HOSTNAME
local2.* @LOGHOST.HOSTNAME
local3.* @LOGHOST.HOSTNAME

[/shell]

Die Facilities local0, local2 werden verwendet für Errorlogs, während die Facilities local1 und local3 für Accesslogs genutzt
werden.

Clients mit syslog-ng

Hier muss die Default-syslog.conf um folgende Einträge modifi ziert werden.
Im oberen Optionsbereich:

[shell]
# Global options.
options {
long_hostnames(off);
time_reopen(10);
time_reap(360);
log_fifo_size(0);
sync(0);
perm(0640);
stats(3600);
log_msg_size(32768);
use_dns(no);
};

[/shell]

Innerhalb der üblichen Filter:

[shell]

filter f_apache { facility(local0, local1,local2,local3)
and level(info); };
filter f_local { facility(local0, local1, local2, local3,
local4, local5, local6, local7) and
not filter(f_apache);
};

[/shell]

Und neu am Ende:

[shell]

destination webserver { tcp(„IP.ADRESSE.LOGHOST“); };
log { source(src); filter(f _ apache); destination(webserver); };

[/shell]

Der Loghost

Mit folgender Konfi guration des syslog-ng sorgt man dafür, dass

  • Access-Logdateien auf täglicher Basis
  • Errorlog-Dateien auf monatlicher Basis

ausgeführt und in ein Logverzeichnis (/proj.stand/log/access und /proj.stand/log/errors/) geschrieben werden. Ein Aufräumskript sorgt für die Löschung der Logdateien nach einer gewissen Zeit.

Server-Konfiguration in /etc/syslog-ng/syslog.conf

Zunächst legt man im Verzeichniss /etc einen Ordner syslog-ng und darin eine Datei mit dem Namen syslog.conf an.

Hier definiert man dann die Quelle: Betrachtet werden Syslog-Messages, die sowohl über das User Datagram Protocol (UDP)
als auch über das Transmission Control Protocol (TCP) herein kommen.

[shell]

source net {
tcp(ip(„0.0.0.0“) port(514));
udp(ip(„0.0.0.0“) port(514));
};

[/shell]

Die folgenden Anweisungen definieren das Format der Messages wie sie gespeichert werden sollen:

[shell]

template t_accesslog {
template(„$MSGONLY\n“); template_escape(no);
};
template t_errorlog {
template(„$HOST $MSGONLY\n“); template_escape(no);
};
template t_hostactivity {
template(„$STAMP\t$HOST\t$PROGRAM\t$FACILITY\n“); template_escape(no);
};
destination d_apacheerror {
file(„/proj.stand/logs/errors/$MONTH/$PROGRAM.log“ template(t_errorlog));
};
destination d_apacheaccess {
file(„/proj.stand/logs/access/$MONTH/$DAY/$PROGRAM.log“ template(t_accesslog));
};
destination d_sn_apacheerror {
program(„/proj.stand/bin/dest-error-filter.pl $MONTH“ template(t_errorlog));
};
destination d_sn_apacheaccess {
program(„/proj.stand/bin/dest-access-filter.pl $MONTH $DAY“ template(t_accesslog));
};
destination d_hostactivity {
file(„/proj.stand/logs/hostactivity/activity-$YEAR-$MONTH-$DAY.log“ template(t_hostactivity));
};
destination netaccess {
file(„/proj.stand/logs/unknown/$FULLHOST.log“);
};

[/shell]

Zu beachten ist auch hier, dass bei den Error- und Accesslogs jeweils zwei Varianten betrachtet werden:
destination d_apacheaccess vs. destination d_sn_ apacheaccess und destination d_apacheerror vs. destination d_sn_apacheerror. Im ersten Fall speichert man die Logmessages direkt in eine Datei, im zweiten Fall werden sie an ein Programm gesendet.
Die beiden letzten Destinations dienen nur noch zu Kontrollzwecken. Mit der Destination d_hostactivity{} wird überprüft, welche Server konkret Apache-Anfragen senden. Auf diese Weise lässt sich beim Einsatz des Webclusters feststellen, ob die einzelnen Server überhaupt aktiv sind und wie häufi g sie es in Relation zu anderen Servern sind. Hierdurch kann man wieder Rückschlüsse auf das Load Balancing ziehen. Die Destination netaccess nutzt man hingegen, um solche Hosts abzufangen, die Nachrichten an den eigenen Loghost senden, sich aber nicht im erlaubten Subnetz IP.WEBSERVER.SUBNETZ.0 aufhalten.
Nun, da man den Speicherort der Zugriffe kennt, müssen noch die Filter defi niert werden:

[shell]

filter f_apacheerror {
facility(local0) and
level(info) and
netmask(IP.WEBSERVER.SUBNETZ.0/24);
};

filter f_apacheaccess {
facility(local1) and
level(info) and
netmask(IP.WEBSERVER.SUBNETZ.0/24);
};
filter f_sn_apacheerror {
facility(local2) and
level(info) and
netmask(IP.WEBSERVER.SUBNETZ.0/24);
};
filter f_sn_apacheaccess {
facility(local3) and
level(info) and
netmask(IP.WEBSERVER.SUBNETZ.0/24);
};

[/shell]

Die Filter defi nieren Bedingungen, die in der nachfolgenden Verarbeitung des syslog-Streams zum Zuge kommen: IP.WEBSERVER.SUBNETZ.0 enthält natürlich die IP-Adresse des Subnetzes in dem die eigenen Server stehen. Ist die jeweilige Filterbedingung erfüllt, wird die Message an die Destination weitergeleitet:

[shell]

log {
source(net);
filter(f_apacheerror);
destination(d_apacheerror);
destination(d_hostactivity);
flags(final);
};
log {
source(net);
filter(f_sn_apacheerror);
destination(d_sn_apacheerror);
destination(d_hostactivity);
flags(final);
};
log {
source(net);
filter(f_sn_apacheaccess);
destination(d_sn_apacheaccess);
destination(d_hostactivity);
flags(final);
};
log {
source(net);
filter(f_apacheaccess);
destination(d_apacheaccess);
destination(d_hostactivity);
flags(final);
};
log {
source(net);
destination(netaccess);
};

[/shell]

Damit ist die syslog-Konfi guration für den Loghost vollständig.
Was hat es eigentlich mit den Facilities auf sich? Wie bereits erwähnt, werden die Facilities local0, local2 für Errorlogs verwendet, die Facilities local1 und local3 für Accesslogs. Warum noch zusätzlich die umständliche Trennung in jeweils zwei Paare mit weiteren Facilities? Eigentlich sollten doch local0 und local1 ausreichen. So zumindest wird es in zahlreichen Dokumentation im Web beschrieben. Die Antwort wird deutlich, wenn man sich noch einmal das Anfangsszenario vor Augen führt − dieses Mal allerdings mit einer Apache-Webserver -Umgebung.

Eine eigene Access- und Errorlog definieren wir für jeden Virtual Host wie folgt:

[shell]

<virtualhost>

Errorlog „|/usr/bin/apache-error-logger.pl www.meine-domainname.tld“
TransferLog „|/usr/bin/apache-access-logger.pl www.meine-domainname.tld“

</virtualhost>

[/shell]

Was geschieht, wenn wir stattdessen einen Apache-Webserver verwenden, der über 600 Webauftritte verwaltet? Nichts wirklich Erfreuliches, denn beim Start des Apache werden allein für das Logging 1.200 Prozesse in Gang gesetzt, die während der Laufzeit des Apache aktiv sind! Damit wird deutlich, dass dieses Szenario für effektives Mass Virtual Hosting unbrauchbar ist und nach einer performance-schonenderen Lösung gesucht werden muss. Dennoch kann die beschriebene „Schmalspurvariante“ problemlos für „kleine“ Server mit einem oder nur wenigen Webauftritten genutzt werden.
Doch wie sieht nun die Lösung für die großen Zugriffsmengen aus? Man defi niert die Facilities local0 und local1, um auf diese Weise Logfiles zu sichern. Für Apache mit vielen zahlreichen Virtual Hosts nutzt man local2 und local3 und ändert die Logdirectiven in der Apache-Konfiguration.

Die Apache-Konfigurationen

Apache Konfiguration für Webserver mit Logdefi nition pro VHOST (wenige VHOSTs)

Die Logs werden in den einzelnen VHOST-Einträgen definiert.

[shell]

<virtualhost>

Errorlog „|/usr/bin/apache-error-logger.pl www.meine-domainname.tld“
TransferLog „|/usr/bin/apache-access-logger.pl www.meine-domainname.tld“
</virtualhost>

[/shell]

Vorher wird das Logformat wie folgt global festgelegt:

LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\""

Durch die Einträge werden beim Start des Apache-Servers zwei Prozesse mit den Skripten /usr/bin/apache-access -logger.pl und /usr/bin/apache-error-logger.pl aufgerufen.
Dabei handelt es sich um zwei simple Perlskripten.

apache-access-logger.pl

[perl]

#!/usr/bin/perl
use Sys::Syslog;
$SERVER _ NAME = shift || „www“;
$PRIORITY = „info“;
$FACILITY = „local1″;
openlog($SERVER _ NAME, ‚ndelay‘, $FACILITY);
while(<>) {
chomp;
if ($ _ =~ /^(\d+\.\d+\.\d+\.\d+) /i) {
$ _ =~ s/^(\d+\.\d+\.\d+)\.\d+ /$1.0 /gi;
# Anonymisieren der IP-Adresse.
# Nur die letzte Ziffer wird weggemacht
} else {
$ _ =~ s/^[a-z0-9\-\.]*\.([a-z0-9\-]+)\.([a-z0-9\-]+) /$1.$2 /gi;
# Anonymisieren des Hostnamens
# nur die letzten beiden Bestandteile werden uebernommen
}

# Folgende Zeile dient dazu Argumente wie Session IDs aus dem GET-
# Aufrufstring zu
# entfernen
$ _ =~ s/“GET ([^?]*)(\?[^“]+)“/“GET $1″/;
syslog($PRIORITY,$ _ );
}

closelog;

[/perl]

Es gibt eine rege Diskussion darüber, ob die IP-Adresse bzw. der Rechnername im Sinne des Datenschutzes erfasst werden darf oder nicht. Diese Diskussion soll jedoch hier nicht weiter ausgeführt werden. Im Falle einer IP-Adresse kürzt das obige Skript diese ab, indem die letzte Ziffer auf 0 gesetzt wird. Falls dies weitergehenden Fordungen nicht genüge tut, kann die Regular Expression auch wie folgt geändert werden:

$ _ =~ s/^(\d+\.\d+)\.\d+\.\d+ /$1.0.0 /gi;

Falls ein Rechnername angegeben ist, wird die IP-Adresse bis auf die Subdomain gekürzt.

apache-error-logger.pl

[perl]

#!/usr/bin/perl
use Sys::Syslog;
$SERVER _ NAME = shift || „www“;
$PRIORITY = „info“;
$FACILITY = „local0“;
# Sys::Syslog::setlogsock(„unix“);
openlog($SERVER _ NAME, ‚ndelay‘, $FACILITY);
while(<>) {
chomp;
syslog($PRIORITY,$ _ );
}

closelog;

[/perl]

Bei auftretenden Fehlern anonymisiert man die Rechneradressen nicht! Dann ist es notwendig alle Daten der Requests zu kennen. Auch die vollständige IP-Adresse. Fehler sollten jedoch garnicht erst auftauchen.

Apache Konfi guration für Webserver mit globaler Logdefinition (viele VHOSTs)

In dieser Konfi guration enthalten die einzelnen VHOSTs keine eigenen Logdirectiven. (Es lassen sich aber durchaus auch beide Konfi gurationsverfahren mischen. Das macht zum Beispiel dann Sinn, wenn man für einzelne VHOSTS eine eigene Errorlog benötigt). Der Aufruf im Apache erfolgt über die globale Defi nition in solcher Form:
[shell]

ErrorLog „|/usr/bin/apache-vhosterror-logger.pl MEIN _ HOSTNAME“
LogFormat „%v %h %l %u %t \“%r\“ %>s %b \“%{Referer}i\“ \“%{User-Agent}i\““ vhost _ combined

CustomLog „|/usr/bin/apache-vhostaccess-logger.pl MEIN _ HOSTNAME“ vhost _ combined
[/shell]

Auch hier wurden zwei Perlskripten erstellt. Diese unterschieden sich von den beiden anderen nur dadurch, dass man hier auf die Anonymisierung verzichtet und andere Facilities angibt. (Die Anonymisierung erfolgt auf der Loghost-Seite, damit man auf der Seite der Webserver etwas Performance spart; selbst wenn es sich nur um Mikrosekunden handelt.)

apache-vhostaccess-logger.pl

[perl]

#!/usr/bin/perl
use Sys::Syslog;
$SERVER _ NAME = shift || „www“;
$PRIORITY = „info“;
$FACILITY = „local3“;

openlog($SERVER _ NAME, ‚ndelay‘, $FACILITY);
while(<>) {
chomp;
syslog($PRIORITY,$ _ );
}

closelog;

[/perl]

apache-vhosterror-logger.pl

[perl]

#!/usr/bin/perl
use Sys::Syslog;
$SERVER _ NAME = shift || „www“;
$PRIORITY = „info“;
$FACILITY = „local2“;
openlog($SERVER _ NAME, ‚ndelay‘, $FACILITY);
while(<>) {
chomp;
syslog($PRIORITY,$ _ );
}

closelog;

[/perl]

Filter-Skripte auf Loghost für Apache Mass-VHosting

Auf dem Loghost müssen nun noch die Filterskripte defi niert werden, die alle Nachrichten der Webserver erhalten.
Da syslog-ng keine Ergebnisse von RegExps in dessen Filtern als Variable weiterverarbeiten kann, wird als “Destination Driver” program() verwendet: “program() Forks and launches the specifi ed program, and sends messages to its standard input.”
Diese Nachrichten enthalten im Falle der Accesslogs dank der Logfrmat-Angabe in der ersten Spalte den Namen des Webauftritts.
Danach wird gefiltert.

dest-access-filter.pl

[perl]

#!/usr/bin/perl
use IO::Handle;
$TARGET _ DIR = „/proj.stand/logs/access/“;
$MONTH = shift;
$DAY = shift;
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
$mon++;
$MONTH = $mon if (not $MONTH);
$DAY = $mday if (not $DAY);
$SIG{ALRM} = sub { &doTimeout; };
alarm(60*60);
$daydir = $TARGET _ DIR.$MONTH.“/“.$DAY;
$monthdir = $TARGET _ DIR.$MONTH;
if (not (-d $monthdir)) {
mkdir($monthdir);
}

if (not (-d $daydir)) {
mkdir($daydir);
}
while(<>) {
chomp;

# Bei Servern auf Basis von SUN wird u.a. durch die MSGId eine Spalte vor den   Syslogeintragen gestellt
# Diese kann hier weggefiltert werden:
$ _ =~ s/^\[ID ([^\]]*)\] //gi;
($vhost,$userhost,$rest) = split(/\s+/,$ _ ,3);
if ($userhost =~ /^(\d+\.\d+\.\d+\.\d+)$/i) {
$userhost =~ s/^(\d+\.\d+\.\d+)\.\d+$/$1.0/gi;
# Anonymisieren der IP-Adresse.
# Nur die letzte Ziffer wird weggemacht
} else {
$userhost =~ s/^[a-z0-9\-\.]*\.([a-z0-9\-]+)\.([a-z0-9\-]+)$/$1.$2/gi;
# Anonymisieren des Hostnamens
# nur die letzten beiden Bestandteile werden uebernommen
}
$vhost = „unknown“ if (not $vhost);
$target = $daydir.“/“.$vhost.“.log“;
open(my ($fh),“>>$target“);
$fh->autoflush(1);
print $fh „$userhost $rest\n“;
$mday = (localtime(time))[3];
if ($mday != $DAY) {
exit;
}
}

exit;
sub doTimeout {
exit;
}

[/perl]

Zu Erwähnen sind die Timeouts und die Prüfung auf den Tageswechsel. Ebenso wie der Apache startet syslog den Filterprozess einmal mit den beim Start geltenden Argumenten.Wird der Prozess nicht gestoppt, läuft er immer weiter – auch über den Tageswechsel hinaus. Logmeldungen werden dann also ggf. in die falsche Datei gespeichert. Aus diesem Grund und um zu viele offene Filehandles im Memory zu vermeiden, lässt man das Skript sich in regelmäßigen Abständen und zum Tageswechsel selbst beenden.

syslog-ng wird dann den Prozess selbständig neu starten – mit jeweils aktuellen Parametern.

dest-error-filter.pl

[perl]

use IO::Handle;
$TARGET _ DIR = „/proj.stand/logs/errors/“;
$MONTH = shift;
my $mon = (localtime(time))[4];
$mon++;
$MONTH = $mon if (not $MONTH);
$SIG{ALRM} = sub { &doTimeout; };
alarm(60*60);
# Das Skript soll sich einmal pro Stunde neu starten
$monthdir = $TARGET _ DIR.$MONTH;
if (not (-d $monthdir)) {
mkdir($monthdir);
}
while(<>) {
chomp;
$orig = $ _ ;
if ($ _ !~ /^\[/i) {
# Quellhost ist vor der Zeit angegeben
# Siehe Einstellung in syslog-ng: template()
($quellhost,$rest) = split(/\s+/,$ _ ,2);
} else {
$rest = $ _ ;

$quellhost = „unknown“;
}
if ($orig =~ /\(server ([a-z0-9\-\.]+):*(\d*)\)/i) {
$vhost = $1;
} else {
$vhost = „“;
}
if ($vhost) {
$target = $monthdir.“/“.$vhost.“.log“;
} else {
$target = $monthdir.“/“.$quellhost.“.log“;
}
local $fh;
if (not $tlist->{$target}->{‚fh‘}) {
open($fh,“>>$target“);
$fh->autofl ush(1);
$tlist->{$target}->{‚fh‘} = $fh;
} else {
$fh = $tlist->{$target}->{‚fh‘};
}
if ($SHOW _ SOURCEHOST) {
print $fh „$orig\n“;
} else {
print $fh „$rest\n“;
}
$mday = (localtime(time))[3];
if ($mday != $DAY) {
exit;
}
}

exit;
sub doTimeout { exit; }

[/perl]

Wie auch das Skript für die Accesslogs enthält der Filter für die Errorlogs ein Timeout und ein Exit beim Tageswechsel.