Back to blog
PowerShell

Building a CPU Monitoring Agent in PowerShell

Introduction

System monitoring does not always need expensive software or complex tools. Sometimes a simple PowerShell script can do the job well. It can watch the system, log what happens, and respond when something goes wrong.

In this post, I explain a monitoring script I built. It watches CPU usage across the whole system and for individual processes, writes logs in multiple places, and can stop processes that use too much CPU.

Setting Up the Logging

Before the script can monitor anything, it needs places to save logs. This script writes to three places: one main log file, daily log files, and the Windows Event Log.

$LogFile = "C:\Lab1_Monitoring_Final.log"
$DailyFolder = "C:\Logs"
$EventSource = "Lab1-Monitoring"

if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {
    New-EventLog -LogName Application -Source $EventSource
}

The Event Log source is only created if it does not already exist. This means the script can be run again without causing errors. The daily log file also uses the current date, so a new file is created each day automatically.

The Write-LabLog Function

Instead of writing log code everywhere in the script, I used one function to handle all logging. This keeps the format the same and makes the script easier to manage.

function Write-LabLog {
    param(
        [Parameter(Mandatory)]
        [string]$Detected,

        [Parameter(Mandatory)]
        [string]$Action,

        [ValidateSet("Info", "Warning", "Error")]
        [string]$Severity = "Info",

        [int]$EventId = 1000
    )

    $Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $Message = "$Timestamp | Detected: $Detected | Action: $Action | By: $Ident"

    Write-Host $Message
    Write-EventLog -LogName Application -Source $EventSource -EntryType $EntryType -EventId $EventId -Message $Message
    Add-Content -Path (Get-DailyLogPath) -Value $Message -Encoding UTF8
    Add-Content -Path $LogFile -Value $Message -Encoding UTF8
}

Each log entry includes the time, what was detected, what action was taken, and which user ran the script. The script also uses different severity levels and Event IDs, which makes it easier to filter logs later.

Monitoring Total CPU Usage

The script checks total CPU usage using a performance counter. It does not log every small spike. Instead, it looks for CPU usage above 80% for 30 seconds, which is more likely to be a real problem.

$CpuTotal = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue

if ($CpuTotal -gt 80) {
    if (-not $CpuHighStart) {
        $CpuHighStart = Get-Date
    }
    elseif ((New-TimeSpan -Start $CpuHighStart -End (Get-Date)).TotalSeconds -ge 30) {
        Write-LabLog -Detected ("CPU total above 80% for 30 seconds (current = {0}%)" -f [int]$CpuTotal) `
            -Action "Recorded threshold breach" `
            -Severity Warning `
            -EventId 1100

        $CpuHighStart = $null
    }
}
else {
    $CpuHighStart = $null
}

When CPU usage goes above 80%, the script starts a timer. If it stays above that level for 30 seconds, the script logs it. If CPU usage drops before that, the timer resets. This helps avoid false alarms from short spikes.

Tracking High-CPU Processes

It is useful to know when total CPU is high, but it is even more useful to know which process is causing it. This script checks individual processes and calculates how much CPU each one uses over time.

$CpuDelta = $CpuNow - [double]$PreviousProcessStats[$ProcessId].Cpu
$AverageCpu = [math]::Round(($CpuDelta / $ElapsedSeconds) * 100, 1)

if ($AverageCpu -gt 80) {
    try {
        Stop-Process -Id $ProcessId -Force

        Write-LabLog -Detected ("Process {0} (PID {1}) averaged about {2}% CPU" -f $Process.ProcessName, $ProcessId, $AverageCpu) `
            -Action "Remediation: terminated process" `
            -Severity Error `
            -EventId 1101
    }
    catch {
        Write-LabLog -Detected ("Process {0} (PID {1}) was above threshold" -f $Process.ProcessName, $ProcessId) `
            -Action "Remediation failed: could not terminate process" `
            -Severity Warning `
            -EventId 1102
    }
}

The script compares CPU time between loop cycles to estimate average CPU usage for each process. If a process stays above 80%, the script tries to stop it. If that works, it logs the action. If it fails, it logs the failure instead.

Why Use Multiple Log Locations

The script writes logs to the console, the Windows Event Log, a daily log file, and a main log file. This may seem like a lot, but each one has a purpose.

The console is useful during testing. The Event Log works well with Event Viewer, Task Scheduler, and SIEM tools. Daily logs make it easy to review one day at a time. The main log file gives one full record of everything the script has ever logged.

Possible Improvements

The current script logs events and can stop high-CPU processes, but it does not send alerts. A useful next step would be adding email or webhook notifications when the script takes action.

Another good improvement would be an exclusion list. This would prevent the script from stopping important system processes by mistake.

Conclusion

This project shows how PowerShell can be used for real system administration tasks. It can monitor system activity, work with built-in Windows tools like the Event Log and performance counters, and respond automatically when problems happen.

The script itself is not very complicated. The important part is choosing good thresholds, logging clearly, and deciding what action the script should take when it detects an issue.