Get-Tail

Um das Ende einer Textdatei, quasi am Schwanz (Tail), zu folgen, gibt es in der Unix-Welt das Programm "Tail". Unter Windows musste man sich dagegen mit 3rd Party Tools (Siehe auch Tail) oder eigenen Programmen und Skripts behelfen. Erst die PowerShell brachte mit "Get-Content" eine Funktion mit, auch am Ende vorzusetzen. Für meine Zwecke reicht das aber nicht aus

Dieses Skript ist aktuell schon entwickelt aber die Dokumentation ist noch veraltet. Ich arbeite am Update.

Warum nicht "get-content -wait" ?

Mit PowerShell 5 sind all diese Einschränkungen bei Get-Content nicht mehr vorhanden. Ich werde mein Get-Tail aber weiter pflegen, weil ich das Speichern des letzten Standes in einigen Skripten brauche.

Mit PowerShell 2.0 hat Microsoft den Befehlt "Get-Content" mit dem Parameter "Wait" erweitert, damit das PowerShell Commandlet am Ende einer Datei auf neue Zeilen wartet. Das geht mit Textdateien auch sehr einfach, aber es gibt gleich mehrere Dinge, die mir dabei fehlen:

  • Nur eine Datei
    Wer mal einen "Get-Content -wait *.txt" versucht hat, wird schnell erkennen, dass Get-Content nur die erste Datei verfolgt und weitere oder neue Dateien nicht weiter nutzt. Man müsste also schon mehrere Threads oder Tasks nebeneinander laufen lassen
  • Anfang war nicht zu überspringen
    In den ersten Versionen gab es meiner Analyse nach keine Option den vorhandenen Teil zu überspringen und nur das Ende zu lesen. Get-Content hat bei mir immer erst die komplette Datei bis zum Ende gelesen. Erst ab PowerShell 3 können Sie z.B mit "-tail 0" die Datei überspringen und am Ende fortsetzen.
  • Kein Timeout
    Ein Wait wartet bis das Skript abgebrochen wird. Ich kann also nicht nach einigen Sekunden die Kontrolle zurück erhalten, sondern muss dann den Aufruf aktiv abbrechen.
  • Kein Status speicherbar
    Zuletzt kann Get-Content natürlich auch nicht speichern, wie weit es gelesen hatte und schon gar nicht an der letzten Position aufsetzen.

Vielleicht ist nun klar, warum ich etwas "besseres" gesucht aber nicht gefunden haben.

Eine einfache Umsetzung in Form einer VBScript-Klasse zum fortgesetzten Lesen am Ende einer Datei habe ich auf VBSToolbox unter der URL class.tail.1.0.vbs veröffentlicht, Aber das kommt natürlich überhaupt nicht an das eigentliche Ziel heran.

Anforderungen

