Server Health Check Script

Ensuring the health and performance of servers is a critical task for IT administrators. PowerShell provides a powerful way to automate these tasks and log relevant information. In this blog, we’ll walk through a PowerShell script that gathers various health metrics from a server and logs them to the Windows Event Log. This script includes system information, uptime, patch details, CPU and memory usage, disk space, network activity, stopped services, and top resource-consuming applications.

You can schedule the script every 15/10 mins as per your need using task scheduler, this script will log the output to the Event Log, in application logs with event id 1000 and Source as “ServerCheckReport”

This script uses the thresholds for CPU and RAM usage, categorizing them as OK (0-80%), Warning (80-90%), and Critical (over 90%).

  1. Defines the log file location: The log file is named with the current year and month.
  2. Function to log data: This function adds a timestamped message to the log file.
  3. System Information: Logs system name, OS name, and total physical memory.
  4. Checks if server is Physical/Virtual
  5. System Uptime: Calculates and logs the system uptime in days.
  6. Latest Patch: Logs the latest installed patch details.
  7. CPU Usage: Logs the average CPU load percentage.
  8. Memory Usage: Calculates and logs the memory usage percentage.
  9. Virtual Memory Usage: Logs the virtual memory usage.
  10. Disk Usage: Logs the usage for each logical disk.
  11. Network Usage: Logs bytes sent and received per second for each network adapter.
  12. Automatic Services: Logs any automatically starting services that are currently stopped.
  13. RDP Logons: Logs details of successful RDP logons in the last day.
  14. Top 5 CPU Consuming Apps: Logs the top 5 processes consuming the most CPU.
  15. Top 5 RAM Consuming Apps: Logs the top 5 processes consuming the most RAM.
  16. The script also formats the log messages with timestamps, making it easier to analyze later.
# Allow all scripts to run
Set-ExecutionPolicy Unrestricted -Scope LocalMachine -Force

# Define event log source and log name
$eventSource = "ServerCheckReport"
$eventLog = "Application"

# Create event source if it doesn't exist
if (-not [System.Diagnostics.EventLog]::SourceExists($eventSource)) {
    New-EventLog -LogName $eventLog -Source $eventSource
}

# Initialize an array to hold log messages
$global:logMessages = @()

# Function to collect messages for event log
function Log-Data {
    param (
        [string]$message
    )
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "$timestamp - $message"
    $global:logMessages += $logMessage
}

# Log system information
$systemInfo = Get-WmiObject -Class Win32_OperatingSystem
Log-Data "System Name: $($systemInfo.CSName)"
Log-Data "OS Name: $($systemInfo.Caption)"
Log-Data "Total Physical Memory: $([math]::Round($systemInfo.TotalVisibleMemorySize / 1MB, 2)) GB"

# Log system uptime
$lastBootTime = $systemInfo.ConvertToDateTime($systemInfo.LastBootUpTime)
$uptime = (Get-Date) - $lastBootTime
$uptimeDays = [math]::Floor($uptime.TotalDays)
$uptimeTime = $uptime.ToString("hh\:mm\:ss")
$uptimeFormatted = "$uptimeDays days, $uptimeTime"
Log-Data "System Uptime: $uptimeFormatted"

# Determine if the server is virtual or physical
$computerSystem = Get-WmiObject -Class Win32_ComputerSystem
$manufacturer = $computerSystem.Manufacturer
$model = $computerSystem.Model

if ($manufacturer -match "Microsoft Corporation" -and $model -match "Virtual") {
    Log-Data "Server Type: Virtual (Microsoft)"
} elseif ($manufacturer -match "VMware, Inc." -or $model -match "VMware Virtual Platform") {
    Log-Data "Server Type: Virtual (VMware)"
} elseif ($manufacturer -match "Xen" -or $model -match "HVM domU") {
    Log-Data "Server Type: Virtual (Xen)"
} elseif ($manufacturer -match "KVM" -or $model -match "KVM") {
    Log-Data "Server Type: Virtual (KVM)"
} else {
    Log-Data "Server Type: Physical"
}

