PowerShell-Skripte in der Praxis ── Protokollauswertung, Archivierung und Reporting sicher automatisieren

· · PowerShell, Windows, Automatisierung, Protokollauswertung, Betriebsoptimierung, Nutzung vorhandener Ressourcen

1. Was zuerst wichtig ist

Im letzten Artikel haben wir die grundlegenden PowerShell-Befehle, die Pipeline, CSV-Ausgabe, JSON, .ps1-Skripte und die Sicherheitsprüfung mit -WhatIf behandelt. Diesmal bauen wir darauf aufbauend ein etwas größeres Skript, das sich gut im Praxisalltag einsetzen lässt. Als Thema dient folgende Betriebsaufgabe:

  1. Protokolle auswerten
  2. Fehlerzeilen in einer CSV zusammenfassen
  3. Alte Protokolle als Archivierungskandidaten auflisten
  4. Alte Protokolle bei Bedarf verschieben
  5. Das Ergebnis des Laufs als Nachweis aufbewahren

Bei der praktischen Anwendung von PowerShell kommt es nicht darauf an, Befehle länger zu machen, sondern den folgenden Ablauf zu etablieren:

  1. Konfiguration trennen
  2. Lesende Verarbeitung aufbauen
  3. Ausgaben aufbewahren
  4. Verändernde Verarbeitung in Funktionen auslagern
  5. Mit -WhatIf einen Probelauf durchführen
  6. Zuletzt eine automatische Ausführung in Erwägung ziehen

Automatisierung bedeutet nicht, „die menschliche Prüfung wegzulassen“. Es bedeutet, festzulegen, an welcher Stelle geprüft wird, und dafür zu sorgen, dass bei jedem Lauf derselbe Nachweis entsteht.

Der in diesem Artikel gezeigte Code steht als direkt ausführbares Beispielpaket auf GitHub bereit (das fertige Skript, die Konfigurationsdatei, ein Skript zum Erzeugen einer Dummy-Protokollumgebung sowie Pester-Tests, die Auswertung, Probelauf und Verschieben prüfen).

powershell-script-log-maintenance-automation - komurasoft-blog-samples (GitHub)

2. Was wir diesmal bauen

Diesmal erstellen wir ein Skript namens Invoke-LogMaintenance.ps1.

Die wichtigsten Funktionen sind:

Funktion Inhalt
Protokollsuche Sucht .log-Dateien unterhalb des angegebenen Ordners
Zeitraum Untersucht nur Protokolle, die innerhalb der letzten N Tage aktualisiert wurden
Fehlerextraktion Extrahiert Zeilen mit ERROR, WARN, FATAL und Ähnlichem
CSV-Ausgabe Speichert die Suchergebnisse in log-hits.csv
Liste alter Protokolle Speichert Protokolle, die älter als N Tage sind, in archive-targets.csv
Archivierung Verschiebt alte Protokolle in einen separaten Ordner
Probelauf Bei Angabe von -Preview wird nicht verschoben, sondern nur der geplante Vorgang angezeigt
Laufprotokoll Speichert Transkript, zusammenfassendes JSON und Ergebnis-CSV

Gelöscht wird dabei nichts. In diesem ersten vertiefenden Teil beschränken wir uns auf das sicherere „Verschieben“ statt auf ein Löschen.

3. Ordnerstruktur

Als Beispiel verwenden wir folgende Struktur.

C:\Ops
  Invoke-LogMaintenance.ps1
  log-maintenance.json

C:\App\Logs
  app.log
  batch.log
  old
    app-202401.log

C:\App\Reports
  20260602-030000
    log-hits.csv
    archive-targets.csv
    archive-result.csv
    summary.json
    transcript.txt

C:\App\Archive
  20260602-030000
    old
      app-202401.log

Wenn Sie das eigentliche Skript und die Konfigurationsdatei getrennt halten, lässt sich der Austausch je Umgebung leicht bewerkstelligen. So können Sie etwa in der Entwicklungsumgebung C:\Test\Logs und in der Produktion D:\App\Logs verwenden, indem Sie nur den Pfad ändern.

4. Die Konfigurationsdatei anlegen

Zunächst erstellen wir log-maintenance.json.

{
  "LogPath": "C:\\App\\Logs",
  "OutputPath": "C:\\App\\Reports",
  "Days": 7,
  "Patterns": [
    "ERROR",
    "WARN",
    "FATAL"
  ],
  "ArchiveDays": 90,
  "ArchivePath": "C:\\App\\Archive"
}