Daher überlege ich schon lange, ob ich mit eine eigene TAIL-Klasse bzw. TAIL-Objekt erstelle, welche für mich essentielle Funktionen mitbringt. Es reicht mir nicht aus, einfach nur ein Programm mit einem Dateinamen aufzurufen, welches dann die neu angefügten Daten ausgibt.

  • Mehrere Dateien
    Das Überwachen einer Datei ist trivial. Zumindest die Angabe von Wildcards solle möglich sein, womit dann natürlich auch verbunden ist, dass die Dateien parallel überwacht werden. Schließlich können sich alle Dateien ändern.
  • Wiederaufsetzen
    Skripte werden auch mal beendet oder Server durchgestartet. Ein TAIL, der Änderungen in der Pausenzeit nicht erkennt verliert Daten und wenn ein Tail alles wieder einliest, gibt es doppelte Einträge. Daher sollte das Modul die letzte Position der Dateien sich merken (Registry, Datei o.ä.) und beim Abbruch oder Neustart dort wieder aufsetzen.
  • Dynamische Dateien addieren
    Wenn während des Laufs neue Dateien angelegt werden (z.B. man überwacht das Exchange Messagetracking und sogar neu angelegte Dateien des Musters dynamisch mit überwacht werden. Wird eine Datei gelöscht und dann wieder angelegt, muss Sie natürlich auch wieder "von vorne" gelesen werden.
  • CSV-Format
    Get-Content liest einfach nur "Text" während Import-CSV sogar Tabellen formatiert einliest und als Objekte in die Pipeline schiebt. Eine Kombination hiervon wäre wünschenswert, d.h. dass die Aufgaben eines GET-TAIL auf eine CSV-Datei auch eine entsprechende Ausgabe liefert. Genial wäre natürlich, denn das Modul optional am Anfang versuchen würde das Format der Datei (Headerzeile) aufgreifen würde.
  • Pipeline-Output
    Als PowerShell-Modul sollte per Default natürlich einfach die Zeile ausgegeben werden. Werden aber mehrere Dateien überwacht und vielleicht sogar CSV-Dateien analysiert, dann wäre ein Objekt besser. Selbst wenn "nur" Zeilen übergeben werden müssten, könnten diese ja aus unterschiedlichen Quellen sein. Insofern könnte pro Zeile durchaus ein Datensatz bestehend aus einem Zeitstempel, dem Dateinamen, Pfad und dann der Zeile entstehen.
  • Zeilenfilter
    Wenn es die Zeit erlaubt, wäre ein Filterausdruck zumindest für die Zeilen wünschenswert. Über einen regulären Ausdruck (RegEx) könnte GET-TAIL schon filtern, welche Zeilen überhaupt zur Weiterverarbeitung gesendet werden müssen.
  • Timeout
    Moderne Hochsprachen können auch "Callback"-Funktionen, d.h. das aufrufende Programm kann weiter arbeiten und wird von dem untermodul wieder angesprungen. PowerShell und viele anderen Skriptsprachen sind aber sequentiell und warten daher, bis das aufgerufene Modul zurück kommt. Insofern wäre es wichtig, dass nach einem einstellbaren Timeout das Modul wieder die Kontrolle an das Hauptprogramm mit einer leeren Zeile übergibt, so dass dieses auf andere Vorgänge reagieren kann. Auch hier wird klar, warum sich das Modul die letzten Positionen merken sollte.

Ich erwarte mir deutlich mehr als eine einfach "tail.exe". Ideal fände ich eine .NET DLL, welche als PSSnapin einfach nachgeladen werden kann. Vielleicht kann Sie auch noch per COM erreicht werden, damit die VB-Skript-Jünger auch noch etwas davon haben.

Einsatzbereiche

Nun werden Sie sich fragen, warum so was "einfaches" überhaupt entwickelt werden soll. Dazu möchte ich ein paar Beispiele aufführen. Weitere Einsatzbereiche finden Sie auch auf Tail.

  • IIS-Überwachung
    Es ist gar nicht mal unüblich, dass man z.B.: die Logdateien eines IIS (Webservers) in nahezu Echtzeit überwachen will. Ein öffnen mit Notepad ist bei großen Dateien nicht mehr sinnvoll, Get-Content liest dann auch erst mal lange alte Daten und Windows-Versionen eines TAIL zeigen zwar schön an, aber man kann nicht effektiv filtern.
    PowerShell mit einem GET-TAIL würde hier sehr einfach eine Lösung erlauben, besonders wenn die Ausgabe per Pipeline noch gefiltert werden könnte. So könnte ich gezielt eine ClientIP, einen UserAgent, einen Benutzernamen oder spezielle URLs überwachen.
  • IIS-Auswertung
    In Verbindung mit der Funktion die letzte Position zu speichern würde es erlauben, kleinere Auswertungen selbst durchzuführen, z.B. alle 10 Min ein Skript zu starten, um die Anzahl der OWA-Anmeldungen, ActiveSync Einträge zu ermitteln und in eine Datenbank zu importieren.
  • Exchange Messagetracking
    Die Exchange 2007+ PowerShell-Befehle kann man nur über "Start" und "End" auf einen Bereich festlegen. Das ist in vielen Fällen ausreichend aber manchmal will man die Dateien doch schneller "Massenverarbeiten" und sicher keine Zeile übersehen.
  • Einbindung fremder Systeme
    Sehr viele Programme können keine Eventlog o.ä. schreiben aber Protokolldateien. So gibt es verschiedene SYSLOG-Daemons, die Meldungen von Routern in Dateien schreiben können. So könnten diese Dateien einfach weiter verarbeitet werden. Auch ich habe das ein oder anderen Skripte, welches Ergebnisse in Dateien protokolliert (z.B. TrackLoginEvents). Auch hier wäre eine Weiterverarbeitung möglich.
  • Analyse von Router Logs
    Router oder auch Unified Messaging Gateways können per SYSLOG eine Meldung an einen SyslogD schreiben, der die Daten natürlich in eine Text-Datei schreiben kann. So wäre eine Weiteverarbeitung und Analyse einfach möglich.

Dateien sind eine einfache stabile und flexible Schnittstelle zur Übergabe von Informationen von einem Prozess zu einem anderen Dienst und funktionieren auch Betriebssystemübergreifend und über die meisten (Datei)-Netzwerke.

Wie können Änderungen verfolgt werden ?

Ich habe mir natürlich schon meine Gedanken gemacht. Es gibt gar mehrere Optionen, möglichst schnell zu erfahren, ob eine Datei sich geändert hat. Vier Verfahren konnte ich ausfindig machen:

  • DIR mit "Last Modified-Timestamp
    Jede Datei auf einem Dateisystem hat ein "Änderungsdatum". Ein Skript könnte also immer wie ein "DIR" periodisch das Verzeichnis pollen und so Änderungen erkennen. Ohne besondere Rechte kann das lokal gut funktionieren, da die meisten Dateisystem auch einen Cache haben. Über LAN erzeugt die aber schon eine Last, die mit dem Bedarf an Aktualität steigt. Am kritischsten ist aber zu sehen, dass nicht alle Programme eine Datei nach dem Schreiben schließen und daher das Datum nicht immer zeitnah auch aktualisiert wird.
  • Immer am Ende lesen
    Ein zweiter Weg besteht darin, die Dateien einfach bis ans Ende zu lesen bzw. ans Ende zu springen (Wenn die Wunschposition bekannt ist) und von dort über die EOF-Marke immer mal wieder weiter zu lesen. Solange keine Dateien angehängt wurden liefert der Lesebefehl ein EOF (End of File) zurück und man wartet weiter. Es werden keine besonderen Rechte benötigt, bei lokalen Zugriffen spart der Cache entsprechende IOs. Über LAN hingegen ist diese Version zu prüfen.
  • NTFS Change Notification
    Das NTFS-Dateisystem erlaubt die Aktivierung von Änderungsmeldungen, d.h. Windows informiert ein Programm., wenn sich in einem Verzeichnis oder einer Datei etwas tut. Dieser Ansatz wäre nahezu Echtzeit, würde auch über LAN (SMB) funktionieren und benötigt keine höheren Privilegien. In PowerShell könnte so etwas wie folgt beginnen:
$watcher = [system.io.filesystemwatcher]
$watcher.path = ""
watcher.Filter = "*.txt"
$watcher.NotifyFilter = (NotifyFilters.LastAccess Or NotifyFilters.LastWrite Or NotifyFilters.FileName Or NotifyFilters.DirectoryName)
  • Filter Treiber / Debug-Schnittstellen
    ähnlich einen Virenscanner oder einer Archiv/HSM-Lösung könnte natürlich auch ein NTFS-Filter alle (Schreib)-Zugriffe auf Dateien überwachen (Analog zu Sysinternals Filemon) und daraus Rückschlüsse ziehen, welche Dateien wohl geändert und nachgelesen werden müssen. Diese Weg wäre "Echtzeit" aber eher überdimensioniert und nicht sehr einfach umzusetzen. Man müsste zwingend lokal und höher privilegiert sein.

Jede Lösung hat ihre Vor- und Nachteile. Aus Kompatibilitätsgründen und der Frage der erforderlichen Berechtigungen könnte ich mir aber vorstellen, dass das fortgesetzte Lesen am Ende der Dateien und die Überwachung nach neuen bzw. gelöschten Dateien mit einem DIR eine sinnvolle Kombination sein könnte.

Letztlich realisiert habe ich aber einfach die Speicherung der letzten Position in einer Metadatei.

Wie können Dateien zeilenweise positioniert eingelesen werden ?

Wenn wir uns mal in der .NET-Welt bewegen, dann gibt es natürlich die vorhandenen .NET-Klassen:

Klasse Beschreibung

StreamReader TextReader

Diese Klasse ist ideal zum sequentiellen Lesen von Textdateien. Vor allem weil sie ein "ReadLine" unterstützt, welches einfach die nächste Zeile liest. Dummerweise unterstützt sie aber keine "Seek"-Funktion um bestimmte Stellen direkt anzuspringen. Man kann immer nur "vorwärts" lesen. Zurückspringen geht nicht. Man kann aber die Datei einfach schließen und wieder neu von vorne öffnen. Selbst die Funktion "peek", zeigt in der Regel auf das letzte Byte im Buffer und nicht die aktuelle Position von "ReadLine". Der Einsatz ist relativ einfach, z.B. mit

$reader = [System.IO.File]::OpenText("c:\test.txt")

# oder

$sr = new-object System.IO.Streamreader $stream

Wenn man versucht erst mit einem Filestream zu starten und auf dem dann den Textreader anzuwenden, dann ist der darunterliegende Stream vom Textreader bis zum Ende gelesen worden. Dem Textreader fehlt das "Position"-Property, so dass man selbst eine "Zählung" aufbauen müsste aber der darunterliegende Stream "stimmt" dann auch nicht.

FileStream BinaryReader

Leistungsfähiger ist die FileStream-Klasse, welche auch eine freie Positionierung erlaubt. Allerdings fehlt dieser Klasse die "ReadLine". Man muss also selbst eine Bufferverwaltung bauen, um die Strings zu Lesen und als Zeilen zurück zu geben. Dafür hat diese Klasse eine "Position"-Property. Hier eine ganz einfache Funktion

# open file
$filename="c:\test.txt"
$stream= [System.IO.File]::open($filename, [system.IO.filemode]::open, [System.IO.FileAccess]::Read, [System.IO.Fileshare]::readwrite)
# this basic filestream also support position, seek, read

#Binaryreader allows to read "characters" in addition but not "lines"
$binfile= new-object System.IO.binaryreader $stream
write-host "Position sollte 0 als Start sein :"$binfile.BaseStream.Position
write-host "Ein Zeichen lesen:"$binfile.readchar()
write-host "Position ist nun 1:"$binfile.BaseStream.Position
write-host "ueberspringe 5 Zeichen"
write-host "Next Char lesen ohne pointer zu schieben:"$binfile.peekchar()
$binfile.BaseStream.Seek(5,[System.IO.seekorigin]::current)
write-host "Länge" $binfile.BaseStream.length
$bytearray = $file.readchars(5)
write-host ""[string]::join ("",$bytearray)

Alle "Readxxxx"-Methoden lesen die Binärdaten RAW ein. Ein ReadString ist nicht mit einem ReadLine zu verwechseln, da es erwartet, dass am Anfang die Länge des String steht. Also C++ Denkweise. ReadChars kann mehrere Zeichen lesen und diese als Array liefern, die dann mit "Join". Oder man nutzt direkt den Filestream und baut sich die Pufferverwaltung selbst.

Idealerweise sollte eine Tail-Funktion mit Speicher als auf einem Filestream aufsetzen und Option einen Binaryreader nutzen.

Filestream mit Streamreader kombiniert

Interessant wird der Einsatz der beiden Funktion, indem ein Filestream genutzt wird, um eine Datei zu öffnen und dann an die gewünschte Position zu springen um dann den Stream in einem Streamreader zu verwenden, um bis zum Ende dann mit "ReadLine" die Zeilen einzulesen.

# open file
$filename="c:\test.txt"
$stream= [System.IO.File]::open($filename, [system.IO.filemode]::open, [System.IO.FileAccess]::Read, [System.IO.Fileshare]::readwrite)

# Jump to last position
$stream.Seek(10,[system.io.seekorigin]::begin)

# Start reading
$textstream = new-object System.IO.Streamreader $stream
$textstream.readline()

# Jump back
$stream.Seek(0,[system.io.seekorigin]::begin)
$textstream.DiscardBufferedData()

Wird während des Lesens an die Datei was angehängt, dann verschiebt sich die "Length"-Eigenschaft, aber der Streamreader verharrt mit der "Position" erst mal, bis er mit einem ReadLine am Ende angekommen ist. Dann allerdings ist die Position auch wieder erhöht worden, wobei es nicht sicher ist, ob man dann schon alle Zeilen auch mit ReadLine verarbeitet hat. Hier sollte man am Ende auf jeden Fall Position mit Length abgleichen.

Springt man im Stream mit "Seek" herum, dann bekommt das der Streamreader erst mal nicht mit, da er seine eigene "Bufferverwaltung" hat.. Erst ein "DiscardBufferedData" forciert ein Neueinlesen.

Schade ist, dass der Streamreader nicht auf ein "CR/LF" am Ende wartet. Addiert ein Prozess also eine neue Zeile und ist diese nur "halb" geschrieben, dann wird einmal die bereits geschriebene Zeile und beim nächsten Lesen der zweite Teil als eigene Zeile ausgegeben. Es sei denn der schreibende Prozess "lockt" die Datei anstelle eines einfachen "Append".

Der Einfachheit halber basiert mein Modul auf der dritten Variante und ich erspare mir so umfangreiche Buffer-Routinen um aus einem Binarystream wieder Zeilen zu machen.

Überlegung zur Umsetzung

Eine Umsetzung mit einer aktiven "Watcher-Funktion" ist zwar interessant aber erfordert, dass das Programm quasi "permanent" läuft um auf die Meldungen zu reagieren. Das ist eine Aufgabe für "richtige" Programme und Dienste aber für meinen Ansatz überzogen. Ich kann analog zu Get-USNChanges und GET-ADChanges ganz gut damit leben, wenn ein Skript beim Aufruf die aktuellen Änderungen geeignet ausgibt und sich dann wieder beendet oder nach einer kurzen Wartezeit, die durchaus Sekunden sein können, wieder an die Arbeit macht. Der Vorteil bei dieser Lösung ist die Kompatibilität auch mit UNC-Pfaden und anderen Server, da es nicht auf NTFS-Change-Notifications aufsetzen muss.

Technisch sollte das Script eine Liste von Dateien gemäß einem Dateifilter in einem Verzeichnis einlesen und neue Zeilen einfach über die Pipeline ausgeben. Wenn es die Möglichkeit hat, einen Status zu speichern, dann kann das Script auch beendet und später wieder gestartet werden. Dann sollte es dort aufsetzen, wo es beendet wurde.

Das Script ist aber definitiv nicht für die Überwachung von Verzeichnissen mit SEHR VIELEN DATEIEN geeignet, da es ja alle Dateien regelmäßig abscannt.

Parameter

 

Typ Parameter Default Bedeutung

[string]

cookiefilename

Kein Cookiefile

Das Skript muss, wenn es beendet und später wieder aufgerufen wird, irgendwo den letzten Stand speichern. In der angegebenen Datei landet der Status der überwachten Dateien und die Position.

[string]

filter

.\*.*

Der Filter spezifiziert Pfad und Dateipattern der zu überwachenden Dateien
Ein Cookiefile wird natürlich ausgeschlossen.

[int]

sleeptime

3 Sekunden

Immer wenn das Skript eine Suche abgeschlossen und die Elemente ausgegeben hat, legt es eine "Pause" ein. Kürzere Pausen belasten das System mehr aber sie können schneller reagieren.

[switch]

once

$false

Wird der Schalter "-once" gesetzt, dann beendet sich das Skript nach einem Durchlauf. Beachten Sie, dass dies nur in Verbindung mit einem Cookie-File Sinn macht. Ansonsten gibt es keine Änderungen zu melden

[switch]

skipold

$true

Weist das Script an, nur neue Änderungen zu melden. Diese Funktion "überstimmt" einen eventuell per Cookiefile eingelesenen alten Status !. Es wird also bei den Dateien zuerst an das Ende gesprungen und dann weiter gelesen. Es werden also keine alten Daten oder zwischenzeitliche Änderungen erkannt.

[switch]

nosave

$false

Mit dem Schalter "-nosave" wird der Cookie am Ende einer Bearbeitung nicht zurück geschrieben. Beim nächsten Aufruf werden also die zuletzt gefundenen Änderungen erneut zurück gegeben. Dies ist primär für Tests geeignet.

[switch]

verbose

$false

Durch die Angabe von "-verbose" werden detailliertere Ausgaben während der Verarbeitung gemacht, die bei eier Fehlersuche helfen können.

[switch]

noscreen

$false

Deaktiviert die zusätzliche Ausgabe auf den Bildschirm der gefundenen Änderungen . Sinnvoll, wenn die normale Ausgabe in die Pipeline nicht mit "| out-null" unterdrückt oder mit einem anderen Prozess weiter verarbeitet wird.

[switch]

pipeline

no

Steuert die Ausgabe der Daten an die Pipeline zur weiteren Verarbeitung

  • No
    Keine Ausgabe in die Pipeline
  • mini
    Gibt einfach die Zeile ohne weitere Informationen an die Pipeline
  • full
    Es wird ein Datensatz mit den Properties "Timestamp", "Filename", "Line" gemeldet.

 

 

 

Funktionsweise

 

$FileSystemWatcher = New-object System.IO.FileSystemWatcher "c:\temp"
$result = $FileSystemWatcher.WaitForChanged("all")

 

 

 

 

Erkenne "gekürzte Dateien"

Erkenne gelöschte Dateien

Erkenne neue Dateien

 

Tail auf...

Der Einsatz dieser Routinen ist universell und vielfältig. Denken Sie mal an.

  • IIS-Parser 1
    Ein Script welches die IISLogs auf problematische Zugriffe auswertet. Die abgerufenen mit nicht mit einem 4xx/5xx quittierten URLs könnten gegen eine Allowlist von "erlaubten" Verzeichnissen und Dateien geprüft werden. So würde auffallen, wenn der Webserver Inhalte ausliefert, die sie so nicht bereitgestellt haben.
  • IIS-Parser 2
    Genauso könnten Sie natürlich quasi Realtime Zugriffe auf URLs erkennen und z.B. per Mail melden
  • Lync Logging
    Der Communicator Client loggt optional verschiedene Informationen in einer Textdatei. Ein Parser könnte die letzten Aktivitäten hierzu z.B.: ausgehen.

So ein "Tail" auf eine Datei oder eine Dateigruppe ist auch automatisiert gut einzusetzen.

Weitere Links