Aus Linux-Magazin 06/2001

Logfiles effektiv überwachen

Die E-Mail mit den Logdaten ist da!

Wer hat schon Zeit, täglich seine Logdateien zu kontrollieren? Das im Folgenden vorgestellte Skript fasst alle aktuellen Veränderungen in einem Report zusammen und verschickt sie einmal täglich per E-Mail.

Logdateien halten wichtige Ereignisse fest und helfen dabei, auftretende Fehler einzukreisen. Beim Überwachen aktiver Prozesse nützen die gesammelten Daten aber nur, wenn sich jemand aktiv um sie kümmert und täglich kontrolliert, ob noch alles ordnungsgemäß läuft. Logdateien wachsen sehr schnell, so dass wir einen Mechanismus brauchen, der nur die seit dem letzten Prüftermin hinzugekommenen Daten analysiert.

Auf Perlmeister.com gibt es beispielsweise die Datei rbsub.txt, die festhält, welche Leute sich auf die Liste für Amerika-Rundbriefe setzen ließen: Wenn jemand das entsprechende Web-Formular ausgefüllt hat, wächst rbsub.txt um eine Zeile, die die eingetragene E-Mail-Adresse enthält. Zusätzlich protokolliert die Datei errors .txt, ob irgendwo auf dem Webserver Fehler aufgetreten sind.

Tägliche Kontrolle per Skript

Das Skript logmail.pl läuft einmal täglich per Cronjob, merkt sich die aktuelle Größe der Logdateien, stellt hinzugekommene Zeilen fest, fasst sie in einem Report zusammen und verschickt ihn per E-Mail an mich. So sehe ich beim täglichen E-Mail-Lesen, ob noch alles in Ordnung ist.

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 das Skript sich merkt, welche Dateien es schon gelesen hat.

Die E-Mail mit den Logdaten ist da!

Die E-Mail mit den Logdaten ist da!

Die Werte für $FROM, @TO und $SUBJECT geben für die später zu verschickende E-Mail an, woher sie stammt, wohin sie geht und was in der Betreffzeile steht. $MAXLINES legt fest, wie viele Zeilen pro Logdatei maximal in den E-Mail-Report kommen. Der letzte Parameter $B gibt ein Verzeichnis an, in dem die Dateien stehen, damit wir es später nicht hundertmal tippen müssen.

In Zeile 10 steht ein Array, das Logdateipfaden aussagekräftige Namen zuordnet. Die geraden Elemente des Arrays enthalten die Pfadnamen, die ungeraden die Aliasnamen. Zeile 17 deklariert den Hash %offs, der jeweils unter dem Pfadnamen der Logdatei abspeichert, wie viele Bytes logmail .pl beim letzten Aufruf schon aus der Datei ausgelesen hat, also im aktuellen Durchlauf überspringen kann.

Die Zeilen 14 und 15 holen die Module Mail::Mailer und Storable, das erste zum Verschicken von E-Mail, das zweite zum Speichern von Daten, die die Laufzeit von logmail.pl in einer Datei überleben und beim nächsten Aufruf wieder bereitstehen. Beide kommen frisch vom CPAN und werden wie üblich installiert:

perl -MCPAN -eshell
cpan> install Storable
cpan> install Mail::Mailer

Speichern und Laden mit Storable

Während man Hashes sonst mit tie und DBM-Dateien persistent macht, wollen wir jetzt mal Storable verwenden, das sich durch ein bestechend klares Interface auszeichnet: store() friert Daten in einer angegebenen Datei ein und restore() holt sie wieder zurück – einfacher geht’s nicht.

Beide Funktionen arbeiten mit einer Referenz auf die beliebig komplexe Datenstruktur. Folgender Code legt einen einfachen Hash 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 im {}-Konstrukt 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 schief geht, weshalb 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 @FILES– Array. Der erste Wert ist, wie in Zeile 10 festgelegt, immer der beschreibende Text, der zweite Wert ist der Pfad zur jeweiligen Logdatei. In der Variablen $mailtext steht nach Abschluss der Loganalyse der fertige Report.

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. Zeile 35 findet heraus, wie viele Zeichen der Datei bereits während früherer Aufrufe von logmail.pl analysiert wurden. Die werden übersprungen, denn schließlich soll logmail.pl nicht jedes Mal bei Adam und Eva anfangen. Existiert im persistenten Hash bereits ein Eintrag zur gerade analysierten Logdatei, dann 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 überhaupt sinnvoll ist. Ist er größer als die Datei selbst (was sich leicht mit -s herausfinden lässt), wurde die Datei offensichtlich manuell verkürzt und logmail.pl schreibt statt eines Reports eine Warnmeldung nach $mailtext.