Die Bedeutung der Felder ist wie folgt.

Feld Bedeutung
LogPath Der zu untersuchende Protokollordner
OutputPath Ziel für die Reports
Days Wie viele der letzten Tage an Protokollen untersucht werden
Patterns Die zu suchenden Zeichenketten/Muster
ArchiveDays Ab welchem Alter ein Protokoll als Archivierungskandidat gilt
ArchivePath Der Zielordner für die Archivierung

Durch die Auslagerung nach JSON lassen sich die Bedingungen ändern, ohne das eigentliche Skript zu bearbeiten.

In PowerShell können Sie JSON mit ConvertFrom-Json als Objekt behandeln. Umgekehrt verwenden Sie ConvertTo-Json, um Verarbeitungsergebnisse als JSON abzulegen. ConvertTo-Json ist das Cmdlet, das Objekte in JSON-Zeichenketten umwandelt; bei tief verschachtelten Strukturen ist die Angabe von -Depth wichtig.

5. Zunächst nur den lesenden Teil bauen

Schreiben Sie nicht sofort die Verschiebeverarbeitung, sondern beginnen Sie damit, Protokolle zu finden und in eine CSV zu überführen.

$config = Get-Content .\log-maintenance.json -Raw -Encoding UTF8 | ConvertFrom-Json

$since = (Get-Date).AddDays(-[int]$config.Days)

$files = Get-ChildItem -LiteralPath $config.LogPath -Filter *.log -File -Recurse |
  Where-Object { $_.LastWriteTime -ge $since }

$files |
  Select-Object FullName, Length, LastWriteTime

Als Nächstes durchsuchen wir den Inhalt der Protokolle.

$patterns = [string[]]$config.Patterns

Select-String -LiteralPath ($files | Select-Object -ExpandProperty FullName) -Pattern $patterns |
  Select-Object Path, LineNumber, Pattern, Line |
  Export-Csv .\log-hits.csv -NoTypeInformation -Encoding UTF8

Bis hierhin handelt es sich ausschließlich um lesende Vorgänge. Auch wenn Sie dies im Produktivordner ausprobieren, halten Sie zunächst an dieser Stelle an.

6. Alte Protokolle auflisten

Als Nächstes listen wir die Archivierungskandidaten auf.

$limit = (Get-Date).AddDays(-[int]$config.ArchiveDays)

$targets = Get-ChildItem -LiteralPath $config.LogPath -Filter *.log -File -Recurse |
  Where-Object { $_.LastWriteTime -lt $limit } |
  Sort-Object LastWriteTime

$targets |
  Select-Object FullName, Length, LastWriteTime |
  Export-Csv .\archive-targets.csv -NoTypeInformation -Encoding UTF8

Auch in diesem Schritt wird noch nichts verschoben. Prüfen Sie anhand von archive-targets.csv, ob die Kandidaten nicht zu zahlreich sind und der Ordner stimmt.

7. Verändernde Verarbeitung in Funktionen auslagern

Trennen Sie verändernde Vorgänge wie das Verschieben von der lesenden Verarbeitung.

In PowerShell können Sie eine Funktion mit -WhatIf und -Confirm ausstatten, indem Sie ihr SupportsShouldProcess hinzufügen. -WhatIf zeigt, ohne auszuführen, an, „was geändert würde“, und -Confirm ist der Mechanismus, der vor der Ausführung eine Bestätigung einholt. Details dazu finden Sie in about_Functions_CmdletBindingAttribute auf Microsoft Learn.

function Move-OldLogFile {
  [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
  param(
    [Parameter(Mandatory)]
    [System.IO.FileInfo[]]$File,

    [Parameter(Mandatory)]
    [string]$SourceRoot,

    [Parameter(Mandatory)]
    [string]$ArchiveRoot
  )

  foreach ($item in $File) {
    $relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $item.FullName)
    $destination = Join-Path $ArchiveRoot $relativePath
    $destinationDirectory = Split-Path -Path $destination -Parent

    if ($PSCmdlet.ShouldProcess($item.FullName, "Move to $destination")) {
      if (-not [System.IO.Directory]::Exists($destinationDirectory)) {
        [System.IO.Directory]::CreateDirectory($destinationDirectory) | Out-Null
      }

      Move-Item -LiteralPath $item.FullName -Destination $destination -ErrorAction Stop

      [pscustomobject]@{
        Source      = $item.FullName
        Destination = $destination
        Status      = "Moved"
        Message     = ""
      }
    }
    else {
      [pscustomobject]@{
        Source      = $item.FullName
        Destination = $destination
        Status      = "Preview"
        Message     = ""
      }
    }
  }
}

