Scripts/Get-SystemHealthReport.ps1
|
<#
.SYNOPSIS Collects a comprehensive system health snapshot for IT support diagnostics. .DESCRIPTION Gathers CPU, memory, disk, uptime, and core system information into a single structured report. Designed to be run by a standard (non-administrator) user during help desk triage to capture evidence before escalation, and to be attached to a ticket per the diagnostic-report-template.md format. This script is read-only. It makes no changes to the system. It is safe to run on any Windows 10/11 workstation without administrative privileges. .NOTES Author: it-support-ops repository Run as: Standard user (no elevation required) Compatibility: Windows 10, Windows 11, PowerShell 5.1+ Output: Console output + log file in user's Documents folder .EXAMPLE .\Get-SystemHealthReport.ps1 Runs the report and saves output to: $env:USERPROFILE\Documents\IT-Diagnostics\SystemHealthReport_<timestamp>.txt .EXAMPLE .\Get-SystemHealthReport.ps1 -OutputPath "C:\Temp\report.txt" Runs the report and saves output to the specified custom path. #> [CmdletBinding()] param( # Optional custom output path. If not specified, defaults to the user's # Documents\IT-Diagnostics folder with a timestamped filename. [Parameter(Mandatory = $false)] [string]$OutputPath ) # --------------------------------------------------------------------------- # SETUP: Establish output location and logging # --------------------------------------------------------------------------- $ErrorActionPreference = "Continue" # Continue collecting data even if one section fails $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" if (-not $OutputPath) { $diagnosticsFolder = Join-Path $env:USERPROFILE "Documents\IT-Diagnostics" if (-not (Test-Path $diagnosticsFolder)) { try { New-Item -ItemType Directory -Path $diagnosticsFolder -Force | Out-Null } catch { Write-Warning "Could not create diagnostics folder. Output will be console-only." $diagnosticsFolder = $null } } if ($diagnosticsFolder) { $OutputPath = Join-Path $diagnosticsFolder "SystemHealthReport_$timestamp.txt" } } # Collect all output into an array so it can be written to console AND file # without running every command twice. $reportLines = New-Object System.Collections.Generic.List[string] function Write-ReportLine { <# Writes a line to both the console and the in-memory report buffer. Centralising this ensures console and file output never drift apart. #> param([string]$Text = "") z $Text $reportLines.Add($Text) } function Write-SectionHeader { <# Standardises section headers across the report for consistent, scannable output — important when a technician is reading this quickly during a live ticket. #> param([string]$Title) Write-ReportLine "" Write-ReportLine "=" * 70 Write-ReportLine " $Title" Write-ReportLine "=" * 70 } # --------------------------------------------------------------------------- # REPORT HEADER # --------------------------------------------------------------------------- Write-ReportLine "=" * 70 Write-ReportLine " IT SUPPORT - SYSTEM HEALTH REPORT" Write-ReportLine "=" * 70 Write-ReportLine "Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" Write-ReportLine "Computer Name: $env:COMPUTERNAME" Write-ReportLine "Current User: $env:USERNAME" Write-ReportLine "Domain/Workgroup: $env:USERDOMAIN" # --------------------------------------------------------------------------- # SECTION 1: OPERATING SYSTEM INFORMATION # --------------------------------------------------------------------------- Write-SectionHeader "OPERATING SYSTEM" try { $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop $bootTime = $os.LastBootUpTime $uptime = (Get-Date) - $bootTime Write-ReportLine "OS Name: $($os.Caption)" Write-ReportLine "OS Version: $($os.Version)" Write-ReportLine "OS Architecture: $($os.OSArchitecture)" Write-ReportLine "Last Boot Time: $($bootTime.ToString('yyyy-MM-dd HH:mm:ss'))" Write-ReportLine "Uptime: $($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m" # Flag long uptime as a potential factor in performance issues — # this gives the technician an immediate, actionable observation # rather than requiring them to do the date math manually. if ($uptime.Days -ge 14) { Write-ReportLine "NOTE: Uptime exceeds 14 days. Consider recommending a restart" Write-ReportLine " if the user is experiencing performance issues." } } catch { Write-ReportLine "ERROR: Could not retrieve OS information - $($_.Exception.Message)" } # --------------------------------------------------------------------------- # SECTION 2: CPU INFORMATION AND CURRENT USAGE # --------------------------------------------------------------------------- Write-SectionHeader "CPU" try { $cpu = Get-CimInstance -ClassName Win32_Processor -ErrorAction Stop | Select-Object -First 1 Write-ReportLine "Processor: $($cpu.Name.Trim())" Write-ReportLine "Cores: $($cpu.NumberOfCores)" Write-ReportLine "Logical Processors: $($cpu.NumberOfLogicalProcessors)" # Sample CPU usage over 3 readings to smooth out momentary spikes # that would otherwise give a misleading single-point reading. Write-ReportLine "" Write-ReportLine "Current CPU Usage (3 samples, 2 seconds apart):" $cpuSamples = Get-Counter '\Processor(_Total)\% Processor Time' ` -SampleInterval 2 -MaxSamples 3 -ErrorAction Stop $avgCpu = ($cpuSamples.CounterSamples | Measure-Object -Property CookedValue -Average).Average foreach ($sample in $cpuSamples.CounterSamples) { Write-ReportLine (" {0}: {1}%" -f $sample.Timestamp.ToString('HH:mm:ss'), [math]::Round($sample.CookedValue, 1)) } Write-ReportLine ("Average CPU Usage: {0}%" -f [math]::Round($avgCpu, 1)) if ($avgCpu -ge 85) { Write-ReportLine "FLAG: CPU usage is critically high (>85%)." } elseif ($avgCpu -ge 60) { Write-ReportLine "FLAG: CPU usage is elevated (60-85%). Monitor." } } catch { Write-ReportLine "ERROR: Could not retrieve CPU information - $($_.Exception.Message)" } # Top 10 CPU-consuming processes — gives the technician an immediate # starting point if usage is elevated, without needing a second script. Write-ReportLine "" Write-ReportLine "Top 10 Processes by CPU:" try { Get-Process -ErrorAction Stop | Sort-Object CPU -Descending | Select-Object -First 10 -Property ProcessName, Id, CPU, @{Name = "MemoryMB"; Expression = { [math]::Round($_.WorkingSet / 1MB, 1) } } | ForEach-Object { Write-ReportLine (" {0,-25} PID:{1,-8} CPU:{2,-10} Mem:{3}MB" -f ` $_.ProcessName, $_.Id, [math]::Round($_.CPU, 1), $_.MemoryMB) } } catch { Write-ReportLine "ERROR: Could not retrieve process list - $($_.Exception.Message)" } # --------------------------------------------------------------------------- # SECTION 3: MEMORY # --------------------------------------------------------------------------- Write-SectionHeader "MEMORY" try { $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop $totalMemGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2) $freeMemGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2) $usedMemGB = [math]::Round($totalMemGB - $freeMemGB, 2) $pctUsed = [math]::Round(($usedMemGB / $totalMemGB) * 100, 1) Write-ReportLine "Total Memory: ${totalMemGB} GB" Write-ReportLine "Used Memory: ${usedMemGB} GB" Write-ReportLine "Free Memory: ${freeMemGB} GB" Write-ReportLine "Usage Percentage: ${pctUsed}%" if ($pctUsed -ge 90) { Write-ReportLine "FLAG: Memory usage is critically high (>90%)." } elseif ($pctUsed -ge 75) { Write-ReportLine "FLAG: Memory usage is elevated (75-90%). Monitor." } } catch { Write-ReportLine "ERROR: Could not retrieve memory information - $($_.Exception.Message)" } Write-ReportLine "" Write-ReportLine "Top 10 Processes by Memory:" try { Get-Process -ErrorAction Stop | Sort-Object WorkingSet -Descending | Select-Object -First 10 -Property ProcessName, Id, @{Name = "MemoryMB"; Expression = { [math]::Round($_.WorkingSet / 1MB, 1) } } | ForEach-Object { Write-ReportLine (" {0,-25} PID:{1,-8} Mem:{2}MB" -f ` $_.ProcessName, $_.Id, $_.MemoryMB) } } catch { Write-ReportLine "ERROR: Could not retrieve process list - $($_.Exception.Message)" } # --------------------------------------------------------------------------- # SECTION 4: DISK # --------------------------------------------------------------------------- Write-SectionHeader "DISK USAGE" try { Get-PSDrive -PSProvider FileSystem -ErrorAction Stop | Where-Object { $_.Used -gt 0 } | ForEach-Object { $usedGB = [math]::Round($_.Used / 1GB, 2) $freeGB = [math]::Round($_.Free / 1GB, 2) $totalGB = $usedGB + $freeGB $pctFree = if ($totalGB -gt 0) { [math]::Round(($freeGB / $totalGB) * 100, 1) } else { 0 } Write-ReportLine ("Drive {0}: Used: {1}GB | Free: {2}GB | Total: {3}GB | {4}% Free" -f ` $_.Name, $usedGB, $freeGB, $totalGB, $pctFree) if ($pctFree -lt 10) { Write-ReportLine " FLAG: Critically low disk space (<10% free)." } elseif ($pctFree -lt 20) { Write-ReportLine " FLAG: Low disk space (<20% free). Monitor." } } } catch { Write-ReportLine "ERROR: Could not retrieve disk information - $($_.Exception.Message)" } # --------------------------------------------------------------------------- # SECTION 5: NETWORK ADAPTER SUMMARY # --------------------------------------------------------------------------- # A brief network summary is included here for context — the full diagnostic # detail belongs in Get-NetworkDiagnostics.ps1, but a basic adapter status # check is valuable in a general health report since network state often # correlates with reported performance issues. Write-SectionHeader "NETWORK ADAPTER SUMMARY" try { Get-NetAdapter -ErrorAction Stop | Where-Object { $_.Status -eq "Up" } | Select-Object Name, Status, LinkSpeed, MacAddress | ForEach-Object { Write-ReportLine ("Adapter: {0,-20} Status: {1,-8} Speed: {2}" -f ` $_.Name, $_.Status, $_.LinkSpeed) } } catch { Write-ReportLine "ERROR: Could not retrieve network adapter information - $($_.Exception.Message)" } # --------------------------------------------------------------------------- # SECTION 6: PENDING RESTART CHECK # --------------------------------------------------------------------------- Write-SectionHeader "PENDING RESTART STATUS" try { $rebootPending = Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" if ($rebootPending) { Write-ReportLine "FLAG: A system restart is pending (Windows Update)." } else { Write-ReportLine "No pending restart detected from Windows Update." } } catch { Write-ReportLine "Could not determine pending restart status - $($_.Exception.Message)" } # --------------------------------------------------------------------------- # REPORT FOOTER AND FILE OUTPUT # --------------------------------------------------------------------------- Write-SectionHeader "REPORT COMPLETE" Write-ReportLine "Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" if ($OutputPath) { try { $reportLines | Out-File -FilePath $OutputPath -Encoding UTF8 -ErrorAction Stop z "" z "Report saved to: $OutputPath" -ForegroundColor Green z "Attach this file to the ticket per diagnostic-report-template.md" -ForegroundColor Green } catch { Write-Warning "Could not save report to file: $($_.Exception.Message)" Write-Warning "Report was displayed in console only — copy manually if needed." } } else { z "" z "No output file path available — report displayed in console only." -ForegroundColor Yellow } |