PowerShell’s event-driven programming capabilities enable you to create responsive, automated systems that react to changes in real-time. Whether monitoring file system changes, responding to WMI events, or scheduling periodic tasks, understanding events and timers is essential for building robust automation solutions.
Understanding PowerShell Event Architecture
PowerShell’s event system allows scripts to subscribe to and respond to events generated by .NET objects, WMI, and other event sources. This asynchronous model enables your scripts to wait for specific conditions without continuously polling.
Core Event Cmdlets
PowerShell provides four primary cmdlets for event management:
Register-ObjectEvent– Subscribe to .NET object eventsRegister-EngineEvent– Create custom PowerShell eventsRegister-WmiEvent– Monitor WMI events (legacy systems)Register-CimIndicationEvent– Monitor CIM events (modern alternative)Get-Event– Retrieve pending events from the event queueRemove-Event– Clear events from the queueUnregister-Event– Remove event subscriptionsWait-Event– Block until an event occurs
File System Monitoring with FileSystemWatcher
One of the most common automation scenarios involves monitoring directories for file changes. The FileSystemWatcher class provides powerful real-time monitoring capabilities.
Basic File Monitoring Example
# Create a FileSystemWatcher object
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\Temp\Monitor"
$watcher.Filter = "*.txt"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
# Define the action script block
$action = {
$path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$timestamp = $Event.TimeGenerated
Write-Host "[$timestamp] File $changeType: $path" -ForegroundColor Green
# Log to file
$logLine = "$timestamp - $changeType - $path"
Add-Content -Path "C:\Temp\file-changes.log" -Value $logLine
}
# Register event handlers
$handlers = . {
Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -SourceIdentifier FileCreated
Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action -SourceIdentifier FileChanged
Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action $action -SourceIdentifier FileDeleted
Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action $action -SourceIdentifier FileRenamed
}
Write-Host "Monitoring C:\Temp\Monitor for changes. Press Ctrl+C to stop." -ForegroundColor Yellow
# Keep the script running
try {
while ($true) {
Start-Sleep -Seconds 1
}
}
finally {
# Cleanup
Get-EventSubscriber | Where-Object { $_.SourceIdentifier -like "File*" } | Unregister-Event
$watcher.Dispose()
Write-Host "Monitoring stopped and resources cleaned up." -ForegroundColor Red
}
Expected Output:
Monitoring C:\Temp\Monitor for changes. Press Ctrl+C to stop.
[01/22/2025 15:30:45] File Created: C:\Temp\Monitor\test.txt
[01/22/2025 15:30:52] File Changed: C:\Temp\Monitor\test.txt
[01/22/2025 15:31:10] File Deleted: C:\Temp\Monitor\test.txt
Advanced File Monitoring with Email Notifications
function Start-FileMonitorWithAlert {
param(
[string]$Path = "C:\Important\Documents",
[string[]]$Extensions = @("*.docx", "*.xlsx", "*.pdf"),
[string]$SmtpServer = "smtp.company.com",
[string]$To = "[email protected]"
)
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $Path
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
# Filter for multiple extensions
$watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor
[System.IO.NotifyFilters]::LastWrite
$action = {
param($source, $e)
$details = @{
FileName = $e.Name
FullPath = $e.FullPath
ChangeType = $e.ChangeType
Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
}
# Check if file matches our extensions
$matchesFilter = $false
foreach ($ext in $using:Extensions) {
if ($details.FileName -like $ext) {
$matchesFilter = $true
break
}
}
if ($matchesFilter) {
$subject = "File Alert: $($details.ChangeType) - $($details.FileName)"
$body = @"
A monitored file has been $($details.ChangeType.ToString().ToLower()).
File: $($details.FileName)
Full Path: $($details.FullPath)
Time: $($details.Timestamp)
Change Type: $($details.ChangeType)
"@
Send-MailMessage -To $using:To -From "[email protected]" `
-Subject $subject -Body $body `
-SmtpServer $using:SmtpServer
Write-Host "Alert sent for: $($details.FullPath)" -ForegroundColor Cyan
}
}
Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action
Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action
Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action $action
return $watcher
}
# Usage
$monitor = Start-FileMonitorWithAlert -Path "C:\Important\Documents" -Extensions @("*.docx", "*.pdf")
WMI and CIM Event Monitoring
WMI events allow you to monitor system-level changes such as process creation, service state changes, and hardware events.
Monitoring Process Creation
# Monitor when specific processes are started
$query = "SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE " +
"TargetInstance ISA 'Win32_Process' AND " +
"TargetInstance.Name = 'notepad.exe'"
$action = {
$process = $Event.SourceEventArgs.NewEvent.TargetInstance
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "[$timestamp] Notepad started!" -ForegroundColor Yellow
Write-Host " Process ID: $($process.ProcessId)" -ForegroundColor Gray
Write-Host " Command Line: $($process.CommandLine)" -ForegroundColor Gray
Write-Host " User: $($process.GetOwner().User)" -ForegroundColor Gray
# Optional: Take action
# Stop-Process -Id $process.ProcessId -Force
}
# Register the event
Register-CimIndicationEvent -Query $query -SourceIdentifier ProcessMonitor -Action $action
Write-Host "Monitoring for Notepad launches. Press Ctrl+C to stop." -ForegroundColor Green
# Keep running
try {
while ($true) { Start-Sleep -Seconds 1 }
}
finally {
Unregister-Event -SourceIdentifier ProcessMonitor
}
Sample Output:
Monitoring for Notepad launches. Press Ctrl+C to stop.
[2025-01-22 15:45:12] Notepad started!
Process ID: 8456
Command Line: "C:\Windows\System32\notepad.exe" C:\test.txt
User: DOMAIN\username
Service State Monitoring
function Watch-ServiceState {
param(
[string[]]$ServiceNames = @("Spooler", "W32Time"),
[string]$LogPath = "C:\Logs\service-monitor.log"
)
# Build WMI query for multiple services
$serviceFilter = ($ServiceNames | ForEach-Object { "Name='$_'" }) -join " OR "
$query = "SELECT * FROM __InstanceModificationEvent WITHIN 5 WHERE " +
"TargetInstance ISA 'Win32_Service' AND ($serviceFilter)"
$action = {
$service = $Event.SourceEventArgs.NewEvent.TargetInstance
$prevService = $Event.SourceEventArgs.NewEvent.PreviousInstance
if ($service.State -ne $prevService.State) {
$logEntry = @{
Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
ServiceName = $service.Name
DisplayName = $service.DisplayName
OldState = $prevService.State
NewState = $service.State
Status = $service.Status
}
$message = "$($logEntry.Timestamp) - $($logEntry.DisplayName) changed from " +
"$($logEntry.OldState) to $($logEntry.NewState)"
Write-Host $message -ForegroundColor $(if ($service.State -eq "Running") { "Green" } else { "Red" })
Add-Content -Path $using:LogPath -Value $message
# Alert if critical service stopped
if ($service.State -eq "Stopped" -and $service.Name -in @("W32Time", "Spooler")) {
Write-Host " CRITICAL: Essential service stopped!" -ForegroundColor Red
# Send alert (email, Teams, etc.)
}
}
}
Register-CimIndicationEvent -Query $query -SourceIdentifier ServiceStateMonitor -Action $action
Write-Host "Monitoring services: $($ServiceNames -join ', ')" -ForegroundColor Cyan
}
# Start monitoring
Watch-ServiceState -ServiceNames @("Spooler", "W32Time", "WinRM")
Creating Custom PowerShell Events
You can create your own events using Register-EngineEvent and New-Event, enabling inter-script communication and custom workflows.
# Create a custom event system for workflow orchestration
function Initialize-WorkflowEngine {
# Register event handlers
$dataProcessed = {
$data = $Event.MessageData
Write-Host "Stage 1 Complete: Data processed - $($data.RecordCount) records" -ForegroundColor Green
# Trigger next stage
New-Event -SourceIdentifier "DataValidation" -MessageData $data
}
$dataValidated = {
$data = $Event.MessageData
Write-Host "Stage 2 Complete: Data validated - $($data.RecordCount) records" -ForegroundColor Green
# Simulate validation
$data.IsValid = $true
# Trigger final stage
New-Event -SourceIdentifier "DataExport" -MessageData $data
}
$dataExported = {
$data = $Event.MessageData
Write-Host "Stage 3 Complete: Data exported to $($data.ExportPath)" -ForegroundColor Green
Write-Host "Workflow finished successfully!" -ForegroundColor Cyan
}
# Register the workflow stages
Register-EngineEvent -SourceIdentifier "DataProcessing" -Action $dataProcessed
Register-EngineEvent -SourceIdentifier "DataValidation" -Action $dataValidated
Register-EngineEvent -SourceIdentifier "DataExport" -Action $dataExported
}
# Initialize the workflow
Initialize-WorkflowEngine
# Start the workflow by triggering the first event
$workflowData = @{
RecordCount = 1500
ExportPath = "C:\Export\data.csv"
StartTime = Get-Date
}
Write-Host "Starting workflow..." -ForegroundColor Yellow
New-Event -SourceIdentifier "DataProcessing" -MessageData $workflowData
# Wait for all events to process
Start-Sleep -Seconds 2
# Cleanup
Get-EventSubscriber | Where-Object { $_.SourceIdentifier -like "Data*" } | Unregister-Event
Output:
Starting workflow...
Stage 1 Complete: Data processed - 1500 records
Stage 2 Complete: Data validated - 1500 records
Stage 3 Complete: Data exported to C:\Export\data.csv
Workflow finished successfully!
Timer-Based Automation
Timers enable periodic task execution without external schedulers. PowerShell offers multiple approaches for timer-based automation.
System.Timers.Timer for Periodic Tasks
function Start-PeriodicHealthCheck {
param(
[int]$IntervalSeconds = 30,
[string[]]$Services = @("W32Time", "Spooler"),
[string]$LogPath = "C:\Logs\health-check.log"
)
# Create timer
$timer = New-Object System.Timers.Timer
$timer.Interval = $IntervalSeconds * 1000 # Convert to milliseconds
$timer.AutoReset = $true
# Define health check action
$action = {
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$results = @()
Write-Host "`n[$timestamp] Running health check..." -ForegroundColor Cyan
foreach ($serviceName in $using:Services) {
$service = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($service) {
$status = if ($service.Status -eq "Running") { "✓" } else { "✗" }
$color = if ($service.Status -eq "Running") { "Green" } else { "Red" }
Write-Host " $status $($service.DisplayName): $($service.Status)" -ForegroundColor $color
$results += "$timestamp - $($service.Name) - $($service.Status)"
# Auto-remediation
if ($service.Status -ne "Running" -and $service.StartType -ne "Disabled") {
Write-Host " Attempting to restart $($service.Name)..." -ForegroundColor Yellow
try {
Start-Service -Name $service.Name -ErrorAction Stop
Write-Host " Successfully restarted $($service.Name)" -ForegroundColor Green
$results += "$timestamp - $($service.Name) - RESTARTED"
}
catch {
Write-Host " Failed to restart: $_" -ForegroundColor Red
$results += "$timestamp - $($service.Name) - RESTART_FAILED"
}
}
}
}
# Log results
Add-Content -Path $using:LogPath -Value ($results -join "`n")
# Memory check
$memory = Get-Counter '\Memory\Available MBytes' | Select-Object -ExpandProperty CounterSamples
$availMB = [math]::Round($memory.CookedValue, 2)
Write-Host " Available Memory: $availMB MB" -ForegroundColor Gray
if ($availMB -lt 1024) {
Write-Host " WARNING: Low memory detected!" -ForegroundColor Red
}
}
# Register timer event
Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action $action -SourceIdentifier HealthCheckTimer
# Start timer
$timer.Start()
Write-Host "Health check started. Checking every $IntervalSeconds seconds." -ForegroundColor Green
Write-Host "Press Ctrl+C to stop.`n" -ForegroundColor Yellow
return $timer
}
# Start monitoring
$healthTimer = Start-PeriodicHealthCheck -IntervalSeconds 30 -Services @("W32Time", "Spooler", "WinRM")
# Keep running
try {
while ($true) { Start-Sleep -Seconds 1 }
}
finally {
$healthTimer.Stop()
$healthTimer.Dispose()
Unregister-Event -SourceIdentifier HealthCheckTimer
Write-Host "`nHealth monitoring stopped." -ForegroundColor Red
}
Sample Output:
Health check started. Checking every 30 seconds.
Press Ctrl+C to stop.
[2025-01-22 16:00:00] Running health check...
✓ Windows Time: Running
✓ Print Spooler: Running
✓ Windows Remote Management (WS-Management): Running
Available Memory: 8192.45 MB
[2025-01-22 16:00:30] Running health check...
✓ Windows Time: Running
✗ Print Spooler: Stopped
Attempting to restart Spooler...
Successfully restarted Spooler
✓ Windows Remote Management (WS-Management): Running
Available Memory: 8156.32 MB
Countdown Timer with Progress
function Start-CountdownTimer {
param(
[int]$Minutes = 5,
[scriptblock]$OnComplete = { Write-Host "Timer completed!" -ForegroundColor Green },
[switch]$ShowProgress
)
$totalSeconds = $Minutes * 60
$timer = New-Object System.Timers.Timer
$timer.Interval = 1000 # 1 second
$timer.AutoReset = $true
$script:elapsedSeconds = 0
$action = {
$script:elapsedSeconds++
$remaining = $using:totalSeconds - $script:elapsedSeconds
if ($using:ShowProgress) {
$percent = [math]::Round(($script:elapsedSeconds / $using:totalSeconds) * 100, 1)
$timeLeft = [TimeSpan]::FromSeconds($remaining)
Write-Progress -Activity "Countdown Timer" `
-Status "Time remaining: $($timeLeft.ToString('mm\:ss'))" `
-PercentComplete $percent
}
if ($script:elapsedSeconds -ge $using:totalSeconds) {
$timer = $Event.MessageData
$timer.Stop()
if ($using:ShowProgress) {
Write-Progress -Activity "Countdown Timer" -Completed
}
& $using:OnComplete
Unregister-Event -SourceIdentifier CountdownTimer
}
}
Register-ObjectEvent -InputObject $timer -EventName Elapsed `
-Action $action -MessageData $timer `
-SourceIdentifier CountdownTimer
$timer.Start()
Write-Host "Countdown started: $Minutes minutes" -ForegroundColor Cyan
return $timer
}
# Usage example
$myTimer = Start-CountdownTimer -Minutes 2 -ShowProgress -OnComplete {
Write-Host "`nCountdown finished! Time to take a break." -ForegroundColor Green
[System.Media.SystemSounds]::Beep.Play()
}
Advanced Event Patterns
Event Throttling and Debouncing
function Register-ThrottledFileWatcher {
param(
[string]$Path,
[int]$ThrottleSeconds = 5,
[scriptblock]$Action
)
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $Path
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
$script:lastEventTime = @{}
$throttledAction = {
$filePath = $Event.SourceEventArgs.FullPath
$currentTime = Get-Date
# Check if enough time has passed since last event for this file
if (-not $script:lastEventTime.ContainsKey($filePath) -or
($currentTime - $script:lastEventTime[$filePath]).TotalSeconds -ge $using:ThrottleSeconds) {
$script:lastEventTime[$filePath] = $currentTime
# Execute the actual action
& $using:Action
Write-Host "Event processed for: $filePath" -ForegroundColor Green
}
else {
Write-Host "Event throttled for: $filePath" -ForegroundColor Yellow
}
}
Register-ObjectEvent -InputObject $watcher -EventName Changed `
-Action $throttledAction -SourceIdentifier ThrottledWatcher
return $watcher
}
# Usage
$watcher = Register-ThrottledFileWatcher -Path "C:\Temp\Monitor" -ThrottleSeconds 5 -Action {
Write-Host " Processing file change..." -ForegroundColor Cyan
# Your actual processing logic here
}
Event Queue Management
function Get-EventQueueStatus {
$events = Get-Event
$subscribers = Get-EventSubscriber
Write-Host "`nEvent Subscriber Status:" -ForegroundColor Cyan
Write-Host ("=" * 80) -ForegroundColor Gray
foreach ($subscriber in $subscribers) {
$eventCount = ($events | Where-Object { $_.SourceIdentifier -eq $subscriber.SourceIdentifier }).Count
Write-Host "`nSubscriber: $($subscriber.SourceIdentifier)" -ForegroundColor Yellow
Write-Host " Event Name: $($subscriber.EventName)"
Write-Host " Pending Events: $eventCount"
Write-Host " Action: $(if ($subscriber.Action) { 'Yes' } else { 'No' })"
Write-Host " Forward: $(if ($subscriber.Forward) { 'Yes' } else { 'No' })"
}
Write-Host "`nTotal Pending Events: $($events.Count)" -ForegroundColor Cyan
Write-Host "Active Subscriptions: $($subscribers.Count)" -ForegroundColor Cyan
}
function Clear-EventQueue {
param([string]$SourceIdentifier)
if ($SourceIdentifier) {
Get-Event -SourceIdentifier $SourceIdentifier | Remove-Event
Write-Host "Cleared events for: $SourceIdentifier" -ForegroundColor Green
}
else {
Get-Event | Remove-Event
Write-Host "Cleared all events from queue" -ForegroundColor Green
}
}
# Check status
Get-EventQueueStatus
# Clear specific or all events
Clear-EventQueue -SourceIdentifier "FileCreated"
Scheduled Tasks Integration
While events handle real-time responses, scheduled tasks complement automation with time-based execution. PowerShell can create and manage Windows Task Scheduler tasks programmatically.
function New-MonitoringTask {
param(
[string]$TaskName = "FileSystemMonitor",
[string]$ScriptPath = "C:\Scripts\monitor.ps1",
[string]$Schedule = "Daily",
[string]$StartTime = "00:00"
)
# Define the action
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
# Define the trigger
$trigger = switch ($Schedule) {
"Daily" {
New-ScheduledTaskTrigger -Daily -At $StartTime
}
"Startup" {
New-ScheduledTaskTrigger -AtStartup
}
"Logon" {
New-ScheduledTaskTrigger -AtLogon
}
default {
New-ScheduledTaskTrigger -Daily -At $StartTime
}
}
# Define settings
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-RunOnlyIfNetworkAvailable:$false
# Register the task
Register-ScheduledTask -TaskName $TaskName `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Description "Automated file system monitoring" `
-RunLevel Highest
Write-Host "Scheduled task '$TaskName' created successfully" -ForegroundColor Green
}
# Create a monitoring task
New-MonitoringTask -TaskName "DailyFileMonitor" -ScriptPath "C:\Scripts\file-monitor.ps1" -Schedule "Daily" -StartTime "02:00"
Error Handling and Cleanup
Robust event-driven automation requires proper error handling and resource cleanup to prevent memory leaks and orphaned event subscriptions.
function Start-RobustEventMonitoring {
param(
[string]$Path = "C:\Temp\Monitor",
[string]$LogPath = "C:\Logs\monitor-errors.log"
)
try {
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $Path
$watcher.EnableRaisingEvents = $true
$action = {
try {
$file = $Event.SourceEventArgs.FullPath
Write-Host "Processing: $file" -ForegroundColor Green
# Simulate processing that might fail
if ($file -match "\.tmp$") {
throw "Temporary files are not supported"
}
# Your actual logic here
}
catch {
$errorMsg = "[$([datetime]::Now)] Error: $_ | File: $($Event.SourceEventArgs.FullPath)"
Write-Host $errorMsg -ForegroundColor Red
Add-Content -Path $using:LogPath -Value $errorMsg
# Don't let errors stop the monitoring
}
}
Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -SourceIdentifier RobustMonitor
Write-Host "Monitoring started. Press Ctrl+C to stop." -ForegroundColor Cyan
# Keep running with proper cleanup
try {
while ($true) { Start-Sleep -Seconds 1 }
}
finally {
Write-Host "`nCleaning up..." -ForegroundColor Yellow
# Unregister events
Get-EventSubscriber -SourceIdentifier RobustMonitor -ErrorAction SilentlyContinue | Unregister-Event
# Clear pending events
Get-Event -SourceIdentifier RobustMonitor -ErrorAction SilentlyContinue | Remove-Event
# Dispose watcher
if ($watcher) {
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
}
Write-Host "Cleanup complete" -ForegroundColor Green
}
}
catch {
Write-Host "Fatal error: $_" -ForegroundColor Red
throw
}
}
# Start with error handling
Start-RobustEventMonitoring
Real-World Automation Scenario
Here’s a comprehensive example combining multiple concepts: a log aggregation system that monitors multiple sources, processes events, and sends periodic summaries.
class LogAggregator {
[System.IO.FileSystemWatcher[]]$Watchers
[System.Timers.Timer]$ReportTimer
[System.Collections.ArrayList]$LogEntries
[string]$ReportPath
LogAggregator([string[]]$paths, [int]$reportIntervalMinutes) {
$this.LogEntries = [System.Collections.ArrayList]::new()
$this.Watchers = @()
$this.ReportPath = "C:\Reports\log-summary.txt"
# Setup watchers for each path
foreach ($path in $paths) {
$watcher = [System.IO.FileSystemWatcher]::new()
$watcher.Path = $path
$watcher.Filter = "*.log"
$watcher.EnableRaisingEvents = $true
# Store watcher
$this.Watchers += $watcher
# Register event
$eventAction = {
$file = $Event.SourceEventArgs.FullPath
$content = Get-Content -Path $file -Tail 10 -ErrorAction SilentlyContinue
foreach ($line in $content) {
if ($line -match "ERROR|CRITICAL|WARN") {
$entry = @{
Timestamp = [datetime]::Now
Source = $file
Message = $line
Level = if ($line -match "ERROR") { "ERROR" }
elseif ($line -match "CRITICAL") { "CRITICAL" }
else { "WARN" }
}
$this.LogEntries.Add($entry) | Out-Null
Write-Host "[$($entry.Level)] $($entry.Message)" -ForegroundColor $(
switch ($entry.Level) {
"CRITICAL" { "Red" }
"ERROR" { "Yellow" }
default { "Gray" }
}
)
}
}
}.GetNewClosure()
Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $eventAction
}
# Setup report timer
$this.ReportTimer = [System.Timers.Timer]::new()
$this.ReportTimer.Interval = $reportIntervalMinutes * 60 * 1000
$this.ReportTimer.AutoReset = $true
$reportAction = {
$this.GenerateReport()
}.GetNewClosure()
Register-ObjectEvent -InputObject $this.ReportTimer -EventName Elapsed -Action $reportAction
$this.ReportTimer.Start()
}
[void]GenerateReport() {
$report = @"
Log Aggregation Report - $([datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss'))
$("=" * 80)
Total Entries: $($this.LogEntries.Count)
By Level:
"@
$grouped = $this.LogEntries | Group-Object -Property Level
foreach ($group in $grouped) {
$report += "`n $($group.Name): $($group.Count)"
}
$report += "`n`nRecent Critical/Errors (Last 10):`n"
$recent = $this.LogEntries |
Where-Object { $_.Level -in @("CRITICAL", "ERROR") } |
Select-Object -Last 10
foreach ($entry in $recent) {
$report += "`n[$($entry.Timestamp.ToString('HH:mm:ss'))] [$($entry.Level)] $($entry.Message)"
}
Add-Content -Path $this.ReportPath -Value $report
Write-Host "`nReport generated: $($this.ReportPath)" -ForegroundColor Cyan
# Clear old entries (keep last 1000)
if ($this.LogEntries.Count -gt 1000) {
$this.LogEntries.RemoveRange(0, $this.LogEntries.Count - 1000)
}
}
[void]Stop() {
Write-Host "Stopping log aggregator..." -ForegroundColor Yellow
# Stop timer
$this.ReportTimer.Stop()
$this.ReportTimer.Dispose()
# Stop watchers
foreach ($watcher in $this.Watchers) {
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
}
# Cleanup events
Get-EventSubscriber | Unregister-Event
# Generate final report
$this.GenerateReport()
Write-Host "Log aggregator stopped" -ForegroundColor Green
}
}
# Usage
$paths = @("C:\Logs\App1", "C:\Logs\App2", "C:\Logs\System")
$aggregator = [LogAggregator]::new($paths, 15) # 15-minute reports
Write-Host "Log aggregation started. Monitoring $($paths.Count) locations." -ForegroundColor Green
Write-Host "Press Ctrl+C to stop and generate final report.`n" -ForegroundColor Yellow
try {
while ($true) { Start-Sleep -Seconds 1 }
}
finally {
$aggregator.Stop()
}
Performance Considerations
When implementing event-driven automation, consider these performance best practices:
- Event Action Efficiency: Keep action scriptblocks lightweight; offload heavy processing to background jobs
- Memory Management: Clear processed events regularly with
Remove-Event - Resource Cleanup: Always dispose of timers and watchers properly
- Event Throttling: Implement debouncing for high-frequency events
- Subscription Limits: Monitor active subscriptions; excessive subscriptions impact performance
Monitoring Event System Health
function Get-EventSystemMetrics {
$subscribers = Get-EventSubscriber
$events = Get-Event
$metrics = [PSCustomObject]@{
ActiveSubscriptions = $subscribers.Count
PendingEvents = $events.Count
SubscriptionsWithActions = ($subscribers | Where-Object { $_.Action }).Count
OldestEventAge = if ($events) {
((Get-Date) - ($events | Sort-Object TimeGenerated | Select-Object -First 1).TimeGenerated).TotalMinutes
} else { 0 }
MemoryUsageMB = [math]::Round((Get-Process -Id $PID).WorkingSet64 / 1MB, 2)
}
Write-Host "`nEvent System Metrics:" -ForegroundColor Cyan
Write-Host ("=" * 50) -ForegroundColor Gray
$metrics.PSObject.Properties | ForEach-Object {
Write-Host "$($_.Name): $($_.Value)" -ForegroundColor White
}
# Health warnings
if ($metrics.PendingEvents -gt 100) {
Write-Host "`nWARNING: High pending event count. Consider processing or clearing." -ForegroundColor Red
}
if ($metrics.OldestEventAge -gt 60) {
Write-Host "WARNING: Events older than 1 hour detected. Check event processing." -ForegroundColor Red
}
return $metrics
}
# Check system health periodically
Get-EventSystemMetrics
Best Practices
- Always Use Try-Finally: Ensure cleanup code runs even if errors occur
- Unique SourceIdentifiers: Use descriptive, unique names for event subscriptions
- Log Everything: Maintain detailed logs for debugging event-driven systems
- Test Cleanup: Verify all resources are released when stopping monitoring
- Use -Forward Sparingly: Forward events to the event queue only when necessary
- Implement Timeouts: Add timeouts to prevent indefinite waiting
- Monitor Resource Usage: Track memory and CPU usage of long-running event handlers
- Handle Errors Gracefully: Don’t let single event failures stop entire monitoring
Troubleshooting Common Issues
Events Not Firing
# Verify event subscription
Get-EventSubscriber | Format-List *
# Check if events are queued
Get-Event
# Test watcher directly
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\Temp"
$watcher.EnableRaisingEvents = $true
# Create a test file and monitor console
Memory Leaks
# Monitor process memory
while ($true) {
$mem = [math]::Round((Get-Process -Id $PID).WorkingSet64 / 1MB, 2)
Write-Host "Memory: $mem MB | Events: $(Get-Event | Measure-Object).Count | Subscriptions: $(Get-EventSubscriber | Measure-Object).Count"
Start-Sleep -Seconds 5
}
# Regular cleanup
Get-Event | Where-Object { $_.TimeGenerated -lt (Get-Date).AddHours(-1) } | Remove-Event
Conclusion
PowerShell’s event and timer capabilities provide powerful tools for building responsive, automated systems. By mastering event subscriptions, timer-based execution, and proper resource management, you can create robust automation solutions that react to system changes in real-time while maintaining scheduled operations.
The key to successful event-driven automation lies in understanding the event architecture, implementing proper error handling, and maintaining clean resource management. Whether monitoring file systems, responding to WMI events, or orchestrating complex workflows, these patterns enable you to build scalable, maintainable automation solutions.
Start with simple monitoring scripts, gradually incorporate timers and multiple event sources, and always prioritize proper cleanup and error handling. With practice, you’ll build sophisticated automation systems that handle real-world scenarios efficiently and reliably.