Entscheidend ist, dass $PSCmdlet.ShouldProcess() unmittelbar vor Move-Item steht. Die Entscheidung, ob geändert wird, wird nicht außerhalb der Funktion getroffen, sondern unmittelbar vor der Änderung selbst.

8. Die fertige Version des Skripts

Hier ist die fertige Version, die alles bisher Gezeigte zusammenführt. Der Dateiname lautet Invoke-LogMaintenance.ps1.

# Invoke-LogMaintenance.ps1
#Requires -Version 7.0

[CmdletBinding()]
param(
  [ValidateNotNullOrEmpty()]
  [string]$ConfigPath = ".\log-maintenance.json",

  [switch]$Preview,

  [switch]$SkipArchive,

  [switch]$SkipTranscript
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

function Ensure-Directory {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [string]$Path
  )

  if (-not [System.IO.Directory]::Exists($Path)) {
    [System.IO.Directory]::CreateDirectory($Path) | Out-Null
  }
}

function Import-LogMaintenanceConfig {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [string]$Path
  )

  if (-not (Test-Path -LiteralPath $Path)) {
    throw "Config file not found: $Path"
  }

  $config = Get-Content -LiteralPath $Path -Raw -Encoding UTF8 | ConvertFrom-Json

  foreach ($name in @("LogPath", "OutputPath", "Days", "Patterns", "ArchiveDays", "ArchivePath")) {
    if (-not ($config.PSObject.Properties.Name -contains $name)) {
      throw "Config value missing: $name"
    }
  }

  if ([string]::IsNullOrWhiteSpace([string]$config.LogPath)) {
    throw "LogPath is empty."
  }

  if (-not (Test-Path -LiteralPath $config.LogPath)) {
    throw "LogPath not found: $($config.LogPath)"
  }

  if ([string]::IsNullOrWhiteSpace([string]$config.OutputPath)) {
    throw "OutputPath is empty."
  }

  if ([string]::IsNullOrWhiteSpace([string]$config.ArchivePath)) {
    throw "ArchivePath is empty."
  }

  if (@($config.Patterns).Count -eq 0) {
    throw "Patterns is empty."
  }

  if ([int]$config.Days -lt 1) {
    throw "Days must be 1 or greater."
  }

  if ([int]$config.ArchiveDays -lt 1) {
    throw "ArchiveDays must be 1 or greater."
  }

  return $config
}

function Export-CsvWithHeader {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [object[]]$InputObject,

    [Parameter(Mandatory)]
    [string]$Path,

    [Parameter(Mandatory)]
    [string[]]$Header
  )

  if ($InputObject.Count -gt 0) {
    $InputObject |
      Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding UTF8
  }
  else {
    ($Header -join ",") |
      Set-Content -LiteralPath $Path -Encoding UTF8
  }
}

function Get-LogHit {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [string]$LogPath,

    [Parameter(Mandatory)]
    [ValidateRange(1, 3650)]
    [int]$Days,

    [Parameter(Mandatory)]
    [string[]]$Pattern
  )

  $since = (Get-Date).AddDays(-$Days)

  $files = @(
    Get-ChildItem -LiteralPath $LogPath -Filter *.log -File -Recurse -ErrorAction Stop |
      Where-Object { $_.LastWriteTime -ge $since }
  )

  Write-Verbose "Recent log files: $($files.Count)"

  if ($files.Count -eq 0) {
    return @()
  }

  $paths = $files | Select-Object -ExpandProperty FullName

  Select-String -LiteralPath $paths -Pattern $Pattern -ErrorAction Stop |
    ForEach-Object {
      [pscustomobject]@{
        Path       = $_.Path
        LineNumber = $_.LineNumber
        Pattern    = $_.Pattern
        Line       = $_.Line.Trim()
      }
    }
}