Entspricht der Offset den Erwartungen, springt Zeile 45 die angegebene Anzahl von Bytes mittels der seek()-Funktion nach vorn. Zeile 48 liest die dann folgenden Zeilen bis zum Zeilenende in den Array @data. $noflines ist die Anzahl der Elemente in @data, also die der neuen Zeilen seit dem letzten Aufruf von logmail.pl. Zeile 52 stellt den persistenten Offset auf jenes Dateiende ein, auf das das Filehandle FILE gerade zeigt und dessen Offset vom Dateianfang daher die tell()-Funktion zurückgibt.

Listing: logmail.pl

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     my $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 = ;
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 $filen";
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 }

Die fertige E-Mail soll nicht mit Logzeilen vollgestopft sein, also wird die Anzahl der auszugebenden Zeilen auf $MAXLINES begrenzt. Dazu schneidet die splice()-Funktion in Zeile 56 den Mittelteil aus @data heraus, so dass am linken und am rechten Ende genau je $MAXLINES/2 Elemente übrig bleiben. Den mittleren Teil ersetzt eine so genannte Ellipse.

Zeile 59 fasst die einzelnen Zeilen zu einem langen Textstring zusammen. Zeile 62 sorgt dafür, dass logmail.pl die Anzahl der neuen Zeilen in der Logdatei korrekt (Einzahl oder Mehrzahl) anzeigt: 0 lines, 1 line, 2 lines. Zeile 66 hängt die gesammelten Logdaten an den Mailtext, Zeile 67 schließt die Logdatei.

Sind alle Dateien aus @FILES abgearbeitet, schickt Zeile 74 den Report mit der mailto()-Funktion (siehe folgenden Abschnitt) an die in der Parametersektion stehende Adresse. Die store()-Funktion in Zeile 76 schreibt die unter der Referenz $offs stehende Datenstruktur mit den Offsets der Logdateien zurück auf die Festplatte.

E-Mailen aus Perl

Die ab Zeile 80 definierte Funktion mailto schickt eine E-Mail mit dem angegebenen Text an alle Adressaten, die der Aufrufer in der Parameterliste übergeben hat. Das Zusatzmodul Mail::Mailer übernimmt dabei die schwierige Arbeit – es ist dann nur noch ein Objekt vom Typ Mail::Mailer zu erzeugen und dessen open()-Methode aufzurufen.

Diese Methode nimmt unter den Einträgen To, From, Subject und Reply-To den beziehungsweise die Adressaten, den Absender, die Betreffzeile und den Wert für den Reply-Header der E-Mail 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. Es funktioniert also nur, wenn auf dem System, auf dem das Skript läuft, auch ein korrekt konfigurierter Sendmail-Dämon aktiv ist. Als weitere Möglichkeit kann man aber auch einfach mail sagen, woraufhin der Mailer das Unix-eigene mail-Kommando für seine Zwecke nutzt. Auch smtp geht, dann kontaktiert der Mailer einen zusätzlich angegebenen Mailserver direkt per SMTP-Protokoll. Einzelheiten stehen in der Manualseite Mail::Mailer.

Jetzt bleibt nur noch übrig, die Parametersektion des Skripts an die lokalen Gegebenheiten anzupassen: Die Mailadressen, die Betreffzeile, die Anzahl der maximal zu druckenden Einträge, die Pfade zur Merkerdatei sowie zu den überwachten Logdateien samt ihren Aliasnamen sollte jeder nach Bedarf einstellen, bevor das Skript zum ersten Mal abläuft. Klappt alles, hilft ein Cronjob dabei, das Skript jeden Tag um Mitternacht zu starten und den Report auf den Weg zu schicken.

00 0 * * * /data/bin/logmail.pl

Bis zum nächsten Mal – und kontrolliert fleißig, was so abgeht! ( tfr)

Der Autor

Michael Schilli arbeitet als Web-Engineer für AOL/Netscape in Mountain View, Kalifornien. Er hat die Bücher “Goto Perl 5” (deutsch) und “Perl Power” (englisch) für Addison-Wesley geschrieben und arbeitet gerade an dem neuen Buch “Perl lernen” für Anfänger. Er ist unter mschilli@perlmeister.com zu erreichen, seine Homepage: http://perlmeister.com

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben