tail -f

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
PNS-Richi
User
Beiträge: 68
Registriert: Donnerstag 17. Januar 2008, 01:48

Hallo,

ich möchte etwas ähnliches wie "tail -f" schreiben. Im Grunde möchte ich logfiles realtime überwachen.

Lösung 1:
Ich lese die gewünschten Dateien in einer Endlosschleife ein und immer wenn das Zeilenende(bzw. "") erreicht wurde, wartet das "Programm" 0.1 Sekunden(um das System nicht unnötig zu belasten) und liest danach weiter. Logrotate könnte hier ein Problem werden.

Lösung 2:
Ich nehme die FUSE Bindings und schreib mir ein Filesystem das diese Aufgabe für mich erledigt. Ich nehme an das es Performanter ist - was die I/O Last angeht, oder liege ich mit dieser Annahme falsch?

Im Grunde sollen die neuen Logfile Einträge von weiteren Programmen ausgewertet werden.

Kennt ihr noch eine andere Möglichkeit?

lg Richi
lunar

Wieso sollte ein Dateisystem im Userspace "performanter" sein als ein normales Programm im Userspace?

Was die Implementierung von "tail -f" angeht … was liegt näher, als den Quelltext von "tail" zu lesen? Außerdem bist Du bestimmt nicht der erste, der sowas schreiben möchte, such halt mal im Netz.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

http://paste.pocoo.org/show/141167/

Das Ganze hat allerdings Probleme, wenn die Datei während der Programmausführung weniger Zeilen bekommt, da dann die Cursorposition nicht mehr stimmt, um eine neue Zeile zu lesen. Ich hab da jetzt noch keinen Mechanismus für eingebaut.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Ungetestet:

Code: Alles auswählen

import itertools

def follow_output(stream):
    def spam():
        while True:
            pos = stream.tell()
            line = stream.readline()
            if not line:
                sleep(UPDATE_SEC)
                stream.seek(pos)
            else:
                yield line

    start = -abs(LAST_LINES)

    return itertools.chain(itertools.islice(stream, start, None), spam())
Edit: "start" übersehen.
Das Leben ist wie ein Tennisball.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

EyDu hat geschrieben:Ungetestet

Code: Alles auswählen

ValueError: Indices for islice() must be non-negative integers or None.
Wobei der Ansatz natürlich sauberer ist (wenn er klappen würde). Mit den itertools bin ich noch nicht so vertraut...
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Stimmt, mit negativen Indizes kann das natürlich nicht funktionieren.

Code: Alles auswählen

def follow_output(stream):
    def spam():
        while True:
            pos = stream.tell()
            line = stream.readline()
            if not line:
                sleep(UPDATE_SEC)
                stream.seek(pos)
            else:
                yield line

    if LAST_LINES > 0:
        data = stream.readlines()[LAST_LINES:]
    else:
        data = stream

    return itertools.chain(data, spam())
Je nach Menge der Datenkann man natürlich auch folgendes nutzen:

Code: Alles auswählen

def follow_output(stream):
    def spam():
        while True:
            pos = stream.tell()
            line = stream.readline()
            if not line:
                sleep(UPDATE_SEC)
                stream.seek(pos)
            else:
                yield line

    return itertools.chain(stream.readlines()[-LAST_LINES:], spam())
Beim letzten nehme ich mal an, dass LAST_LINES nicht negativ werden darf. Sonst könnte man den index als "index = min(0, -LAST_LINES)" bestimmen, im Gegensatz zu meinem vorherigen Post.
Das Leben ist wie ein Tennisball.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Ich war mal so frei, die abgeänderte Variante nochmal hochzuladen: http://paste.pocoo.org/show/141185/

Hm, `data` sollte wohl besser `start` heißen, aber das ist ja jetzt egal. :twisted:

EDIT:

Code: Alles auswählen

tail = min(0, -LAST_LINES)
start = stream.readlines()[tail:]
[...]
return itertools.chain(start, yield_line())
Wenn `LAST_LINES` negativ ist, wird `tail` auf 0 gesetzt und ein Slice mittels `[0:]` gibt die komplette Liste zurück. :)

Den Nachteil, dass man bei großen Dateien auch einen großen Satz `lines` im Puffer hätte, übergehe ich jetzt mal. :D
PNS-Richi
User
Beiträge: 68
Registriert: Donnerstag 17. Januar 2008, 01:48

@lunar:

Ich hab selber funktionstüchtigen Code und auch noch einige Beispiele über Google gefunden. Doch all diese Beispiele sind sich sehr ähnlich, genau so wie bei meinem wird die Datei im Sekunden Intervall überprüft, ob sich etwas geändert hat. Die Größe muss man auch immer im Auge behalten, wegen logroate.

Deshalb wollte ich wissen ob es Performantere Lösungen gibt. Bei FUSE kümmert sich dann Python um das schreiben der Datei, das heißt ich kann schon mitlesen, was geloggt wird, ohne die Datei einlesen zu müssen bzw. die Datei zum lesen zu öffnen und bei vielen Log Dateien macht das schon sinn.

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import time
import os, sys

PATH = "path_to_test.log"
TIME_SLEEP = 1

def main():
	file = open(PATH, "r")
	
	pos = 0
	
	while 1:
		size = os.path.getsize(PATH)
		if size >= pos:
			pos = file.tell()

			line = file.readline()
			if not line:
				time.sleep(TIME_SLEEP)
				file.seek(pos)
			else:
				print line,
		else:
			print "file got truncated"
			pos = 0
			file.seek(pos)

if __name__ == '__main__':
	main()
Ein simples Beispiel. Für Log Dateien reicht es, jedoch sollte man eine Fehlerbehandlung einbauen. Immerhin könnte während eines logrotates ein Fehler bezüglich nicht vorhandener Datei kommen. Würde man diese Datei nun mit einem Editor wie vim editieren und Zeilen raus löschen und/oder hinzufügen(irgendwo mitten drinnen), dann würde dieses Beispiel auch nicht mehr funktionieren. Der Cursor würde richtig positioniert werden, jedoch würde immer ein hauch von nichts zurückkommen. Genauso könnte beim Speichervorgang ein Fehler auftreten das die Datei nicht mehr vorhanden währe. Warscheinlich ändert sich der Inode? Man müsste dann die Datei wohl neu öffnen.

lg Richi
Zuletzt geändert von PNS-Richi am Donnerstag 24. September 2009, 09:30, insgesamt 1-mal geändert.
lunar

@PNS-Richi: Auf der anderen Seite Fuse schickt dann alle Schreibzugriffe unter entsprechend vielen Kontextwechseln durch ein im Vergleich zu einem C-Dateisystem relativ langsames Python-Programm. Letztlich erhöhst Du damit die Effizienz der Überwachung auf Kosten der Effizienz beim Schreibzugriff. Außerdem musst Du dann parallel auch woanders hin loggen, schließlich könnte sonst ein Ausnahme an der falschen Stelle wichtige Logeinträge ins Nirvana schicken.

Ist es Dir denn möglich, Logeinträge direkt in eine Datenbank zu schreiben? Mit entsprechenden Triggern sollte es dann doch recht effizient möglich sein, die Dateien annähernd in Echtzeit zu überwachen.

Ansonsten kannst Du noch inotify zur Überwachung der Dateien nutzen, und eventuell in eine Ramdisk loggen, um Festplatten-E/A so weit als möglich zu vermeiden.

Dein Beispiel funktioniert übrigens nur, wenn "copytruncate" in der logrotate-Konfiguration gesetzt ist. Ansonsten verschiebt logrotate die Datei, das Dateiobjekt muss somit neu geöffnet werden.
PNS-Richi
User
Beiträge: 68
Registriert: Donnerstag 17. Januar 2008, 01:48

@lunar: Da könntest du recht haben mit Python und FUSE, deshalb überleg ich noch, wie ich das alles am besten lösen kann. Das mit FUSE ist trotzdem in Interessanter Ansatz. Immerhin müsste man ja nicht alles ans untere Dateisystem weiterleiten, sondern könnte direkt in eine Datenbank loggen. Ist dann halt ein höherer Aufwand.

Wenn nun logrotate die alte logfile verschiebt, dann würde doch der filehandler auf den falschen inode zeigen, oder? Bei den ganzen "tail -f" Beispielen.

Code: Alles auswählen

...

	inode = os.stat(PATH)[ST_INO]

	while 1:
		inode_new = os.stat(PATH)[ST_INO]
		if inode != inode_new
			file = open(PATH, "r")
			pos = 0
			inode = inode_new

...
lg Richi
lunar

Meinem Verständnis nach schon. "rename()" weist einem Inode ähnlich einem Hardlink nur einen neuen Namen zu, während eine neue Datei einen eigenen INode erhält.

