Public/Invoke-FslDiagnostic.ps1
|
function Invoke-FslDiagnostic { <# .SYNOPSIS Runs the full FSLogixDoctor diagnostic on a session host and answers the question 'why are profiles failing here?' in one command. .DESCRIPTION Combines every FSLogixDoctor check into one read-only run: 1. Environment - install state, service, version 2. Configuration - Test-FslConfiguration rule set 3. SessionState - per-session Status/Reason/Error, translated 4. LogFile - recent errors from the FSLogix text logs, decoded 5. EventLog - FSLogix event channels, bucketed and explained Returns FSLogixDoctor.Finding objects and optionally renders them into a self-contained HTML report (-ReportPath). Two safeguards keep noise from drowning out real problems: messages that match the known-benign noise database (Data\BenignPatterns.psd1) are reported as Info instead of Critical, and when every recorded session attached cleanly, remaining Critical log/event findings are downgraded to Warning because no user-visible impact exists yet. .PARAMETER Hours Look-back window for log and event analysis. Defaults to 24. .PARAMETER LogPath Root of the FSLogix log directory. Defaults to %ProgramData%\FSLogix\Logs. .PARAMETER IncludeWarnings Also surface WARN-level log lines, not only errors. .PARAMETER ReportPath When set, additionally writes the HTML report to this path. .PARAMETER PassThru Emit the finding objects even when -ReportPath is used. .PARAMETER ComputerName Fleet mode: run the diagnostic on one or more session hosts via PowerShell remoting and aggregate the results. FSLogixDoctor must be installed on each target (Install-Module FSLogixDoctor). Findings that are identical across hosts (typically configuration drift) are merged into one finding listing every affected host; host-specific findings stay separate. Unreachable hosts become Critical 'Fleet connectivity' findings instead of aborting the run. .PARAMETER AsSummary Return one FSLogixDoctor.Summary object (severity counts, worst severity, monitoring-friendly ExitCode, plus the findings) instead of the raw finding stream. .PARAMETER AsJson Like -AsSummary, but returns the summary serialized as JSON - made for RMM/monitoring sensors (PRTG, Zabbix, scheduled tasks). .EXAMPLE Invoke-FslDiagnostic Full diagnostic of the local session host, findings on the pipeline. .EXAMPLE Invoke-FslDiagnostic -Hours 4 -ReportPath C:\Temp\fslogix-report.html .EXAMPLE Invoke-FslDiagnostic -ComputerName avd-0, avd-1 -ReportPath .\fleet.html Fleet diagnostic of two session hosts, merged into one report. .EXAMPLE $r = Invoke-FslDiagnostic -AsSummary; exit $r.ExitCode Monitoring wrapper: exit 0 = healthy, 1 = warnings, 2 = critical. #> [CmdletBinding()] [OutputType([pscustomobject])] param( [ValidateRange(1, 8760)] [int]$Hours = 24, [string]$LogPath = (Join-Path $env:ProgramData 'FSLogix\Logs'), [switch]$IncludeWarnings, [string]$ReportPath, [switch]$PassThru, [string[]]$ComputerName = @(), [switch]$AsSummary, [switch]$AsJson ) $findings = New-Object System.Collections.Generic.List[object] # --- Fleet mode: fan out via PowerShell remoting, merge, then fall through # to the shared output section. Each host runs the normal single-host path. if (@($ComputerName).Count -gt 0) { foreach ($computer in $ComputerName) { $hostFindings = @() if ($computer -in @($env:COMPUTERNAME, 'localhost', '.')) { Write-Verbose ("Fleet: running locally on {0}..." -f $env:COMPUTERNAME) $hostFindings = @(Invoke-FslDiagnostic -Hours $Hours -LogPath $LogPath -IncludeWarnings:$IncludeWarnings) } else { Write-Verbose ("Fleet: running remotely on {0}..." -f $computer) try { $remoteFindings = @(Invoke-Command -ComputerName $computer -ErrorAction Stop -ScriptBlock { Import-Module FSLogixDoctor -ErrorAction Stop Invoke-FslDiagnostic -Hours $using:Hours -IncludeWarnings:$using:IncludeWarnings }) # Rebuild as clean local finding objects - Invoke-Command # tacks PSComputerName/RunspaceId metadata onto everything # it returns, which pollutes the merged output. $hostFindings = @($remoteFindings | ForEach-Object { New-FslFinding -Category ([string]$_.Category) -Check ([string]$_.Check) -Severity ([string]$_.Severity) ` -Target ([string]$_.Target) -Message ([string]$_.Message) -Evidence ([string]$_.Evidence) ` -Recommendation ([string]$_.Recommendation) -HelpUri ([string]$_.HelpUri) }) } catch { $hostFindings = @(New-FslFinding -Category Environment -Check 'Fleet connectivity' -Severity Critical -Target $computer ` -Message ("Could not run the diagnostic on '{0}': {1}" -f $computer, $_.Exception.Message) ` -Recommendation 'Verify PowerShell remoting is enabled on the target (Enable-PSRemoting) and that FSLogixDoctor is installed there: Install-Module FSLogixDoctor.') } } foreach ($hostFinding in $hostFindings) { $findings.Add($hostFinding) } } $merged = @(Merge-FslFleetFinding -Finding $findings.ToArray()) $findings = New-Object System.Collections.Generic.List[object] foreach ($mergedFinding in $merged) { $findings.Add($mergedFinding) } } else { foreach ($localFinding in @(Invoke-FslLocalDiagnostic -Hours $Hours -LogPath $LogPath -IncludeWarnings:$IncludeWarnings)) { $findings.Add($localFinding) } } # Output: report, summary/JSON and/or pipeline. $severityOrder = @{ 'Critical' = 0; 'Warning' = 1; 'Info' = 2; 'Pass' = 3 } $sorted = @($findings | Sort-Object -Property @{ Expression = { $severityOrder[$_.Severity] } }, Category, Check) if ($ReportPath) { $report = $sorted | New-FslReport -Path $ReportPath -LookbackHours $Hours Write-Verbose ("Report written to {0}" -f $report.FullName) if (-not $PassThru -and -not $AsSummary -and -not $AsJson) { return $report } } if ($AsSummary -or $AsJson) { $counts = @{} foreach ($severityName in @('Critical', 'Warning', 'Info', 'Pass')) { $counts[$severityName] = @($sorted | Where-Object Severity -eq $severityName).Count } # Worst severity maps to a monitoring-friendly exit code: # 0 = healthy (Pass/Info only), 1 = warnings, 2 = critical. $worst = 'None' $exitCode = 0 if ($counts['Critical'] -gt 0) { $worst = 'Critical'; $exitCode = 2 } elseif ($counts['Warning'] -gt 0) { $worst = 'Warning'; $exitCode = 1 } elseif ($counts['Info'] -gt 0) { $worst = 'Info' } elseif ($counts['Pass'] -gt 0) { $worst = 'Pass' } $summaryObject = [pscustomobject]@{ PSTypeName = 'FSLogixDoctor.Summary' Target = ((@($sorted | ForEach-Object { [string]$_.Target } | Select-Object -Unique | Sort-Object)) -join ', ') GeneratedAt = Get-Date CriticalCount = $counts['Critical'] WarningCount = $counts['Warning'] InfoCount = $counts['Info'] PassCount = $counts['Pass'] WorstSeverity = $worst ExitCode = $exitCode Findings = $sorted } if ($AsJson) { return ($summaryObject | ConvertTo-Json -Depth 5) } return $summaryObject } $sorted } |