Public/Get-FslLogError.ps1
|
function Get-FslLogError { <# .SYNOPSIS Parses FSLogix log files and returns structured warning/error entries. .DESCRIPTION Reads the FSLogix text logs (default %ProgramData%\FSLogix\Logs), extracts WARN/ERROR lines including embedded error codes, and returns one object per entry. Pipe the result to Get-FslErrorCode to decode the codes, or into New-FslReport via Invoke-FslDiagnostic. Each entry carries a Benign flag: $true when the message matches one of the known-benign noise patterns in Data\BenignPatterns.psd1 (e.g. 'Failed to query activity id'), so real errors can be separated from log noise with Get-FslLogError | Where-Object { -not $_.Benign }. .PARAMETER Path Root log directory. Defaults to %ProgramData%\FSLogix\Logs. .PARAMETER Component Which log component subfolder(s) to parse. Defaults to Profile and ODFC. .PARAMETER After Only return entries newer than this timestamp. Defaults to the last 24 hours. .PARAMETER Newest Only parse the newest N log files per component (after the After filter on file write time). Defaults to all matching files. .PARAMETER IncludeWarnings Include WARN entries in addition to ERROR entries. .EXAMPLE Get-FslLogError -After (Get-Date).AddHours(-4) .EXAMPLE Get-FslLogError -IncludeWarnings | Group-Object ErrorCode | Sort-Object Count -Descending #> [CmdletBinding()] [OutputType([pscustomobject])] param( [string]$Path = (Join-Path $env:ProgramData 'FSLogix\Logs'), [ValidateSet('Profile', 'ODFC', 'All')] [string]$Component = 'All', [datetime]$After = (Get-Date).AddHours(-24), [int]$Newest = 0, [switch]$IncludeWarnings ) if (-not (Test-Path -LiteralPath $Path)) { Write-Warning "Log path '$Path' not found. FSLogix is not installed or logging is disabled." return } $folders = @() if ($Component -in @('Profile', 'All')) { $folders += 'Profile' } if ($Component -in @('ODFC', 'All')) { $folders += 'ODFC' } # FSLogix log lines carry a time but no date; the date comes from the file name # (e.g. Profile-20260712.log) with the file write date as fallback. # Level markers per Microsoft: [WARN: xxxxxxxx] has a space after the colon, # [ERROR:xxxxxxxx] does not. Codes are Windows system error codes in hex, # sometimes in HRESULT form (8007xxxx). $levelPattern = '\[(?<level>ERROR|WARN)(?::\s*(?<code>[0-9a-fA-F]{8}))?\s*\]' $timePattern = '^\[(?<time>\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]' $inlineCodePattern = '(?<inline>0x[0-9a-fA-F]{8})' foreach ($folder in $folders) { $folderPath = Join-Path $Path $folder if (-not (Test-Path -LiteralPath $folderPath)) { continue } $allFiles = Get-ChildItem -LiteralPath $folderPath -Filter '*.log' -File -ErrorAction SilentlyContinue $files = @($allFiles | Where-Object { $_.LastWriteTime -ge $After } | Sort-Object LastWriteTime -Descending) if ($Newest -gt 0) { $files = @($files | Select-Object -First $Newest) } foreach ($file in $files) { $fileDate = $file.LastWriteTime.Date if ($file.BaseName -match '(?<date>\d{8})') { try { $fileDate = [datetime]::ParseExact($Matches['date'], 'yyyyMMdd', [System.Globalization.CultureInfo]::InvariantCulture) } catch { Write-Verbose "File name '$($file.Name)' contains no parsable date; using last write date." } } $lineNumber = 0 # Entries written after midnight land in the previous day's file until # FSLogix rolls to a new one; when the time-of-day decreases between # entries, advance the date by a day. $dayOffset = 0 $lastTimeOfDay = $null foreach ($line in [System.IO.File]::ReadLines($file.FullName)) { $lineNumber++ if ($line -notmatch $levelPattern) { continue } $level = $Matches['level'] $code = $Matches['code'] if ($level -eq 'WARN' -and -not $IncludeWarnings) { continue } $timestamp = $fileDate if ($line -match $timePattern) { $timeText = $Matches['time'] $parsed = [datetime]::MinValue # The [string[]] cast is required: without it PS 5.1 binds the # single-format overload and the parse silently fails. FFFFFFF # accepts 1-7 fractional-second digits. $formats = [string[]]@('HH\:mm\:ss.FFFFFFF', 'HH\:mm\:ss') if ([datetime]::TryParseExact($timeText, $formats, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::None, [ref]$parsed)) { if ($null -ne $lastTimeOfDay -and $parsed.TimeOfDay -lt $lastTimeOfDay) { $dayOffset++ } $lastTimeOfDay = $parsed.TimeOfDay $timestamp = $fileDate.AddDays($dayOffset).Add($parsed.TimeOfDay) } else { Write-Verbose ("Could not parse time '{0}' in {1}:{2}; using the file date." -f $timeText, $file.Name, $lineNumber) } } if ($timestamp -lt $After) { continue } # Strip the bracketed prefix blocks to isolate the message text. $message = ($line -replace '^(\[[^\]]*\])+', '').Trim() if (-not $code -and $line -match $inlineCodePattern) { $code = $Matches['inline'].Substring(2) } $normalizedCode = $null if ($code) { $normalizedCode = '0x{0}' -f $code.ToUpperInvariant() } [pscustomobject]@{ PSTypeName = 'FSLogixDoctor.LogEntry' Timestamp = $timestamp Component = $folder Level = $level ErrorCode = $normalizedCode Message = $message Benign = [bool](Test-FslBenignMessage -Message $message) File = $file.FullName LineNumber = $lineNumber } } } } } |