none
Wie lese ich eine Datei per StreamReader "von Unten"? RRS feed

  • Frage

  • Hallo liebe Community,

    ich habe von meinem Ausbildungsbetrieb eine Aufgabe erhalten, bei der ich mich mit der PowerShell vertraut machen und ein Script schreiben soll. Dieses Script soll eine ständig ändernde Log-Datei nach einem speziellen Event überprüfen, dieses vorkommen in den letzten 5 Minuten zählen und dann eine entsprechende Rückmeldung geben. Nur am Rande: Das Script wird als Check für die Monitoringumgebung Icinga 2 verwendet. 

    Bisher habe ich mit "get-content" gearbeitet, musste jedoch feststellen, dass dies den Server bei einer Logdatei von etwas über 100mb (und das ist noch sehr klein) schon etwas zu sehr fordert. get-content möchte ich nun mit dem StreamReader ersetzen. Habt ihr eine Idee, wie ich dies am besten realisieren kann beziehungsweise wie ich mithilfe des StreamReaders die Datei vom Dateiende ab an lesen kann und nicht von oben? Es ist mir leider nicht möglich, eine Anzahl der letzten Zeilen anzugeben, da dies sehr variable sein kann. Dies muss mit dem auslesen des Zeitstempels geschehen.

    Der Aufbau des Logs gestaltet sich so:

    2021-01-13T11:12:55.814 INFO     (000013B8) (1696) Connecting 2A00:6020:11CA:F800:6CE1:EC2D:A611:C309
    2021-01-13T11:12:55.824 INFO     (000013B8) (1696) Connecting FE80::6CE1:EC2D:A611:C309
    2021-01-13T11:14:09.305 INFO     (000013B8) (1084) Connecting 2A00:6020:11CA:F800:6CE1:EC2D:A611:C309
    2021-01-13T11:14:09.305 INFO     (000013B8) (1084) 2A00:*A611:C309 call system T
    2021-01-13T11:14:09.332 INFO     (000013B8) [gw@46910 rtype="D3FC-T/CheckExistenceOfNewHoldFiles" work="17" net="0" dispatch="9" bytes="731"] Request D3FC-T/CheckExistenceOfNewHoldFiles
    2021-01-13T11:14:09.347 INFO     (000013B8) (1072) Connecting FE80::6CE1:EC2D:A611:C309
    2021-01-13T11:14:09.347 INFO     (000013B8) (1072) Sending: MSG\DATE
    2021-01-13T11:14:09.348 INFO     (000013B8) [gw@46910 rtype="CLIENT/DOWNLOAD" work="0" net="0" dispatch="0" bytes="331"] Request CLIENT/DOWNLOAD

    Außerdem füge ich zum besseren Verständnis mal mein Script mit an.

    Param (
        # Ordnerpfad
        [Parameter(Mandatory=$true)]
        [string]
        $dir,
    
        # Name der Komponente
        [Parameter(Mandatory=$true)]
        [string]
        $component,
    
        # Anzahl der letzten Minuten im Log
        [Parameter(Mandatory=$false)]
        [int]
        $minutes = 5,
    
        # Das Event, nach dem gefiltert werden soll
        [Parameter(Mandatory=$true)]
        [string]
        $event,
    
        # Warunung-Schwellenwert für Check-Status
        [Parameter(Mandatory=$false)]
        [int64]
        $Warning = 1,
    
        # Kritisch-Schwellenwert für Check-Status
        [Parameter(Mandatory=$false)]
        [int64]
        $Critical = 3,
    
        # Optionale Leistungsdaten an Icinga2 übergeben
        [Parameter(Mandatory=$false)]
        [switch]
        $NoPerformanceData,
    
        # Anzeige der Hilfe zum Script
        [Parameter(Mandatory=$false)]
        [switch]
        $Help
    )
    # Zur Prüfung, ob die Schritte des Skripts erfolgreich ausgeführt wurden
    [bool]$Validation = $false;
    
    # Wird für die gefundenen Treffer genutzt
    [int64]$lines
    
    # Plugin-Ausgabe
    [string]$Output = "";
        
    # Rückgabewert
    [int]$ReturnCode = 3;
    
    # Zur Ermittlung des Datums, um den korrekten Unterordner zu ermitteln
    $dirdate = Get-Date -UFormat "%Y%m%d"
    
    # Pfad zum gewünschten Log-File
    [string] $logfile = Get-Content $dir"\"$dirdate"\"$component".txt" -Raw
    
    # Ermittelt die letzen 5 Minuten des Logs
    [string] $RecentTimeStamps = $logfile -split '[\r\n](?=\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d)' | Where-Object {[datetime]::Now.AddMinutes($minutes * -1) -lt $_.Substring(0,19)}
    
    # Erfasst alle Treffer
    $Matches = Select-String -InputObject $RecentTimeStamps -Pattern $event -AllMatches
    
    # Zählt die Treffer und bewertet diese Anzahl
    $lines = $Matches.Matches.Count
    
    if(!($Warning -eq 0 -and $Critical -eq 0))
    {
        if($Warning -le $Critical)
        {
    
            if($lines -lt $Warning)
            {
                # Rückgabewert OK
                $ReturnCode=0;
                $Output = "[OK] Lines called '$event' - Count= $lines";
                $Validation = $true;
            }
            elseif($lines -lt $Critical)
            {
                # Rückgabewert WARNING
                $ReturnCode=1;
                $Output = "[WARNING] Lines called '$event' - Count= $lines";
                $Validation = $true;
            }
            elseif($lines -ge $Critical)
            {
                # Rückgabewert CRITICAL
                $ReturnCode=2
                $Output = "[CRITICAL] Lines called '$event' - Count= $lines;"
                $Validation = $true;
            }
        }else 
        {
                $Output = "ERROR: Der Warnungs-Schwellwert darf nicht niedriger sein als der Kritisch-Schwellwert!";
                $ReturnCode=3
                $Validation = $false;
        }
    
    }
    
    # Wenn Parameter 'NoPerformanceData' nicht gesetzt ist, füge Leistungsdaten an Plugin-Ausgabe an.
    if($Validation)
    {
        if(!($NoPerformanceData)){
            if(!($Warning -eq 0 -and $Critical -eq 0)){
                $Output += " | 'Detected Lines'=$lines;$Warning;$Critical;";
            }else{
                $Output += " | 'Detected Lines'=$lines;";
            }
        }
    }
    Write-Output $Output;
    exit $ReturnCode;
    
    Über Hilfe von euch würde ich mich sehr freuen. :-)

    Mittwoch, 13. Januar 2021 10:55

Antworten

Alle Antworten

  • Per Get-Content bekommst du die gesamte Datei.
    Per Split-Funktion mit NewLine bekommst du dann ein Array, dass du iterativ rückwärts durchlaufen kannst.

    https://stackoverflow.com/questions/39252620/powershell-split-specify-a-new-line

    Alternativ: per Array-Reverse kannst du das Array auch umdrehen und vorwärts durchlaufen (Pipeline, Foreach):
    https://devblogs.microsoft.com/scripting/powertip-reverse-array-with-powershell/
    Mittwoch, 13. Januar 2021 13:01
  • Danke für deine Antwort, 

    anders als es aktuell in meinem Script ist, möchte ich aber gar nicht mit get-content arbeiten, da die Log-Dateien dafür einfach zu groß sind. Wenn die ganze Datei geladen wird, werden ca. 6-8 Millionen Zeilen eingelesen. Das geht in dem Moment ordentlich auf den RAM und sorgt beim Monitoring für einen TimeOut, da das Script für einen Durchlauf einfach zu lange braucht.

    Gibt es da nicht eine einfache Alternative, wie ich die Datei ab dem Ende an einlesen kann? Dabei spielt nur ein Zeitraum von 5 Minuten aus dem Log eine Rolle. Die entsprechenden Timestamps, die ich abgleiche kann, sind ja vorhanden.

    Mittwoch, 13. Januar 2021 13:37
  • Moin,

    a. hast Du den Parameter -Tail bei Get-Content beachtet?

    b. Das, was Du suchst, erreichst Du am ehesten nicht mit der StreamReader-, sondern mit der FileStream-Klasse. Diese hat eine Seek-Methode, mit der Du vorspulen kannst.


    Evgenij Smirnov

    http://evgenij.smirnov.de

    Mittwoch, 13. Januar 2021 14:29
  • Die kurze Antwort ist: es geht so nicht bzw. ist sehr tricky (nicht nur in PoSh).

    Solange du ungefähr einschätzen kannst, das die von dir gesuchte Information sich in den letzten x-Zeilen befindet, ist "-tail" deine beste + schnellste Option.
    Kannst du das nicht, bedeutet das ja, die Information kann sich irgendwo, also auch sogar relativ am Anfang befinden. Dann kannst du die Datei auch einfach vom Anfang lesen, komplett oder Zeile-für-Zeile.

    Für komplettes Lesen eignet sich alternativ zu Get-Content z.b. [System.IO.File]::ReadAllLines($fileobj.Fullname) was ca. 10-20-mal schneller ist.
    Wenn dein Problem hingegen beim knappen RAM liegt, eben der Streamreader.

    Wenn das alles nicht ideal ist, lohnt es sich manchmal auch das Problem neu zu denken: z.b. kann man vielleicht die jeweils zu durchsuchende Logdatei verkleinern bzw. splitten? Jedes halbwegs vernünftige Monitoringsystem sollte eigentlich Optionen zur Logfilegröße haben.
    Falls das wirklich nicht der Fall ist, kann man sogar noch überlegen, die Dateigröße konstant mit einem Script zu überwachen und alle x-Minuten "von aussen" zu splitten.

    Oder auch nochmal von einer ganz anderen Seite: was willst du eigentlich erreichen? Ich nehme an auf bestimmte Events reagieren. Bietet ein Monitoringsystem nicht normalerweise genau dafür Eventhandler an?


    Blog: http://www.bytecookie.de

    Powershell Code Manager: Link
    (u.a. Codesnippets verwalten + komplexe Scripte graphisch darstellen)

    Hilf mit und markiere hilfreiche Beiträge mit dem "Abstimmen"-Button (links) und Beiträge die eine Frage von dir beantwortet haben, als "Antwort" (unten).
    Warum das Ganze? Hier gibts die Antwort.

    Samstag, 16. Januar 2021 12:35
    Moderator
  • Für komplettes Lesen eignet sich alternativ zu Get-Content z.b. [System.IO.File]::ReadAllLines($fileobj.Fullname) was ca. 10-20-mal schneller ist.

    ...wobei der Parameter -ReadCount von Get-Content diesen Vorsprung deutlich schmälert, in vielen Fällen nahezu auf Null.

    Evgenij Smirnov

    http://evgenij.smirnov.de

    Samstag, 16. Januar 2021 12:47
  •  Sicher, wobei das "komplette Lesen" einer Datei eben nicht zu diesen Fällen gehört. :)
    UPDATE: Du hast recht, ich habs nochmal getestet. Besonders mit PoSh 7 ist da kein Unterschied mehr.

    Aber je länger ich darüber nachdenke, finde ich die ganze Aufgabenstellung (insbesondere durch den Hinweis auf die zeitkritische Komponente) etwas skurril: ein Monitoringscript für das Log eines Monitoringsystems. ;)


    Blog: http://www.bytecookie.de

    Powershell Code Manager: Link
    (u.a. Codesnippets verwalten + komplexe Scripte graphisch darstellen)

    Hilf mit und markiere hilfreiche Beiträge mit dem "Abstimmen"-Button (links) und Beiträge die eine Frage von dir beantwortet haben, als "Antwort" (unten).
    Warum das Ganze? Hier gibts die Antwort.


    Samstag, 16. Januar 2021 12:57
    Moderator
  • Erstmal danke für eure Antworten. So skurril ist die Aufgabe gar nicht. Unser Unternehmen vertreibt ein DMS-System an Kunden aus dem öffentlichen Dienst. Dabei übernehmen wir auch die Installation sowie Administration. In wenigen Fällen werden die Systeme von uns auch überwacht. Das DMS besitzt im Backend ein System zu loggen von Ereignissen, welches man nur auf dem jeweiligen Server einsehen kann. Dies ist jedoch KEIN Monitoringsystem. Mithilfe von Satelliten, möchten wir dieses Logging aber überwachen. Dafür verwenden wir bekannte Fehlermeldungen die auf einen Systemstillstand hinweisen. Das Logging legt in Echtzeit ein Log auf dem Kundenserver an. Dieses soll alle 5 Minuten mit der Monitoringsoftware auf die entsprechenden Fehlermeldungen überprüft werden, dafür muss jedoch nicht das ganze Log durchgesehen werden, da uns nur die letzten 5 Minuten interessieren. 

    Das heißt: Das Monitoringsystem stößt alle 5 Minuten das PS-Script an, dieses soll überprüfen, was in den letzten 5 Minuten passiert ist. Ist nichts vorgefallen, ist alles in Ordnung, wurde diese Fehler jedoch gefunden, wird dieser gezählt und je nach Anzahl wird es "Warning" oder "Critical" eingestuf.

    Ich hoffe ich konnte die Situation nun etwas besser darstellen.

    Montag, 18. Januar 2021 08:36
  • Da würde ich das Log eher in einem gewissen Zeitramen (5 Minuten) in eine Datenbank übernehmen und dafür dann umbennen. Nach der Übernahme kann es (ggf. zeitgesteuert) dann entfernt werden. So große Logs sind da nicht zielführend.
    Anschließend kann man die Datenbank per SQL auswerten, Häufigkeit und Art der Fehler dokumentieren und grundsätzlichere Lösungen schaffen.
    Das Log sieht bis auf die Zeitmarke eher unstrukturiert aus, da bieten sich dann durchaus Textsearch-Funktiuonen an.

    Montag, 18. Januar 2021 08:53
  • Konnte das Problem jetzt mithilfe einer While- und einer For-Schleife lösen. Ich habe die Anzahl der letzten Zeilen in eine Variable gepackt und diese wird, sollte der Zeitraum mit den Zeilen noch nicht abgedeckt sein, erhöht.

    Hier der verwendete Code. Auch wenn ich eine andere Lösung gefunden habe, danke ich euch für die Hilfe. :-)

    # Zur Prüfung, ob die Schritte des Skripts erfolgreich ausgeführt wurden
    [bool]$Validation = $false
    
    # Zur Prüfung, ob der komplette Zeitraum erfasst wurde
    [bool]$finished = $false
    
    # Anzahl der letzten Zeilen, die das Script liest. 
    [int64]$lastLines = 1000
    
    # Anzahl der Zeilen, die pro Schleifendurchlauf addiert werden, wenn der gewünschte Zeitraum noch nicht erreicht ist
    [int64]$addToLastLines = 1000
    
    # Anzahl der Zeilen, nach denen abgebrochen wird, um die Serverlast zu verringern
    [int64]$abortLines = 50000
    
    # Erstellt eine Liste, in der die abgearbeiteten Zeilen gespeichert werden
    $processedLines = New-Object System.Collections.Generic.List[System.Object]
    
    # Plugin-Ausgabe
    [string]$Output = "";
        
    # Rückgabewert
    [int]$ReturnCode = 3;
    
    # Wird zum zählen der gefundenen Fehler benötigt
    [int64] $detectedLines = 0
    
    # Zur Ermittlung des Datums, um den korrekten Unterordner zu ermitteln
    $dirdate = Get-Date -UFormat "%Y%m%d"
    
    # Beinhaltet die zu verarbeitenden Zeilen des Logs
    $logfile
    
    # Fragt die aktuelle Zeit ab
    $currentTime = Get-Date
    
    # Die Uhrzeit, bei der abgebrochen werden soll
    $abortTime = $currentTime.AddMinutes($minutes * -1)
    
    # Das Format des Zeitstempels aus dem Log
    $timestampFormat = "yyyy-MM-ddTHH:mm:ss.fff"
    
    # Überprüft die letzten 1000 Zeilen eines Log-Files. Sollten in diesen Zeilen der Überprüfungszeitraum von 5 Minuten noch nicht erreicht sein,
    # werden weitere Zeilen hinzugefügt. Ein automatischer Abbruch erfolgt nach erreichen von 50.000 Zeilen
    while(!$finished)
    {
        try
        {
            $logfile = Get-Content -Path $dir'\'$dirdate'\'$component'.txt'  -tail $lastLines -ErrorAction Stop
            $Validation = $true
        }
        catch
        {
            $ReturnCode = 3
            Write-Output "$PSItem"
            $Validation = $false
        }
    if($Validation)
    {	
        for ($i= $logfile.Length -1; $i -gt 0; $i--)
        {
            $currentLine = $logfile[$i]
            # Sondiert den Zeitstempel aus der aktuellen Zeile
            $timestampFromLog = $currentLine.Substring(0,23)
            # Wandelt den Zeitstempel in ein Datum mit Uhrzeit um
            $parsedTimestamp = [datetime]::ParseExact($timestampFromLog, $timestampFormat, $null)
    
            # Es wird überprüft, ob die aktuelle Zeile schon abgearbeitet wurde, wenn nicht, wird diese hinzugefügt
            if (!$processedLines.Contains($currentLine)) 
            {
                $processedLines.Add($currentLine)
                # Vergleicht den Zeitstempel der aktuellen Zeile mit der Abbruchszeit. Ist dieser geringer, wird die while-Schleife abgebrochen
                if ($parsedTimestamp -gt $abortTime) 
                {   
                    # Überprüft die Zeile nach dem gesuchten Event, trifft dies zu, wird $detectedLines um '1' hochgezählt.
                    if($currentLine -match $event)
                    {
                        $detectedLines++
                    } 
                }
                else 
                {
                    $finished = $true
                    $Validation = $true
                    break
                }   
            }
        }
    }
        # Ist die Schleife bis hier hin erfolgreich durchgelaufen, wird die Bedingung überprüft, ansonsten wird die Schleife abgebrochen
        if($Validation)
        {
            # Wenn die Schleife noch nicht das Ende erreicht hat, werden weitere Zeilen zur Überprüfung hinzugefügt
            if(!$finished)
            {
                $lastLines = $lastLines + $addToLastLines 
            }
            # Wenn die hinzugefügten Zeilen die Anzahl der $abortLines übersteigt, wird die die Schleife abgeschlossen
            if ($lastLines -gt $abortLines) 
            {
                $finished = $true
                $Validation = $true
            }
        }
        else
        {
            break
        }       
    }

    • Bearbeitet StythEU Donnerstag, 21. Januar 2021 11:53
    Donnerstag, 21. Januar 2021 11:50