function Get-OldLogFile {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [string]$LogPath,

    [Parameter(Mandatory)]
    [ValidateRange(1, 3650)]
    [int]$ArchiveDays
  )

  $limit = (Get-Date).AddDays(-$ArchiveDays)

  Get-ChildItem -LiteralPath $LogPath -Filter *.log -File -Recurse -ErrorAction Stop |
    Where-Object { $_.LastWriteTime -lt $limit } |
    Sort-Object LastWriteTime
}

function Move-OldLogFile {
  [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "Medium")]
  param(
    [Parameter(Mandatory)]
    [System.IO.FileInfo[]]$File,

    [Parameter(Mandatory)]
    [string]$SourceRoot,

    [Parameter(Mandatory)]
    [string]$ArchiveRoot
  )

  foreach ($item in $File) {
    $relativePath = [System.IO.Path]::GetRelativePath($SourceRoot, $item.FullName)
    $destination = Join-Path $ArchiveRoot $relativePath
    $destinationDirectory = Split-Path -Path $destination -Parent

    if (Test-Path -LiteralPath $destination) {
      $name = [System.IO.Path]::GetFileNameWithoutExtension($item.Name)
      $ext = $item.Extension
      $destination = Join-Path $destinationDirectory ("{0}_{1:yyyyMMddHHmmss}{2}" -f $name, $item.LastWriteTime, $ext)
    }

    if ($PSCmdlet.ShouldProcess($item.FullName, "Move to $destination")) {
      Ensure-Directory -Path $destinationDirectory

      Move-Item -LiteralPath $item.FullName -Destination $destination -ErrorAction Stop

      [pscustomobject]@{
        Source      = $item.FullName
        Destination = $destination
        Status      = "Moved"
        Message     = ""
      }
    }
    else {
      [pscustomobject]@{
        Source      = $item.FullName
        Destination = $destination
        Status      = "Preview"
        Message     = ""
      }
    }
  }
}

$config = Import-LogMaintenanceConfig -Path $ConfigPath

$runStamp = Get-Date -Format "yyyyMMdd-HHmmss"
$reportDir = Join-Path ([string]$config.OutputPath) $runStamp
Ensure-Directory -Path $reportDir

$transcriptStarted = $false
$transcriptPath = Join-Path $reportDir "transcript.txt"

try {
  if (-not $SkipTranscript) {
    Start-Transcript -Path $transcriptPath -Force | Out-Null
    $transcriptStarted = $true
  }

  Write-Host "Report directory: $reportDir"

  $hits = @(
    Get-LogHit `
      -LogPath ([string]$config.LogPath) `
      -Days ([int]$config.Days) `
      -Pattern ([string[]]$config.Patterns)
  )

  $hitCsv = Join-Path $reportDir "log-hits.csv"
  Export-CsvWithHeader `
    -InputObject $hits `
    -Path $hitCsv `
    -Header @("Path", "LineNumber", "Pattern", "Line")

  $oldFiles = @(
    Get-OldLogFile `
      -LogPath ([string]$config.LogPath) `
      -ArchiveDays ([int]$config.ArchiveDays)
  )

  $archiveTargets = @(
    $oldFiles |
      Select-Object FullName, Length, LastWriteTime
  )

  $archiveTargetCsv = Join-Path $reportDir "archive-targets.csv"
  Export-CsvWithHeader `
    -InputObject $archiveTargets `
    -Path $archiveTargetCsv `
    -Header @("FullName", "Length", "LastWriteTime")

  $moveResults = @()

  if ($SkipArchive) {
    Write-Host "Archive skipped."
  }
  elseif ($oldFiles.Count -eq 0) {
    Write-Host "No archive targets."
  }
  else {
    $archiveRunRoot = Join-Path ([string]$config.ArchivePath) $runStamp

    $moveResults = @(
      Move-OldLogFile `
        -File $oldFiles `
        -SourceRoot ([string]$config.LogPath) `
        -ArchiveRoot $archiveRunRoot `
        -WhatIf:$Preview
    )
  }

  $archiveResultCsv = Join-Path $reportDir "archive-result.csv"
  Export-CsvWithHeader `
    -InputObject $moveResults `
    -Path $archiveResultCsv `
    -Header @("Source", "Destination", "Status", "Message")

  $summary = [pscustomobject]@{
    CheckedAt          = (Get-Date).ToString("s")
    ComputerName       = $env:COMPUTERNAME
    LogPath            = [string]$config.LogPath
    ReportDirectory    = $reportDir
    HitCount           = $hits.Count
    ArchiveTargetCount = $oldFiles.Count
    ArchiveResultCount = $moveResults.Count
    Preview            = [bool]$Preview
    SkipArchive        = [bool]$SkipArchive
  }

  $summaryPath = Join-Path $reportDir "summary.json"
  $summary |
    ConvertTo-Json -Depth 5 |
    Set-Content -LiteralPath $summaryPath -Encoding UTF8

  Write-Host "Finished."
  Write-Host "Hits: $($hits.Count)"
  Write-Host "Archive targets: $($oldFiles.Count)"
}
catch {
  $errorPath = Join-Path $reportDir "error.txt"
  $_ | Out-String | Set-Content -LiteralPath $errorPath -Encoding UTF8
  Write-Error "Failed: $($_.Exception.Message)"
  exit 1
}
finally {
  if ($transcriptStarted) {
    Stop-Transcript | Out-Null
  }
}

9. Beispielaufrufe

Zunächst führen wir nur die Protokollauswertung aus, ohne die Archivierung durchzuführen.

.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -SkipArchive

Als Nächstes prüfen wir die geplante Archivierung.

.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview

Mit -Preview wird die Verschiebeverarbeitung für alte Protokolle als -WhatIf behandelt.

An dieser Stelle sind drei Dateien zu prüfen:

  • log-hits.csv
  • archive-targets.csv
  • archive-result.csv

Wenn alles unauffällig ist, führen Sie das Skript ohne -Preview aus.

.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json

Für ausführlichere Ausgaben fügen Sie -Verbose hinzu.

.\Invoke-LogMaintenance.ps1 -ConfigPath .\log-maintenance.json -Preview -Verbose

10. Die erzeugten Ausgabedateien

Beim Ausführen wird unterhalb von OutputPath ein zeitgestempelter Ordner angelegt, etwa C:\App\Reports\20260602-030000.

Darin werden folgende Dateien erzeugt.

Datei Inhalt
log-hits.csv Als Fehler oder Warnung erkannte Zeilen
archive-targets.csv Alte Protokolle, die zu Archivierungskandidaten wurden
archive-result.csv Ergebnis der Verschiebung bzw. das Preview-Ergebnis
summary.json Zusammenfassung von Anzahlen und Ausführungsbedingungen
transcript.txt Aufzeichnung der PowerShell-Sitzung
error.txt Details im Fehlerfall

Start-Transcript ist das Cmdlet, mit dem Befehle und Konsolenausgaben einer PowerShell-Sitzung in einer Textdatei aufgezeichnet werden. Bei Betriebsskripten erleichtert dies im Nachhinein die Feststellung, „wann unter welchen Bedingungen was ausgegeben wurde“.

11. Regelmäßige Ausführung über die Aufgabenplanung

Wenn die manuelle Ausführung unauffällig verläuft, lässt sich das Skript über die Aufgabenplanung regelmäßig ausführen.

Hier ein Beispiel, das zunächst täglich um 3 Uhr nachts läuft.

$scriptPath = "C:\Ops\Invoke-LogMaintenance.ps1"
$configPath = "C:\Ops\log-maintenance.json"

$action = New-ScheduledTaskAction `
  -Execute "pwsh.exe" `
  -Argument "-NoProfile -File `"$scriptPath`" -ConfigPath `"$configPath`"" `
  -WorkingDirectory "C:\Ops"

$trigger = New-ScheduledTaskTrigger -Daily -At 3:00

Register-ScheduledTask `
  -TaskName "AppLogMaintenance" `
  -Action $action `
  -Trigger $trigger `
  -Description "Collect app log errors and archive old logs"

New-ScheduledTaskAction erzeugt ein Objekt, das den von der Aufgabe auszuführenden Befehl darstellt, und New-ScheduledTaskTrigger legt die Startbedingungen fest – täglich, wöchentlich, bei Anmeldung und Ähnliches. Zuletzt registriert Register-ScheduledTask die Aufgabe auf dem lokalen Computer.

Im Produktivbetrieb sollten Sie zusätzlich Folgendes prüfen:

  • Der Ausführungsbenutzer hat Leserechte auf den Protokollordner
  • Es besteht Schreibzugriff auf das Archivierungsziel
  • Der Pfad zu pwsh.exe ist auflösbar
  • Die Ausführungsrichtlinie und die Signaturregeln des Skripts werden eingehalten
  • Manuelle Ausführung und Ausführung über die Aufgabe liefern dasselbe Ergebnis
  • Im Fehlerfall lassen sich error.txt und der Aufgabenverlauf einsehen

12. Häufige Stolperfallen

Symptom Ursache Abhilfe
Protokolle werden nicht gefunden LogPath ist falsch Mit Test-Path und Get-ChildItem prüfen
CSV bleibt leer Im gewählten Zeitraum liegen keine passenden Protokolle vor Days vergrößern und erneut prüfen
Zeichen werden falsch dargestellt Die Zeichenkodierung des Protokolls weicht von der Annahme ab Ein- und Ausgabekodierung überprüfen
Läuft nicht als Aufgabe Abweichender Ausführungsbenutzer oder Arbeitsordner WorkingDirectory und Berechtigungen prüfen
Zu viele Archivierungskandidaten ArchiveDays ist zu kurz gewählt archive-targets.csv ansehen und anpassen
Zielort weicht von der Erwartung ab Die Regel zur Beibehaltung relativer Pfade ist nicht bekannt Destination mit -Preview prüfen
Fehler nur in der Produktion Unterschiede bei Berechtigungen, Richtlinien oder gesperrten Dateien error.txt und transcript.txt prüfen

Insbesondere beim Ausführen über die Aufgabenplanung kann sich „der Benutzer, unter dem Sie selbst ausführen“ vom „Ausführungsbenutzer der Aufgabe“ unterscheiden. Läuft das Skript manuell, schlägt aber als Aufgabe fehl, sollten Sie zuerst Berechtigungen und den Arbeitsordner in Verdacht haben.

13. Überlegungen für Anpassungen

Dieses Skript ist eher als Ausgangspunkt gedacht, den Sie Schritt für Schritt an Ihre Umgebung anpassen, statt es unverändert einzusetzen. Hier einige typische Anpassungen.

Ziel Anpassungsstelle
Auch .txt-Dateien einbeziehen Get-ChildItem -Filter *.log ändern
Auch einige Zeilen um ERROR herum sehen Anhand des Select-String-Ergebnisses mit Get-Content die umliegenden Zeilen abrufen
Vor der Archivierung komprimieren Compress-Archive vor Move-OldLogFile einfügen
Per E-Mail benachrichtigen Anhand von summary.json eine Benachrichtigungsverarbeitung ergänzen
Konfiguration je Anwendung trennen Mehrere JSON-Dateien anlegen und die Aufgaben entsprechend aufteilen
Auch das Löschen automatisieren Zunächst über einen gewissen Zeitraum mit dem Verschiebe-Workflow arbeiten und erst danach entscheiden

Es ist jedoch sicherer, nicht von Anfang an alles einzubauen. Bei Betriebsskripten zählt weniger die Anzahl der Funktionen als vielmehr, im Fehlerfall nachvollziehen zu können, was passiert ist.

14. Betriebs-Checkliste für den Praxiseinsatz

Prüfen Sie vor der regelmäßigen Ausführung eines PowerShell-Skripts Folgendes:

  • Zunächst mit -SkipArchive nur die lesende Verarbeitung ausgeführt
  • Anschließend mit -Preview die geplante Verschiebung geprüft
  • Die Kandidaten in archive-targets.csv waren plausibel
  • Die Destination-Werte in archive-result.csv entsprachen den Erwartungen
  • Im Ausgabeordner blieb ein zeitgestempelter Nachweis erhalten
  • Bestätigt, dass im Fehlerfall error.txt erzeugt wird
  • Die Berechtigungen des Ausführungsbenutzers der Aufgabe geprüft
  • Ausführungsrichtlinie, Signatur und interne Vorgaben geprüft
  • Nicht sofort löschen, sondern zunächst mit Verschieben arbeiten
  • Festgelegt, wohin im Wiederherstellungsfall zurückgespielt wird

15. Zusammenfassung

Praktisches PowerShell bedeutet nicht, viele schwierige Syntaxkonstrukte zu verwenden. Im Berufsalltag zahlt sich aus, dieses Muster zu etablieren:

  • Konfiguration in JSON auslagern
  • Lesende Verarbeitung zuerst aufbauen
  • Nachweise in CSV und JSON festhalten
  • Verändernde Verarbeitung in Funktionen auslagern
  • Ein -WhatIf-ähnliches Probelauf-Verfahren bereitstellen
  • Über Transkript und error.txt Nachvollziehbarkeit sicherstellen
  • Erst nach manueller Prüfung die regelmäßige Ausführung aktivieren

Dieses Skript behandelt Protokollauswertung und Archivierung als Thema, doch die zugrunde liegende Denkweise lässt sich auch auf andere Aufgaben übertragen:

  • Dateiorganisation
  • Erstellung von Berichten
  • CSV-Auswertungen
  • Ablösung von Batch-Verarbeitung
  • Bestandsaufnahme alter Ressourcen
  • Tägliche und monatliche Betriebsprüfungen

PowerShell ist auch als Einzeiler praktisch, doch für den geschäftlichen Einsatz ist es sicherer, folgende Reihenfolge einzuhalten:

Ansehen → Aufzeichnen → Probelauf → Ausführen → Nachweis aufbewahren

Wenn Sie sich an diesem Muster orientieren, wird PowerShell nicht nur zu einem Werkzeug zur Zeitersparnis, sondern zu einer kleinen Fachanwendung, die den Betrieb stabilisiert.

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Womit sollte ich beginnen, wenn ich die Protokollauswertung mit PowerShell automatisiere?
Schreiben Sie nicht sofort verändernde Vorgänge wie Verschieben oder Löschen, sondern bauen Sie zunächst nur die lesenden Verarbeitungsschritte auf. Suchen Sie mit Get-ChildItem die betreffenden Protokolle, extrahieren Sie mit Select-String die Fehlerzeilen und geben Sie sie zunächst vollständig als CSV aus. Auch wenn Sie das Skript im Produktivordner testen, halten Sie an dieser lesenden Stufe zunächst an und prüfen Sie das Ergebnis. Erst danach gehen Sie schrittweise zur Auflistung der Archivierungskandidaten und zur Auslagerung der Verschiebeverarbeitung in eine Funktion über – das ist der sicherere Weg.
Wie baue ich -WhatIf in ein PowerShell-Skript ein?
Wenn Sie das CmdletBinding-Attribut einer Funktion um SupportsShouldProcess ergänzen, kann diese Funktion -WhatIf und -Confirm verwenden. Entscheidend ist, $PSCmdlet.ShouldProcess() unmittelbar vor dem tatsächlich verändernden Vorgang (etwa Move-Item) zu platzieren, sodass die Entscheidung, ob geändert wird, direkt vor der Änderung selbst fällt. Im Skript dieses Artikels wird der Schalter -Preview als -WhatIf:$Preview weitergereicht, sodass sich nur die geplanten Vorgänge anzeigen lassen, ohne tatsächlich etwas zu verschieben.
Warum funktioniert mein PowerShell-Skript manuell, schlägt aber in der Aufgabenplanung fehl?
Ein typischer Grund ist, dass der Benutzer, unter dem die Aufgabe ausgeführt wird, sich von dem unterscheidet, mit dem Sie das Skript selbst getestet haben. Prüfen Sie, ob dieser Ausführungsbenutzer Leserechte auf den Protokollordner und Schreibrechte auf das Archivierungsziel besitzt, ob der Arbeitsordner (WorkingDirectory) korrekt gesetzt ist, ob der Pfad zu pwsh.exe aufgelöst werden kann und ob die Ausführungsrichtlinie sowie die Signaturregeln eingehalten werden. Wichtig ist außerdem, dass sich Fehlschläge anhand von error.txt, transcript.txt und dem Aufgabenverlauf nachvollziehen lassen.
Sollte ich das Löschen alter Protokolle ebenfalls automatisieren?
Es wird nicht empfohlen, das Löschen von Anfang an zu automatisieren. Auch das Skript in diesem Artikel beschränkt sich auf das sicherere „Verschieben“ statt auf ein Löschen. Betreiben Sie zunächst über einen bestimmten Zeitraum den Verschiebe-Workflow, prüfen Sie anhand der Nachweise wie archive-targets.csv und archive-result.csv, dass keine Probleme auftreten, und ziehen Sie erst danach eine Automatisierung des Löschens in Betracht. Ebenso wichtig ist es, vorab festzulegen, wohin Dateien im Wiederherstellungsfall zurückgespielt werden.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Öffentliche Links

Zurück zum Blog