Fuse zum Loggen in eine Datenbank erscheint mir übrigens ziemlich kompliziert ;) Kannst Du nicht direkt ohne Umwege in eine Datenbank loggen, so wie syslog-ng das beispielsweise unterstützt? Oder Lognachrichten zumindest an einen Dienst wie syslog-ng weiterleiten, der sie anschließend in die Datenbank schreiben kann?

Dann könntest Du dein Überwachungsprogramm einfach nur auf die Trigger der Datenbank lauschen lassen, und dann mit einem einfachen SELECT alle neuen Einträge holen.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Klar, würde er das. Wenn eine Datei nicht meher existiert, da sie verschoben oder gelöscht wurde und wenn danach eine neue Datei mit dem Namen der alten erstellt wird, dann bekommt die neue Datei auch einen neuen Inode.

Code: Alles auswählen

sebastian@deepthought:~$ touch test.txt
sebastian@deepthought:~$ stat test.txt
  File: „test.txt“
  Size: 0         	Blocks: 0          IO Block: 4096   reguläre leere Datei
Device: 806h/2054d	Inode: 2050009     Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/sebastian)   Gid: ( 1000/sebastian)
Access: 2009-09-24 11:10:19.000000000 +0200
Modify: 2009-09-24 11:10:19.000000000 +0200
Change: 2009-09-24 11:10:19.000000000 +0200
sebastian@deepthought:~$ mv test.txt test2.txt
sebastian@deepthought:~$ touch test.txt
sebastian@deepthought:~$ stat test.txt
  File: „test.txt“
  Size: 0         	Blocks: 0          IO Block: 4096   reguläre leere Datei
Device: 806h/2054d	Inode: 2050079     Links: 1
Access: (0644/-rw-r--r--)  Uid: ( 1000/sebastian)   Gid: ( 1000/sebastian)
Access: 2009-09-24 11:10:43.000000000 +0200
Modify: 2009-09-24 11:10:43.000000000 +0200
Change: 2009-09-24 11:10:43.000000000 +0200
BlackJack

@snafu: So klar ist das nur beim Verschieben. Wenn man eine Datei löscht, und eine Neue anlegt, kann die Neue durchaus den Inode der Alten bekommen. Der wird durch das Löschen ja wieder frei.
PNS-Richi
User
Beiträge: 68
Registriert: Donnerstag 17. Januar 2008, 01:48

Ach syslog hab ich erst vor kurzem ersetzt. Hab mir da etwas eigenes geschrieben das sich an die RFC des „syslog Protokolls“ hält. Ok wie sinnvoll es ist, ist mir egal – mir geht’s hauptsächlich um den Lerneffekt und den Spaß. Lauscht genau so an einem Unix und Network Socket mit Datagram Protokoll. Benutzt jedoch dann TCP/IP um es an einen anderen LogServer weiter zu leiten(natürlich eigener Dienst, TCP/IP auf UDP wäre etwas sinnlos), oder eben direkt in eine Datenbank. Dazu gibt es ein Unix Socket, damit es mit anderen Programmiersprachen möglich ist die neuen Log Einträge zu parsen. Immerhin taugt TCP/IP mehr als UDP, wenn das Netz stark ausgelastet ist. Nun wollte ich halt, jedenfalls seit gestern auch noch nen Daemon schreiben der vorhandene Log Dateien überwacht und an weitere Dienste weiterleitet die auf gewisse Einträge reagieren sollen, oder sie in diverse Datenbanken eintragen können – klingt immer so einfach, deshalb das „tail –f“ Beispiel. Klar könnte man auch ein Plugin System schreiben, aber so kann ich Beispiele dazu in anderen Sprachen schreiben, überhaupt Gambas wäre dafür mal interessant. Geht rein um den Spaß bei der Sache :-)
lunar

Gambas ist doch Visual Basic?! Lernen hin oder her, aber dazu muss man sich doch nicht herablassen ;)

Im Ernst: Wenn es Dir um das Lernen geht, stellt sich die Frage nach dem Sinn ja nicht. Wenn Du aber eh die Kontrolle über den Logging-Dienst hast, dann ist es imho halt einfacher, in eine Datenbank zu loggen, die man aus jeder Sprache problemlos und ohne Kopfzerbrechen über Synchronisation und Atomarität von Dateisystemoperationen nutzen kann.
Antworten