Public/Read-ChocoLog.ps1

<#
.SYNOPSIS
  Parses a Chocolatey log into an object that is easier to search and filter.
.DESCRIPTION
  Reads Chocolatey log(s) and creates a new set of custom objects. It highlights
  details that make it easier to search and filter.
.NOTES
  Works for Windows PowerShell and PowerShell Core. This works on Linux.
.LINK
  https://heyitsgilbert.github.io/ChocoLogParse/en-US/Read-ChocoLog/
.EXAMPLE
  Read-ChocoLog

  This will read the latest Chocolatey.log on the machine.
.PARAMETER Path
  The log path you want to parse. This will default to the latest local log.
  This can be a directory of logs.
.PARAMETER FileLimit
  The number of files the command should parse given a folder path.
.PARAMETER Filter
  The filter passed to Get Child Item. Default to 'chocolatey*.log.'
.PARAMETER PatternLayout
  The log4net pattern layout used to parse the log. It is very unlikely that you
  need to supply this. The code expects pattern names: time, session, level, and
  message.
#>

function Read-ChocoLog {
  # This makes PlatyPS sad.
  [OutputType([System.Collections.Generic.List[ChocoLog]])]
  param (
    [ValidateScript({
        if (-Not ($_ | Test-Path) ) {
          throw "File or folder does not exist"
        }
        return $true
      })]
    [string[]]
    $Path = "$($env:ChocolateyInstall)\logs\",
    [int]
    $FileLimit = 1,
    [String]
    $Filter = 'chocolatey*.log',
    [string]
    $PatternLayout = '%date %thread [%-5level] - %message'
  )
  $files = Get-Item -Path $Path
  if ($files.PSIsContainer) {
    $files = Get-ChildItem -Path $Path -Filter $Filter |
      Sort-Object -Property LastWriteTime | Select-Object -Last $FileLimit
  }
  Write-Verbose "Found files: $($files -join ',')"

  $parsed = @{}

  # Get the regex for the Log4Net PatternLayout
  $RegularExpression = Convert-PatternLayout -PatternLayout $PatternLayout
  $files | ForEach-Object -Process {
    $file = $_
    Write-Verbose "Reading over file: $file"
    $raw = [System.IO.File]::ReadAllLines($file.FullName)
    Write-Verbose "Lines read: $($raw.Count)"
    # Iterate over each line
    foreach ($line in $raw) {
      Write-Debug $line
      $m = $RegularExpression.match($line)
      if ($m.Success) {
        [int]$threadMatch = $m.Groups['thread'].Value
        # Replace comma with period to make it a valid datetime
        [datetime]$currentDateTime = $m.Groups['date'].Value -replace ',', '.'
        # Check if thread exists, if not make it.
        if (-not ($parsed.ContainsKey($threadMatch))) {
          Write-Verbose "New thread detected: $threadMatch"
          $null = $parsed.Add(
            $threadMatch,
            [ChocoLog]::new(
              $threadMatch,
              $file
            )
          )
        }
        Write-Verbose "Adding new log line to thread $threadMatch"
        $parsed.Item($threadMatch).AddLogLine(
          [Log4NetLogLine]::new(
            $currentDateTime,
            $threadMatch,
            $m.Groups['level'].Value,
            $m.Groups['message'].Value
          ))
      } else {
        Write-Verbose "Line did not match regex"
        Write-Debug $line
        # if it doesn't match regex, append to the previous
        if ($threadMatch) {
          Write-Verbose "Appending to existing thread: $threadMatch"
          $parsed.Item($threadMatch).AppendLastLogLine($line)
        } else {
          # This might happen if the log starts on what should have been a
          # multiline entry... Not very likely
          Write-Warning "No currentSession. File: $File; Line: $Line"
        }
      }
    }
  }

  # Doing this at the end since threads can get mixed
  $parsed.Keys | ForEach-Object {
    Write-Verbose "Parsing special logs for: $_"
    # This updates fields like: cli, environment, and configuration
    $parsed.Item($_).ParseSpecialLogs()
  }

  # Return the whole parsed object
  Write-Verbose "Returning results in desceding order. Count: $($parsed.Count)"
  $parsed.Values | Sort-Object -Descending Time
}