Wer hat schon Zeit, täglich seine Logdateien zu kontrollieren? Das heute vorgestellte Skript fasst alle aktuellen Veränderungen in einem Report zusammen und verschickt sie einmal am Tag per Email.
Logdateien protokollieren mit, was so abläuft. Sie halten wichtige Ereignisse oder Fehler fest und helfen, eventuell aufgetretene Fehler einzukreisen und zu beheben. Beim Überwachen (``Monitoring'') aktiver Prozesse helfen die angesammelten Daten allerdings nur, falls sich jemand aktiv um sie kümmert und täglich kontrolliert, ob noch alles ordnungsgemäß läuft. Auch tendieren Logdateien dazu, sehr schnell zu wachsen, so dass wir einen Mechanismus brauchen, der nur seit dem letzten Prüftermin hinzugekommene Daten analysiert.
Auf perlmeister.com
gibt es beispielsweise die Datei rbsub.txt
,
die festhält, welche Leute sich auf die Liste für die Amerika-Rundbriefe
setzen ließen -- immer wenn jemand das entsprechende Webformular
ausgefüllt hat, wächst rbsub.txt
um eine Zeile, die die eingetragene
Email-Adresse enthält. Außerdem protokolliert die Datei errors.txt
,
ob irgendwo auf dem Webserver Fehler aufgetreten sind.
Das heute vorgestellte Skript logmail.pl
läuft einmal am Tag per
Cronjob, merkt sich die aktuelle Größe der Logdateien, stellt
hinzugekommene Zeilen fest, fasst sie in einem Report nach Abbildung 1
zusammen und verschickt sie per Email an mich. So sehe ich beim
täglichen Email-Lesen, ob noch alles in Ordnung ist.
Abbildung 1: Die Email mit den Logdaten ist da! |
Listing logmail.pl
zeigt die Implementierung. Die
Konfigurationssektion am Anfang legt eine Reihe von Parametern fest:
In $DATABASE
steht, wie die Datei heißt, in der logmail.pl
sich
merkt, wie weit es in den einzelnen Dateien schon gelesen hat.
Die Werte
für $FROM
, @TO
und $SUBJECT
geben für die später zu
verschickende Email an, woher sie stammt, wohin sie geht und was in der
Betreffs-Zeile steht. $MAXLINES
legt fest, wieviele Zeilen pro
Logdatei maximal vollständig im Email-Report stehen. Der letzte
Parameter $B
gibt nur ein Verzeichnis an, in dem die Dateien stehen,
damit wir es später nicht hundertmal tippen müssen.
In Zeile 10 steht ein Array, der Logdateipfaden aussagekräftige
Namen zuordnet. Die geraden Elemente des Arrays enthalten die Pfadnamen,
die ungeraden die Alias-Namen.
Zeile 17 deklariert den Hash %offs
, der später unter dem
Pfadnamen der Logdatei jeweils abspeichert, wieviele Bytes logmail.pl
beim letzten Aufruf schon aus der Datei
ausgelesen hat und dementsprechend im
aktuellen Durchlauf überspringen kann. Die Zeilen 14 und 15 ziehen die
Module Mail::Mailer
und Storable
herein, ersteres zum Verschicken
von Email und letzteres zum Speichern von Daten, die die Laufzeit von
logmail.pl
in einer Datei überleben und beim nächsten Aufruf wieder
bereit stehen. Beide kommen frisch vom CPAN und werden wie üblich mit
perl -MCPAN -eshell cpan> install Storable cpan> install Mail::Mailer
installiert.
Storable
Während man Hashes sonst mit tie
und DBM-Dateien persistent
macht, wollen wir heute mal Storable
verwenden, das sich durch
ein bestechend klares Interface auszeichnet: store()
friert
Daten in einer angegebenen Datei ein und restore()
holt sie
daraus wieder zurück -- einfacher geht's nicht.
Beide Funktionen arbeiten mit einer Referenz auf die beliebig
komplexe zu sichernde Datenstruktur. Einen einfachen
Hash legt folgender Code in der in der Datei datei.dat
ab:
use Storable;
$offs = { "rbsub.txt" => 257, "errors.txt" => 12 };
# Abspeichern store($offs, "datei.dat");
$offs
ist eine Referenz auf einen anonymen Hash, der mittels des
{}
-Konstrukts entstand. retrieve()
holt die Datenstruktur
wieder aus der Versenkung:
use Storable;
# Zurückholen $offs = retrieve("datei.dat");
# => 257 print "$offs->{rbsub.txt}\n"; # => 12 print "$offs->{errors.txt}\n";
Sowohl store()
als auch retrieve()
liefern undef
zurück,
falls etwas schiefgeht, weswegen die Zeilen 21 und 76 in logmail.pl
ordnungsgemäß den Rückgabewert prüfen und notfalls das
Programm abbrechen.
Die while()
-Schleife ab Zeile 27 holt paarweise Werte aus
dem Array @FILES
. Der erste Wert ist, wie in
Zeile 10 festgelegt, immer der beschreibende Text und der zweite
Wert der Pfad zur jeweiligen Logdatei.
In der Variablen $mailtext
wird nach Abschluss der Loganalyse
der fertige Report stehen. Die Zeilen 30 bis 32 schreiben eine
Überschrift für die aktuell analysierte Datei, eingerahmt von
zwei Zeilen, in denen jeweils 70 Mal das Zeichen #
steht,
wie in Abbildung 1 gezeigt.
Zeile 35 findet heraus, wieviele Zeichen der Datei bereits während
früherer Aufrufe von logmail.pl
analysiert wurden -- die werden
anschließend übersprungen, denn schließlich soll logmail.pl
nicht jedes Mal bei Adam und Eva anfangen. Existiert ein
Eintrag im persistenten Hash zur gerade analysierten Logdatei,
steht $offset
nach Zeile 35 auf dem gespeicherten Wert. Andernfalls
setzt es die ||
-Verknüpfung auf 0
.
Zeile 37 öffnet schließlich die Logdatei zum Lesen. Die nachfolgende
if
-Bedingung prüft, ob der gespeicherte Offset zum Überspringen
von Daten überhaupt sinnvoll ist -- ist er größer als die Datei
selbst (was sich leicht mit -s
herausfinden lässt), wurde
offensichtlich die Datei manuell verkürzt und statt eines Reports
schreibt logmail.pl
eine Warnungsmeldung nach $mailtext
.
Ist der Offset sinnvoll, springt Zeile 45 die angegebene Anzahl
von Bytes mittels der seek()
-Funktion nach vorne und Zeile
48 liest die dann folgenden Zeilen bis zum Zeilenende auf einen
Rutsch in den Array @data
. $noflines
ist die Anzahl der
Elemente in @data
, also die Anzahl der neuen Zeilen seit
dem letzten Aufruf von logmail.pl
.
Zeile 52 stellt den persistenten Offset auf das Dateiende ein,
auf das das Filehandle FILE
zu diesem Zeitpunkt zeigt und dessen
Offset vom Dateianfang deswegen die tell()
-Funktion zurückgibt.
Die auszusendende Email soll freilich nicht mit tausenden von
Logzeilen vollgekleistert sein, deswegen wird die Anzahl der
auszugebenden Zeilen auf $MAXLINES
begrenzt. Hierzu schneidet
die splice()
-Funktion in Zeile 56 den Mittelteil aus @data
heraus, so dass am linken und am rechten Ende jeweils genau
$MAXLINES/2
Elemente übrigbleiben und ersetzt sie durch
ein Element, das mit drei Punkten die herausgeschnittenen Elemente
symbolisieren soll.
Zeile 59 fasst die Einzelzeilen zu einem einzigen
langen Textstring zusammen. Zeile 62 sorgt dafür, dass
logmail.pl
die Anzahl der neuen Zeilen in der Logdatei korrekt
in Einzahl oder Mehrzahl anzeigt: 0 lines, 1 line, 2 lines.
Die gesammelten Logdaten hängt Zeile 66 an den zu verschickenden
Mailtext an und Zeile 67 schließt die Logdatei.
Nachdem alle Dateien aus @FILES
abgearbeitet wurden, schickt
Zeile 74 mit der weiter unten definierten mailto()
-Funktion
den Report in $mailtext
an die in der Parametersektion definierte
Adresse. Die store()
-Funktion in Zeile 76 schreibt die unter
der Referenz $offs
stehende Datenstruktur mit den Offsets der einzelnen
Logdateien zurück auf die Festplatte.
Die ab Zeile 80 definierte Funktion mailto
schickt eine Email
mit dem angegebenen Text an alle Adressaten, die der Aufrufer
in der Parameterliste übergeben hat. Das Zusatzmodul
Mail::Mailer
nimmt uns dabei die ganze schwierige Arbeit
ab, wir müssen nur ein Objekt vom Typ Mail::Mailer
erzeugen
und dessen open()
-Methode aufrufen. Diese nimmt unter den
Einträgen "To"
, "From"
, "Subject"
und "Reply-To"
den/die Adressaten, den Absender,
die Betreffszeile und den Wert für den Reply-Header
der Email
entgegen. Zeile 95 verwendet das Mailer-Objekt daraufhin wie
ein Filehandle und nutzt die print
-Funktion, um den übergebenen
Nachrichtentext einzuspeisen. Die in Zeile 96 aufgerufene
close()
-Methode schickt die Mail ab.
Das in Zeile 86 erzeugte Mail::Mailer
-Objekt wurde auf den
sendmail
-Dämon zugeschnitten -- dies funktioniert, falls auf
dem System, auf dem das Skript läuft, ein korrekt konfigurierter
Sendmail-Dämon aktiv ist. Als weitere Möglichkeit kann man
einfach 'mail'
sagen, worauf der Mailer das Unix-eigene
mail
-Kommando für seine Zwecke nutzt. Auch 'smtp' geht, dann
kontaktiert der Mailer einen extra angegebenen Mailserver direkt
per SMTP
-Protokoll. Einzelheiten stehen in der Manualseite,
die mit Mail::Mailer
kommt.
Bleibt nur noch, die Parametersektion des Skripts an die lokalen Gegebenheiten anzupassen: Die Mailadressen, die Betreffszeile und die Anzahl der maximal gedruckten Einträge und die Pfade zur Merkerdatei sowie der überwachten Logdateien samt ihrer Alias-Namen sollte jeder nach Bedarf einstellen, bevor das Skript zum ersten Mal abläuft. Klappt alles, hilft ein Cronjob der Art
00 0 * * * /data/bin/logmail.pl
das Skript jeden Tag um Mitternacht zu starten und den Report auf den Weg zu schicken. Bis zum nächsten Mal, kontrolliert fleißig, was so abgeht!
01 #!/usr/bin/perl -w 02 03 my $FROM = 'absender@irgendwo.com'; 04 my @TO = qw(logs@perlmeister.com); 05 my $SUBJECT = 'Today\'s Logs'; 06 my $MAXLINES = 10; 07 my $DATABASE = '/home/mschilli/tmp/logmail.dat'; 08 my $B = '/home/mschilli/tmp'; 09 10 my @FILES = ("Neukunden" => "$B/rbsub.txt", 11 "Fehler" => "$B/errors.txt", 12 ); 13 use strict; 14 use Mail::Mailer; 15 use Storable; 16 17 my $offs = {}; 18 19 if(-r $DATABASE) { 20 # Gespeicherte Daten auslesen 21 $offs = retrieve($DATABASE) or 22 die "Cannot open $DATABASE"; 23 } 24 25 my $mailtext = ""; 26 27 while(my ($text, $file) = splice(@FILES, 0, 2)) { 28 29 # Teilüberschrift 30 $mailtext .= "\n" . "#" x 70; 31 $mailtext .= "\n$text:\n"; 32 $mailtext .= "#" x 70 . "\n"; 33 34 # Gespeicherter Offset vom letzten Mal 35 my $offset = ($offs->{$file} || 0); 36 37 if(open(FILE, "<$file")) { 38 if(-s $file < $offset) { 39 # Datei wurde gekürzt 40 $offs->{$file} = -s $file; 41 $mailtext .= "(Offset adjusted)\n"; 42 } else { 43 # Bis zum gespeicherten Offset 44 # vorfahren 45 seek(FILE, $offset, 0); 46 47 # Rest zeilenweise auslesen 48 my @data = <FILE>; 49 my $noflines = @data; 50 51 # Neuen Offset einstellen 52 $offs->{$file} = tell(FILE); 53 54 # Kürzen falls > $MAXLINES 55 if(@data > $MAXLINES) { 56 splice(@data, $MAXLINES/2, 57 @data-$MAXLINES, "...\n"); 58 } 59 my $data = join '', @data; 60 61 # Zeilenzahl ausgeben 62 $mailtext .= sprintf "(%d line%s)\n", 63 $noflines, 64 $noflines == 1 ? "" : "s"; 65 # Logdaten ausgeben 66 $mailtext .= $data; 67 close(FILE); 68 } 69 } else { 70 $mailtext .= "Cannot open $file\n"; 71 } 72 } 73 74 mailto($SUBJECT, $mailtext, $FROM, @TO); 75 76 store($offs, $DATABASE) or 77 die "Cannot store data in $DATABASE"; 78 79 ################################################## 80 sub mailto { 81 ################################################## 82 my ($subj, $text, $from, @to) = @_; 83 84 return if @to == 0; 85 86 my $mailer = Mail::Mailer->new( 'sendmail' ); 87 my $to = join ", ", @to; 88 89 $mailer->open( { "To" => $to, 90 "From" => $from, 91 "Subject" => $subj, 92 "Reply-To" => $from, 93 } ) or die "open failed"; 94 95 print $mailer $text; 96 $mailer->close(); 97 }
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |