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 {
  [OutputType([System.Collections.ArrayList])]
  param (
    [ValidateScript({
        if (-Not ($_ | Test-Path) ) {
          throw "File or folder does not exist"
        }
        return $true
      })]
    [string[]]
    $Path = 'C:\ProgramData\chocolatey\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
  }

  [System.Collections.ArrayList]$parsed = @()
  $detected = [System.Collections.Generic.List[int]]::new()

  $RegularExpression = Convert-PatternLayout $PatternLayout

  $files | ForEach-Object -Process {
    $file = $_
    $raw = [System.IO.File]::ReadAllLines($file.FullName)

    # Iterate over each line
    foreach ($line in $raw) {
      # Write-Debug $line
      $m = $RegularExpression.match($line)
      if ($m.Success) {
        $threadMatch = $m.Groups['thread'].Value
        # If it matches the regex, tag it
        if ($threadMatch -ne $currentSession.thread) {
          # This is a different thread

          # Save the current session to the parsed list
          if ($currentSession) {
            $parsed.Add($currentSession) > $null
          }

          # Look up if current thread exists in Parsed and append to that if so
          if ($detected.Contains($threadMatch)) {
            $currentSession = $parsed | Where-Object { $_.Thread -eq $threadMatch }
          } else {
            # We haven't seen this thread before, let's make a new object
            $detected.Add($threadMatch) > $null
            $currentSession = [ChocoLog]::new(
              $threadMatch,
              ($m.Groups['date'].Value -replace ',', '.'),
              $file
            )
          }
        }

        $currentSession.logs.Add(
          [Log4NetLogLine]::new(
            [Datetime]($m.Groups['date'].Value -replace ',', '.'),
            $threadMatch,
            $m.Groups['level'].Value,
            $m.Groups['message'].Value
          )) > $null
      } else {
        # if it doesn't match regex, append to the previous
        if ($currentSession) {
          $currentSession.logs[-1].AppendMessage($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"
        }
      }
    }
  }
  # Write out the last log line!
  if (-Not $parsed.Contains($currentSession)) {
    $parsed.Add($currentSession) > $null
  }

  # Doing this at the end since threads can get mixed
  $parsed | ForEach-Object {
    # This updates fields like: cli, environment, and configuration
    $_.ParseSpecialLogs()
  }

  # Return the whole parsed object
  $parsed
}