Public/Get-FslEventSummary.ps1

function Get-FslEventSummary {
    <#
    .SYNOPSIS
        Summarizes FSLogix events from the Windows event logs, bucketed by event ID
        and translated via the curated event database.
    .DESCRIPTION
        Queries the FSLogix event channels (Operational and Admin), groups the events
        by ID and enriches each bucket with the plain-English explanation from
        Data\EventIds.psd1. Ideal for spotting login-storm patterns across a host.
    .PARAMETER ComputerName
        One or more session hosts to query. Defaults to the local machine.
    .PARAMETER After
        Only consider events newer than this timestamp. Defaults to the last 24 hours.
    .PARAMETER MinimumLevel
        'Error' returns only error events, 'Warning' also warnings (default),
        'Information' everything.
    .EXAMPLE
        Get-FslEventSummary -After (Get-Date).AddHours(-8)
    .EXAMPLE
        Get-FslEventSummary -ComputerName avd-sh-01, avd-sh-02 -MinimumLevel Error
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [string[]]$ComputerName = @($env:COMPUTERNAME),

        [datetime]$After = (Get-Date).AddHours(-24),

        [ValidateSet('Error', 'Warning', 'Information')]
        [string]$MinimumLevel = 'Warning',

        [switch]$IncludeCloudCache
    )

    $eventTable = Get-FslDataTable -Name EventIds

    # Event level values: 1=Critical, 2=Error, 3=Warning, 4=Information
    $maxLevel = switch ($MinimumLevel) {
        'Error' { 2 }
        'Warning' { 3 }
        'Information' { 4 }
    }

    $channels = @('Microsoft-FSLogix-Apps/Operational', 'Microsoft-FSLogix-Apps/Admin')
    if ($IncludeCloudCache) {
        $channels += @('Microsoft-FSLogix-CloudCache/Operational', 'Microsoft-FSLogix-CloudCache/Admin')
    }

    foreach ($computer in $ComputerName) {
        $events = @()
        foreach ($channel in $channels) {
            $filter = @{
                LogName   = $channel
                StartTime = $After
                Level     = @(1..$maxLevel)
            }
            try {
                $getParams = @{ FilterHashtable = $filter; ErrorAction = 'Stop' }
                if ($computer -ne $env:COMPUTERNAME) { $getParams['ComputerName'] = $computer }
                $events += @(Get-WinEvent @getParams)
            }
            catch {
                # Locale-independent detection of the 'no events match' case.
                if ($_.FullyQualifiedErrorId -like 'NoMatchingEventsFound*') {
                    Write-Verbose "No matching events in '$channel' on '$computer'."
                }
                else {
                    Write-Warning "Could not query '$channel' on '$computer': $($_.Exception.Message)"
                }
            }
        }

        foreach ($group in ($events | Group-Object Id | Sort-Object Count -Descending)) {
            $eventId = [string]$group.Name
            $known = $null
            if ($eventTable.ContainsKey($eventId)) { $known = $eventTable[$eventId] }

            $sorted = @($group.Group | Sort-Object TimeCreated)
            $sample = ($sorted | Select-Object -Last 1).Message
            if ($sample -and $sample.Length -gt 400) { $sample = $sample.Substring(0, 400) + '...' }

            # LevelDisplayName is localized ('Fehler' on German Windows) - keep it
            # for display only and carry the numeric level for classification
            # (1=Critical, 2=Error, 3=Warning, 4=Information). Use the most severe
            # level present in the bucket; ignore 0 (LogAlways).
            $levelValues = @($sorted | ForEach-Object { [int]$_.Level } | Where-Object { $_ -gt 0 })
            $mostSevereLevel = 4
            if ($levelValues.Count -gt 0) {
                $mostSevereLevel = ($levelValues | Measure-Object -Minimum).Minimum
            }

            $meaning = 'Not in the curated FSLogixDoctor event database.'
            $recommendation = ''
            if ($known) {
                $meaning = $known.Meaning
                $recommendation = ($known.Fixes -join ' ')
            }

            # Generic-error IDs (notably event 26) reuse one ID for many different
            # messages; classify each message so known-benign noise can be told
            # apart from real failures. AlertMessages carries only the
            # alert-worthy breakdown - in a mixed bucket the one real message
            # must never drown in the noise counts.
            $messages = @($sorted | ForEach-Object { $_.Message })
            $benignCount = 0
            $alertRaw = New-Object System.Collections.Generic.List[string]
            foreach ($messageText in $messages) {
                if (-not $messageText) { continue }
                if (Test-FslBenignMessage -Message $messageText) { $benignCount++ }
                else { $alertRaw.Add($messageText) }
            }
            $topMessages = @(Get-FslMessageBreakdown -Message $messages)
            $alertMessages = @(Get-FslMessageBreakdown -Message $alertRaw.ToArray() -Top 5)

            # A curated entry may pin the severity (e.g. housekeeping events that
            # Windows logs as Warning but that only carry an FYI).
            $curatedSeverity = $null
            if ($known -and $known.ContainsKey('Severity')) { $curatedSeverity = [string]$known.Severity }

            # Record which channel(s) the bucket came from - the same ID can
            # exist in Operational and Admin, and follow-up queries need to
            # know where to look.
            $channelLabel = (@($sorted | ForEach-Object { [string]$_.LogName } | Where-Object { $_ } | Select-Object -Unique) -join ', ')

            [pscustomobject]@{
                PSTypeName      = 'FSLogixDoctor.EventSummary'
                ComputerName    = $computer
                Channel         = $channelLabel
                EventId         = [int]$group.Name
                Count           = $group.Count
                BenignCount     = $benignCount
                Level           = ($sorted | Select-Object -Last 1).LevelDisplayName
                LevelValue      = $mostSevereLevel
                CuratedSeverity = $curatedSeverity
                Meaning         = $meaning
                Recommendation  = $recommendation
                FirstSeen       = ($sorted | Select-Object -First 1).TimeCreated
                LastSeen        = ($sorted | Select-Object -Last 1).TimeCreated
                SampleMessage   = $sample
                TopMessages     = $topMessages
                AlertMessages   = $alertMessages
            }
        }
    }
}