# Log the last 2 patches installed details 
$patches = Get-Hotfix | Sort-Object InstalledOn -Descending | Select-Object -First 2 
foreach ($patch in $patches) { 
    Log-Data "Patch Installed: $($patch.Description) (KB$($patch.HotFixID)) on $($patch.InstalledOn)"
}

# Log CPU usage
$cpuLoad = Get-WmiObject -Class Win32_Processor | Measure-Object -Property LoadPercentage -Average | Select -ExpandProperty Average
$statusCpu = if ($cpuLoad -lt 80) { "OK" } elseif ($cpuLoad -lt 90) { "Warning" } else { "Critical" }
Log-Data "CPU Load: $cpuLoad% - Status: $statusCpu"

# Log memory usage
$memoryStatus = Get-WmiObject -Class Win32_OperatingSystem
$usedMemory = $memoryStatus.TotalVisibleMemorySize - $memoryStatus.FreePhysicalMemory
$memoryUsage = [math]::Round(($usedMemory / $memoryStatus.TotalVisibleMemorySize) * 100, 2)
$statusMemory = if ($memoryUsage -lt 80) { "OK" } elseif ($memoryUsage -lt 90) { "Warning" } else { "Critical" }
Log-Data "Memory Usage: $memoryUsage% - Status: $statusMemory"

# Log virtual memory usage
$virtualMemoryStatus = Get-WmiObject -Class Win32_PageFileUsage

# Get total and used virtual memory
$totalVirtualMemory = [math]::Round($virtualMemoryStatus.AllocatedBaseSize, 2)  # Total virtual memory in MB
$usedVirtualMemory = [math]::Round($virtualMemoryStatus.CurrentUsage, 2)        # Used virtual memory in MB

# Calculate the percentage of used virtual memory
$usedVirtualMemoryPercentage = [math]::Round(($usedVirtualMemory / $totalVirtualMemory) * 100, 2)
$statusVirtualMemory = if ($usedVirtualMemoryPercentage -lt 80) { "OK" } elseif ($usedVirtualMemoryPercentage -lt 90) { "Warning" } else { "Critical" }
Log-Data "Total Virtual Memory: $totalVirtualMemory MB"
Log-Data "Used Virtual Memory: $usedVirtualMemory MB"
Log-Data "Used Virtual Memory Percentage: $usedVirtualMemoryPercentage% - Status: $statusVirtualMemory"

# Log disk usage for each logical disk
$logicalDisks = Get-WmiObject -Class Win32_LogicalDisk -Filter "DriveType=3"
foreach ($disk in $logicalDisks) {
    $freeSpaceGB = [math]::Round($disk.FreeSpace / 1GB, 2)
    $totalSpaceGB = [math]::Round($disk.Size / 1GB, 2)
    $diskUsage = [math]::Round((($disk.Size - $disk.FreeSpace) / $disk.Size) * 100, 2)
    $statusDisk = if ($diskUsage -lt 80) { "OK" } elseif ($diskUsage -lt 90) { "Warning" } else { "Critical" }
    Log-Data "Disk $($disk.DeviceID): $diskUsage% used ($freeSpaceGB GB free of $totalSpaceGB GB) - Status: $statusDisk"
}

# Log network usage
$networkAdapters = Get-WmiObject -Class Win32_PerfFormattedData_Tcpip_NetworkInterface
foreach ($adapter in $networkAdapters) {
    Log-Data "Network Adapter $($adapter.Name): Bytes Sent/sec $($adapter.BytesSentPersec), Bytes Received/sec $($adapter.BytesReceivedPersec)"
}

# Log status of services that are stopped and have startup type "Automatic"
$automaticServices = Get-WmiObject -Class Win32_Service -Filter "StartMode='Auto' AND State='Stopped'"
foreach ($service in $automaticServices) {
    Log-Data "Service $($service.DisplayName): Stopped"
}

# Get the current date and time
$currentDate = Get-Date

# Calculate the date 1 days ago
$startDate = $currentDate.AddDays(-1)

# Query the Security Event Log for successful logons (Event ID 4624) in the last 5 days
$logonEvents = Get-WinEvent -FilterHashtable @{
    LogName   = 'Security'
    ID        = 4624
    StartTime = $startDate
} | Where-Object {
    $_.Properties[8].Value -eq '10' # Logon Type 10 indicates RDP logon
}

# Process each logon event
foreach ($event in $logonEvents) {
    $userName = $event.Properties[5].Value
    $sourceIP = $event.Properties[18].Value # Source IP address
    $logonTime = $event.TimeCreated
    Log-Data "User: $userName logged in via RDP at $logonTime from IP: $sourceIP"
}

# Log top 5 apps consuming CPU in percentage
$topCpuProcesses = Get-Process | Sort-Object -Property CPU -Descending | Select-Object -First 5
Log-Data "Top 5 CPU Consuming Apps:"
foreach ($process in $topCpuProcesses) {
    $cpuUsageSample = Get-Counter -Counter "\Process($($process.Name))\% Processor Time" -SampleInterval 1 -MaxSamples 1
    $cpuPercent = [math]::Round($cpuUsageSample.CounterSamples[0].CookedValue / (Get-WmiObject Win32_ComputerSystem).NumberOfLogicalProcessors, 2)
    Log-Data "App: $($process.Name) - CPU: $cpuPercent%"
}

# Log top 5 apps consuming RAM in percentage
$totalMemory = (Get-WmiObject Win32_ComputerSystem).TotalPhysicalMemory
$topRamProcesses = Get-Process | Sort-Object -Property WorkingSet64 -Descending | Select-Object -First 5
Log-Data "Top 5 RAM Consuming Apps:"
foreach ($process in $topRamProcesses) {
    $ramPercent = [math]::Round((($process.WorkingSet64 / $totalMemory) * 100), 2)
    Log-Data "App: $($process.Name) - RAM: $ramPercent%"
}

# Insert a line break at the end
Log-Data "`n"

# Write consolidated log message to event log
if ($global:logMessages.Count -gt 0) {
    $consolidatedMessage = [string]::Join("`n", $global:logMessages)
    Write-EventLog -LogName $eventLog -Source $eventSource -EntryType Information -EventId 1000 -Message $consolidatedMessage
}

Write-Host "Performance, RDP logon data, and app information logged to Windows Event Log"

Script to get list of Patches installed on a Windows server in last N days….

Below is the simple script in which you provide the list of servers in the ServersList.txt file from which you want the data, this script will get list of patches installed on the serves in last 50 days.

You can modify the days as per your requirement, just modify the number 50 with your desired number in line No 3.

$computers = Get-Content -Path "C:\Scripts\whatupdatesinstalled\ServersList.txt"
foreach ($computer in $computers) {
Get-Hotfix -Computername $computer | Where-Object -Property InstalledOn -GE (Get-Date).AddDays(-50)
}

Get Windows Services status of multiple servers using PowerShell

Below script will check the status of Windows Update service on the servers provided in the file ServersList.txt.

##Provide the list of servers in the ServersList.txt file

get-content C:\TEMP\ServiceStatus\ServersList.txt |
ForEach-Object { Get-Service -Name wuauserv -ComputerName $_ } |
Select-Object -Property Name, Status, MachineName, starttype |
Sort-Object -Property MachineName |
Format-Table -AutoSize

Get LastLogonDate of users from AD

Below is the PowerShell script to get last logon date for users from Active Directory, you need to specify the user for which you want details in the the users.txt file.

Output will be saved in LoginDetails.csv file.

#Import Active Directory PowerShell Module
Import-Module ActiveDirectory


# Read servers from Notepad file
$users = Get-Content -Path "D:\Scripts\Last_login\users.txt"

# Iterate through users
$output = foreach ($user in $users) {
  #Write-Host "Getting Last Login for $user..."
  Get-ADUser -filter {SamAccountName -eq $user } -Properties "LastLogonDate" | select SamAccountName, name, Enabled, LastLogonDate
  }
  $output | Export-Csv "D:\Scripts\Last_login\LoginDetails.csv" -Encoding UTF8 -NoTypeInformation

Install PowerShell Module on Offline Server

Many people face challenge when they want to install an PowerShell module on an offline server, please follow below steps to install the module, in our example we will import ImportExcel module

Step 1: Download the Module on an Online Machine

  1. Open PowerShell on an Online Machine: Launch PowerShell with administrative privileges.
  2. Find the Module: Use the Find-Module cmdlet to search for the module you need. For example, if you are searching for the ImportExcel module, run:
Find-Module -Name ImportExcel

3. Save the Module Locally: Use the Save-Module cmdlet to download the module to a specified directory on your local machine. For example:

Save-Module -Name ImportExcel -Path "C:\Path\To\Save"

This will download the module and all its dependencies to the specified path.

Step 2: Transfer the Module to the Offline Machine

  1. Copy the Module: Transfer the downloaded module files from the online machine to the offline machine. You can use a USB drive, network share, or any other method to copy the files.

2. Copy the Module Folder: Copy the entire folder containing the module to the C:\Program Files\WindowsPowerShell\Modules directory on the offline machine.

3. Import the Module: After copying, you can import the module using the Import-Module cmdlet

Import-Module ImportExcel

4. Verify the Installation: You can verify the module is installed by running:

Get-Module -ListAvailable

Output should be like above.

WinDbg

WinDbg (Windows Debugger) is a powerful debugging tool developed by Microsoft. It’s used for analyzing and debugging both user-mode and kernel-mode applications on Windows.

  1. Open the tool and Set Symbol path – this is needed to properly analyze the dump file.

2. Set the symbol path as below and click on OK.

.sympath srv*C:\MyServerSymbols*https://msdl.microsoft.com/download/symbols

3. Now open the dump file that you want to analyze.

4. at the command prompt in Windbg tool enter below commands to analyze the dump file.

.symfix
.reload
!analyze -v

ProcDump

ProcDump is a versatile command-line tool primarily used for monitoring applications and generating crash dumps.

Download ProcDump from Microsoft official page and extract it in a folder on the server.

Open command prompt as administrator and you can run below commands.

Command SyntaxPurpose
procdump64.exe -i -mato monitor and create a full dump file when one or more processes crashs
procdump64.exe -c 20 -s 15 <ProcessName>command to write a minidump file when the process exceeds the 20% processor usage for 15 seconds
procdump -ma <APPName-OR-PID>command to create a full dump file for an application
procdump <PROCESS-ID>command to create a dump file using the process ID of the app
procdump.exe -uCommand to stop Procdump

Copy Latest file from Source to Destination using PowerShell and rename it.

#Set Source and Destination Directories
$SourceDir = "D:\Copy_Test\Source"
$DestDir = "D:\Copy_Test\Destination"
$LatestFile = ""

#Cleanup Destination Directory before copying file
Remove-Item -Path $DestDir\*.xlsx -Force

#Get Latest file from the Source Directory
$LatestFile = dir $SourceDir *.xlsx | Sort-Object -Descending LastWriteTime | Select-Object -First 1
Write-Host $LatestFile

#Copy the Latest file to Destination Directory
Copy-Item -path "$SourceDir\$LatestFile" "$DestDir\$LatestFile"

write-host "Copying" $LatestFile to $DestDir

#Rename/Change extention from xlsx to csv of the latest file that is coped at the destination.
#Rename-Item $DestDir\$LatestFile -NewName ($LatestFile.Name.Replace('.xlsx','.csv'))

PowerShell script to get VM’s restarted by HA in last 24 hours on Mail.

As an IT administrator, maintaining visibility into the health and activity of your virtual infrastructure is crucial. In a VMware environment, tracking virtual machine (VM) restarts initiated by High Availability (HA) ensures you stay informed about any unexpected events. In this blog, we’ll explore a PowerShell script that connects to a vCenter server, collects VM restart events, and sends an email report with styled HTML content.

# Import VMware module
if (-not (Get-Module -Name VMware* -ListAvailable)) {
    Install-Module -Name VMware.PowerCLI -Scope CurrentUser
}
Import-Module VMware.PowerCLI

# Define the execution server name
$executionServer = $env:COMPUTERNAME

# Connect to our vCenter Server using the logged-in credentials
Connect-VIServer <vCenterIP/FQDN> -User '<Username@domain.com>' -Password '<password>'

write-host "Connected to vCenter, collecting details, please wait...."

# HTML Head
$head = @'
<style>
body { background-color:#FFFFFF; font-family:calibri; font-size:11pt; }
td, th { border:1px solid black; border-collapse:collapse; }
th { color:white; background-color:#4CAF50; } /* Green headers */
table, tr, td, th { padding: 2px; margin: 0px }
table { margin-left:50px; }
.message-green { color: #228B22; font-size: 18pt; font-weight: bold; font-family: Arial; }
.execution-red { color: #B22222; font-size: 12pt; font-family: Arial; }
</style>
'@

# Get VM restart events
$events = Get-VIEvent -MaxSamples 100000 -Start (Get-Date).AddDays(-1) -Type Warning |
          Where {$_.FullFormattedMessage -match "restarted"}

if ($events.Count -eq 0) {
    $Output = "<p class='message-green'>No VMs Restarted by HA in last 24 hours</p>"
} else {
    $Output = $events | ForEach-Object {
        $vm = Get-View -Id $_.Vm.VM
        $vmName = $vm.Name
        $guestOS = $vm.Summary.Config.GuestFullName
        $resourcePool = Get-View -Id $vm.ResourcePool
        $parent = $resourcePool.Parent
        while ($parent.Type -ne "ClusterComputeResource") {
            $resourcePool = Get-View -Id $parent
            $parent = $resourcePool.Parent
        }
        $clusterName = (Get-View -Id $parent).Name
        $timestamp = $_.CreatedTime
        [PSCustomObject]@{
            Timestamp    = $timestamp
            VMName       = $vmName
            GuestOS      = $guestOS
            ClusterName  = $clusterName
        }
    } | ConvertTo-Html -Head $head -PreContent "<h1>VMs Restarted by HA in last 24 hours</h1>" -Property VMName, GuestOS, ClusterName, Timestamp
}

$finalout = "<html><head>$head</head><body>$Output<p class='execution-red'>The script was executed on server $executionServer.</p></body></html>"

# Send email to Admins
$smtpServer = "<IP Address>"
$smtpFrom = "email@domain.com"
$smtpTo =  "email@domain.com, email@domain.com"
$messageSubject = "List of VMs restarted by HA in last 24 Hrs"
$message = New-Object System.Net.Mail.MailMessage $smtpFrom, ($smtpTo -join ",")
$message.Subject = $messageSubject
$message.IsBodyHTML = $true
$message.Body = $finalout
$smtp = New-Object Net.Mail.SmtpClient($smtpServer)
$smtp.Send($message)

# Disconnect from our vCenter Server
Disconnect-VIServer -Confirm:$false

PowerShell Script to Ping and Telnet RDP port 3389 on multiple servers

$Header = @"
<style>
TABLE {border-width: 1px; border-style: solid; border-color: black; border-collapse: collapse;}
TH {border-width: 1px; padding: 3px; border-style: solid; border-color: black; background-color: #6495ED;}
TD {border-width: 1px; padding: 3px; border-style: solid; border-color: black;}
</style>
"@
# Continue even if there are errors 
$ErrorActionPreference = "Continue";
Write-Host "Ping and Telenet on 3389 started on servers listed in servers.txt file......." -ForegroundColor black -BackgroundColor green
$computers = Get-Content "E:\RDP_Check\servers.txt"

$output = foreach($computer in $computers)
{
Write-Host "Checking Server" $computer 
 Test-NetConnection -ComputerName $computer -Port 3389 | select ComputerName, RemoteAddress, PingSucceeded, TcpTestSucceeded
}

$output | ConvertTo-Html -Head $Header | Out-File out.html
Write-Host "Script Completed ouptput saved in out.html file.!!" -ForegroundColor black -BackgroundColor green