PowerShell Server Health Check Script for Daily Reports
─□✕

PowerShell Server Health Check Script for Daily Reports

PowerShell Tips Editor 3 min read
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 -ComputerName uses WS-Man (WinRM) by default. If WinRM is not enabled on target servers, queries will fail. Verify with Test-WSMan -ComputerName Server01 before 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.

Send-Item -To