PowerShell Server Health Check Script for Daily Reports

Your morning routine should include a health summary for every production server — CPU load, memory pressure, disk space, critical service status, and any overnight errors — before users start calling. A PowerShell server health check script that runs at 6 AM and emails an HTML report makes that happen automatically. This post builds the complete solution: collecting system metrics with CIM, checking service status, scanning the event log, building a formatted HTML table, and emailing the report.
Collect CPU and Memory Stats
Use Get-CimInstance (the modern replacement for the deprecated Get-WmiObject) to query CPU load average and memory statistics:
function Get-SystemMetrics {
param([string]$ComputerName = $env:COMPUTERNAME)
$cs = Get-CimInstance -ClassName Win32_ComputerSystem -ComputerName $ComputerName
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName
$cpu = Get-CimInstance -ClassName Win32_Processor -ComputerName $ComputerName |
Measure-Object -Property LoadPercentage -Average
$totalMemGB = [Math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
$freeMemGB = [Math]::Round($os.FreePhysicalMemory / 1MB / 1024, 1)
$usedMemPct = [Math]::Round((($totalMemGB - $freeMemGB) / $totalMemGB) * 100, 0)
[PSCustomObject]@{
Server = $ComputerName
CPUPct = [Math]::Round($cpu.Average, 0)
TotalMemGB = $totalMemGB
FreeMemGB = $freeMemGB
UsedMemPct = $usedMemPct
}
}
$metrics = Get-SystemMetrics -ComputerName "Server01"
$metrics | Format-List
Check Disk Space Thresholds
Alert on drives where free space falls below a configurable threshold percentage:
function Get-DiskHealth {
param(
[string]$ComputerName = $env:COMPUTERNAME,
[int]$WarnPct = 20,
[int]$CritPct = 10
)
Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName $ComputerName `
-Filter "DriveType=3" |
Select-Object DeviceID,
@{N='TotalGB'; E={[Math]::Round($_.Size / 1GB, 1)}},
@{N='FreeGB'; E={[Math]::Round($_.FreeSpace / 1GB, 1)}},
@{N='FreePct'; E={[Math]::Round(($_.FreeSpace / $_.Size) * 100, 0)}},
@{N='Status'; E={
$pct = ($_.FreeSpace / $_.Size) * 100
if ($pct -lt $CritPct) { "CRITICAL" }
elseif ($pct -lt $WarnPct) { "WARNING" }
else { "OK" }
}}
}
$diskHealth = Get-DiskHealth -ComputerName "Server01"
$diskHealth | Format-Table -AutoSize
DeviceID TotalGB FreeGB FreePct Status
-------- ------- ------ ------- ------
C: 120.0 18.2 15 WARNING
D: 500.0 287.4 57 OK
Verify Critical Services Running
Check a predefined list of critical services and flag any that are stopped or missing:
function Test-CriticalServices {
param([string]$ComputerName, [string[]]$Services)
$Services | ForEach-Object {
$svc = Get-Service -Name $_ -ComputerName $ComputerName -ErrorAction SilentlyContinue
[PSCustomObject]@{
Service = $_
Status = if ($svc) { $svc.Status } else { "NOT FOUND" }
Alert = if (-not $svc -or $svc.Status -ne 'Running') { $true } else { $false }
}
}
}
$critSvcs = @('W3SVC','MSSQLSERVER','Spooler','EventLog','Winmgmt')
$svcStatus = Test-CriticalServices -ComputerName "Server01" -Services $critSvcs
$svcStatus | Format-Table -AutoSize
Scan Recent Event Log Errors
Retrieve Application and System event log errors from the past 24 hours:
function Get-RecentErrors {
param([string]$ComputerName = $env:COMPUTERNAME, [int]$HoursBack = 24)
$since = (Get-Date).AddHours(-$HoursBack)
@('Application','System') | ForEach-Object {
$log = $_
Get-EventLog -LogName $log -ComputerName $ComputerName `
-EntryType Error,Warning -After $since -ErrorAction SilentlyContinue |
Select-Object @{N='Log';E={$log}}, TimeGenerated, Source, EventID, Message |
Select-Object -First 5 # top 5 per log
}
}
$errors = Get-RecentErrors -ComputerName "Server01"
Write-Host "Recent errors/warnings: $($errors.Count)"
Build HTML Report Table
Combine all collected data into a styled HTML report using a here-string with inline CSS:
$htmlStyle = @"
<style>
body { font-family: Segoe UI, Arial, sans-serif; font-size: 13px; }
table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
th { background: #0078d4; color: white; padding: 6px 10px; text-align: left; }
td { padding: 5px 10px; border-bottom: 1px solid #ddd; }
.ok { color: green; } .warn { color: orange; } .crit { color: red; font-weight: bold; }
</style>
"@
$diskRows = $diskHealth | ForEach-Object {
$cls = switch ($_.Status) { "OK" { "ok" } "WARNING" { "warn" } "CRITICAL" { "crit" } }
"<tr><td>$($_.DeviceID)</td><td>$($_.TotalGB) GB</td><td>$($_.FreeGB) GB</td><td class='$cls'>$($_.Status)</td></tr>"
}
$html = @"
$htmlStyle
<h2>Server Health: $($metrics.Server) — $(Get-Date -Format 'yyyy-MM-dd HH:mm')</h2>
<p>CPU: $($metrics.CPUPct)% | Memory Used: $($metrics.UsedMemPct)% ($($metrics.FreeMemGB) GB free)</p>
<h3>Disk Space</h3>
<table><tr><th>Drive</th><th>Total</th><th>Free</th><th>Status</th></tr>
$($diskRows -join '')</table>
"@
Email Report to Team
Send the HTML report as an email body. Flag the subject line when any check is in a warning or critical state:
$hasAlerts = ($diskHealth | Where-Object Status -ne 'OK').Count -gt 0 -or
($svcStatus | Where-Object Alert -eq $true).Count -gt 0
$subject = if ($hasAlerts) {
"⚠ Health ALERT: $($metrics.Server) — $(Get-Date -Format 'HH:mm')"
} else {
"✓ Health OK: $($metrics.Server) — $(Get-Date -Format 'HH:mm')"
}
Send-MailMessage -From "[email protected]" -To "[email protected]" `
-Subject $subject -Body $html -BodyAsHtml -SmtpServer "smtp-relay.corp.com"
Write-Host "Health report emailed"
Common Errors and Fixes
-
HTML table formatting needs careful here-string escaping. Inside PowerShell here-strings and double-quoted strings,
<and>do not need escaping in HTML, but make sure your here-string delimiters ("@) are on their own line at column zero. Variable expansion inside here-strings is active — wrap complex expressions in$(). -
Remote WMI queries need WinRM or firewall exception.
Get-CimInstance -ComputerNameuses WS-Man (WinRM) by default. If WinRM is not enabled on target servers, queries will fail. Verify withTest-WSMan -ComputerName Server01before deploying a multi-server health check script.
Related Cmdlets / See Also
Wrapping Up
A daily server health check script turns reactive firefighting into proactive monitoring. Collect metrics with Get-CimInstance, check services, scan event logs, build an HTML table with conditional formatting, and email the result. Schedule it at 6 AM with Task Scheduler and your team starts every day already informed about the state of every production server.


