前面幾天,我們已經讓 PowerShell 可以檢查:
CPU
Memory
Disk
Service
Uptime
Last Boot Time
到 Day 8,已經可以大概知道:
這台 Server 現在的資源與重要 Service 看起來是否正常?
但實際做 Windows 維運時,常常會遇到另一種情況:
CPU 正常
Memory 正常
Disk 正常
Service 也是 Running
使用者卻還是說:
「系統剛剛有問題。」
這時候我通常還會去看另一個地方:
Windows Event Log。
也就是大家很熟悉的:
Event Viewer
事件檢視器
以前可能是:
Win + R
↓
eventvwr.msc
↓
Windows Logs
↓
System / Application
↓
Filter Current Log
↓
慢慢找 Error
今天我們就把這個流程交給 PowerShell。
目標是:
Windows Event Log
↓
只找最近 24 小時
↓
Critical / Error / Warning
↓
整理 Event ID
↓
找出重複發生的事件
↓
輸出 CSV
為什麼 Event Log 值得自動化?
Event Viewer 本身其實很好用。
如果只是偶爾排查單一問題,我還是會直接打開:
eventvwr.msc
但假設每天都要巡檢:
SERVER01
SERVER02
SERVER03
SERVER04
...
然後每一台都:
開 Event Viewer
點 System
Filter Error
看最近一天
這就開始變成很適合自動化的工作。
尤其 Event Log 最大的問題不是「沒有資料」。
而是:
資料太多。
一台長期運作的 Windows Server,可能累積大量:
Information
Warning
Error
Critical
我們真正要做的是:
從很多 Event 裡,快速找出值得進一步確認的東西。
今天主要使用 Get-WinEvent
PowerShell 可以透過:
Get-WinEvent
讀取 Windows Event Log。
先看看有哪些 Log:
Get-WinEvent -ListLog *
通常會看到非常多項目。
例如:
Application
Security
Setup
System
Windows PowerShell
Microsoft-Windows-...
今天先不要一次處理全部。
我們先鎖定最常用的兩個:
System
Application
先讀 System Log
最直接可以:
Get-WinEvent -LogName System
但不建議直接這樣跑完整紀錄。
因為可能非常多。
先只看最近 20 筆:
Get-WinEvent -LogName System
-MaxEvents 20
會看到類似:
TimeCreated Id LevelDisplayName ProviderName
2026/09/17 05:30 7036 Information Service Control Manager
2026/09/17 05:28 10016 Warning DistributedCOM
2026/09/17 05:20 6005 Information EventLog
這裡幾個很重要的欄位:
TimeCreated
Id
LevelDisplayName
ProviderName
Message
後面我們產生報表主要就是看這些。
Event ID 是什麼?
每一個 Windows Event 通常都會有:
Event ID
例如:
7036
6005
6006
41
10016
它可以幫助我們判斷:
到底是哪一類事件。
不過要注意:
只看 Event ID 不一定足夠。
因為不同 Provider 可能使用相同的 Event ID。
所以排查時最好一起看:
ProviderName
+
Event ID
+
Message
而不是只看到:
Event ID = 1234
就直接下結論。
Event Level 有哪些?
Windows Event 常見 Level 大概可以看到:
Critical
Error
Warning
Information
在 Get-WinEvent 的 Level 數值裡,大致是:
Level 意義
1 Critical
2 Error
3 Warning
4 Information
5 Verbose
今天我們主要想找:
Critical
Error
Warning
也就是:
1
2
3
不要先抓全部再 Where-Object
一開始很容易寫成:
Get-WinEvent -LogName System |
Where-Object {
$_.LevelDisplayName -eq "Error"
}
這樣不是不能用。
但如果 Event Log 很大,就代表:
先把大量 Event 讀進來
↓
再交給 Where-Object 篩選
效率通常不是很好。
Get-WinEvent 本身提供:
-FilterHashtable
可以讓我們在讀取時就先限定條件。
FilterHashtable
例如只找 System Log 裡面的 Error:
Get-WinEvent `
-FilterHashtable @{
LogName = "System"
Level = 2
}
這裡:
@{
}
叫做:
Hashtable
可以先把它理解成:
一組「欄位 = 條件」。
例如:
@{
LogName = "System"
Level = 2
}
就是:
LogName 必須是 System
而且
Level 必須是 Error
我們真正想要的是最近 24 小時
如果 Server 已經跑半年,直接抓所有 Error 一樣沒有太大意義。
我們現在比較想知道:
最近一天發生了什麼?
先建立:
$StartTime = (Get-Date).AddHours(-24)
如果現在:
2026/09/17 05:30
那 $StartTime 就會是:
2026/09/16 05:30
接著:
Get-WinEvent `
-FilterHashtable @{
LogName = "System"
StartTime = $StartTime
}
這樣就只會抓最近 24 小時。
再加入 Critical、Error、Warning
可以:
$Events = Get-WinEvent `
-FilterHashtable @{
LogName = "System"
StartTime = $StartTime
Level = 1, 2, 3
}
現在:
$Events
就是:
最近 24 小時
+
System Log
+
Critical / Error / Warning
這已經很接近每日巡檢需要的資料。
只留下我們真的想看的欄位
直接輸出 Event Object 還是有很多資訊。
所以使用:
$Events |
Select-Object `
TimeCreated,
Id,
LevelDisplayName,
ProviderName,
Message
結果大概會變成:
TimeCreated
Id
LevelDisplayName
ProviderName
Message
這幾個欄位基本上已經很夠我們做第一輪判斷。
先看看最近 24 小時有幾筆異常事件
可以:
$Events.Count
假設結果:
48
代表:
最近 24 小時共有 48 筆 Critical、Error 或 Warning Event。
但這個數字本身其實還不能直接代表 Server 很糟。
因為可能:
同一個 Warning
每隔幾分鐘一直重複出現
所以接下來更有用的是:
哪些 Event ID 出現最多次?
找出最常發生的 Event ID
可以使用:
$Events |
Group-Object Id
這裡第一次使用:
Group-Object
它會把相同的資料分在一起。
例如原本:
Event ID 10016
Event ID 10016
Event ID 7031
Event ID 10016
Event ID 55
Event ID 7031
經過:
Group-Object Id
之後會變成:
Count Name
3 10016
2 7031
1 55
這就非常實用。
因為比起看到:
今天有 50 個 Warning。
我更想知道:
是哪個 Warning 重複了 40 次?
再按照次數排序
可以:
$Events |
Group-Object Id |
Sort-Object Count -Descending
結果:
Count Name
35 10016
8 7031
3 55
2 41
現在最值得先看的可能就是:
10016 → 35 次
不過跟前面說的一樣:
Event ID 最好不要脫離 ProviderName 單獨判斷。
更實用:Event ID + Provider 一起 Group
可以建立自訂資料:
$EventSummary = $Events |
Group-Object Id, ProviderName |
Sort-Object Count -Descending
再看:
$EventSummary
可能:
Count Name
35 10016, Microsoft-Windows-DistributedCOM
8 7031, Service Control Manager
3 55, Ntfs
這就比單純看 Event ID 更有意義。
Warning 不一定代表故障
這一點跟前幾天一樣。
假設你打開一台 Windows Server:
Warning 30 筆
不能直接說:
Server 有 30 個問題。
例如有些環境可能長期存在:
DistributedCOM Warning
但實際上服務完全正常。
所以我們真正應該注意的是:
Critical
新的 Error
短時間大量重複
跟使用者回報時間吻合的 Event
重要 Service 相關 Error
Disk / NTFS / Hardware 類型 Event
因此 Event Log 自動化的目的不是:
所有 Warning 都通知工程師。
而是:
先幫工程師把值得看的東西整理出來。
先把 Critical 和 Error 分開計算
例如:
$CriticalEvents = $Events |
Where-Object {
$_.LevelDisplayName -eq "Critical"
}
$ErrorEvents = $Events |
Where-Object {
$_.LevelDisplayName -eq "Error"
}
$WarningEvents = $Events |
Where-Object {
$_.LevelDisplayName -eq "Warning"
}
接著:
$CriticalCount = $CriticalEvents.Count
$ErrorCount = $ErrorEvents.Count
$WarningCount = $WarningEvents.Count
最後:
Critical : 0
Error : 5
Warning : 42
第一眼就比:
Total Events = 47
容易理解很多。
Application Log 也用同一套方式
除了:
System
應用程式問題通常會進:
Application
所以只需要把:
LogName = "System"
改成:
LogName = "Application"
例如:
$ApplicationEvents = Get-WinEvent `
-FilterHashtable @{
LogName = "Application"
StartTime = $StartTime
Level = 1, 2, 3
}
同樣可以:
$ApplicationEvents |
Select-Object `
TimeCreated,
Id,
LevelDisplayName,
ProviderName,
Message
這樣我們就同時有:
System Events
Application Events
如果最近完全沒有 Event 呢?
這裡會遇到一個實際的小問題。
假設:
Get-WinEvent
找不到符合條件的 Event,有時候可能產生錯誤訊息。
所以可以配合 Day 6 學到的:
try
catch
例如:
try {
$SystemEvents = Get-WinEvent `
-FilterHashtable @{
LogName = "System"
StartTime = $StartTime
Level = 1, 2, 3
} `
-ErrorAction Stop
}
catch {
$SystemEvents = @()
}
這樣即使沒有符合條件的事件,後面的 Script 還是可以繼續。
但 catch 不一定代表系統真的錯了
這裡也要小心。
有些狀況是:
真的無法讀 Event Log
有些則只是:
沒有任何符合條件的 Event
正式 Script 最好還是把錯誤:
$_.Exception.Message
留下來。
例如:
catch {
$SystemEvents = @()
$EventReadError = $_.Exception.Message
}
這樣後面才知道:
是真的零筆,還是讀取過程出問題。
做一份 Event Summary
接著把結果做成:
$EventSummary = [PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
CheckTime = Get-Date
StartTime = $StartTime
CriticalCount = (
$SystemEvents |
Where-Object LevelDisplayName -eq "Critical"
).Count
ErrorCount = (
$SystemEvents |
Where-Object LevelDisplayName -eq "Error"
).Count
WarningCount = (
$SystemEvents |
Where-Object LevelDisplayName -eq "Warning"
).Count
}
例如:
ComputerName : SERVER01
CheckTime : 2026/09/17 06:00
StartTime : 2026/09/16 06:00
CriticalCount : 0
ErrorCount : 3
WarningCount : 28
這就是很基本的:
Event Log Daily Summary
哪些 Event 應該優先注意?
今天我們可以先定一個很簡單的規則:
有 Critical
→ Critical
沒有 Critical,但有 Error
→ Warning
只有 Warning
→ Review
完全沒有
→ Normal
例如:
if ($CriticalCount -gt 0) {
$EventStatus = "Critical"
}
elseif ($ErrorCount -gt 0) {
$EventStatus = "Warning"
}
elseif ($WarningCount -gt 0) {
$EventStatus = "Review"
}
else {
$EventStatus = "Normal"
}
不過這裡一樣只是 Demo 邏輯。
正式環境不建議:
只要有一筆 Error,整台 Server 就一定是 Warning。
因為很多 Server 可能存在已知、無影響的 Event。
後面可以再慢慢加入:
Ignore List
Known Event
Critical Event ID
Provider Filter
讓規則更精準。
建立 Event Detail Report
除了 Summary,我們還是要保留明細。
可以:
$EventDetails = $SystemEvents |
Select-Object `
@{Name="ComputerName"; Expression={$env:COMPUTERNAME}},
TimeCreated,
Id,
LevelDisplayName,
ProviderName,
Message
這時:
$EventDetails
就會是一份完整的事件清單。
例如:
ComputerName TimeCreated ID Level Provider
SERVER01 05:23 7031 Error Service Control Manager
SERVER01 05:17 10016 Warning DistributedCOM
SERVER01 04:58 55 Error Ntfs
Message 可能很長
Event Log 的:
Message
有時候非常長。
放進 CSV 沒問題,但如果只是要做簡單摘要,可能不好閱讀。
所以我們可以保留完整 Message 在:
Event_Detail.csv
Summary 則只顯示:
Count
Event ID
Provider
Level
這樣比較合理。
也就是:
Summary
→ 快速看問題
Detail
→ 真正排查時使用
這個觀念跟前面的:
Server Summary
Disk Detail
Service Detail
其實是一樣的。
找出發生最多次的事件
再建立一份:
$TopEvents = $SystemEvents |
Group-Object Id, ProviderName |
Sort-Object Count -Descending |
Select-Object -First 10
例如:
Count Name
31 10016, Microsoft-Windows-DistributedCOM
7 7031, Service Control Manager
4 55, Ntfs
這份資料其實非常適合每日巡檢。
因為工程師不用從幾百筆 Event 中找:
哪一類事件一直重複?
PowerShell 直接幫我們整理好了。
讓 TopEvents 更適合 CSV
Group-Object 的結果直接輸出並不是最漂亮。
可以再轉一次:
$TopEvents = $SystemEvents |
Group-Object Id, ProviderName |
Sort-Object Count -Descending |
Select-Object -First 10 |
ForEach-Object {
$FirstEvent = $_.Group[0]
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
EventID = $FirstEvent.Id
Provider = $FirstEvent.ProviderName
Level = $FirstEvent.LevelDisplayName
Count = $_.Count
}
}
結果變成:
ComputerName EventID Provider Level Count
SERVER01 10016 DistributedCOM Warning 31
SERVER01 7031 Service Control Manager Error 7
SERVER01 55 Ntfs Error 4
這就很好拿去做報表。
今天完整 Script
把今天內容整理成一支簡單的 Event Log 巡檢:
$ComputerName = $env:COMPUTERNAME
$CheckTime = Get-Date
$StartTime = (Get-Date).AddHours(-24)
$ReportFolder = "C:\Temp"
$Date = Get-Date -Format "yyyyMMdd"
if (-not (Test-Path $ReportFolder)) {
New-Item `
-Path $ReportFolder `
-ItemType Directory |
Out-Null
}
try {
$SystemEvents = Get-WinEvent `
-FilterHashtable @{
LogName = "System"
StartTime = $StartTime
Level = 1, 2, 3
} `
-ErrorAction Stop
$EventReadError = ""
}
catch {
$SystemEvents = @()
$EventReadError = $_.Exception.Message
}
$CriticalCount = (
$SystemEvents |
Where-Object {
$_.LevelDisplayName -eq "Critical"
}
).Count
$ErrorCount = (
$SystemEvents |
Where-Object {
$_.LevelDisplayName -eq "Error"
}
).Count
$WarningCount = (
$SystemEvents |
Where-Object {
$_.LevelDisplayName -eq "Warning"
}
).Count
if ($CriticalCount -gt 0) {
$EventStatus = "Critical"
}
elseif ($ErrorCount -gt 0) {
$EventStatus = "Warning"
}
elseif ($WarningCount -gt 0) {
$EventStatus = "Review"
}
else {
$EventStatus = "Normal"
}
$Summary = [PSCustomObject]@{
ComputerName = $ComputerName
CheckTime = $CheckTime
StartTime = $StartTime
CriticalCount = $CriticalCount
ErrorCount = $ErrorCount
WarningCount = $WarningCount
EventStatus = $EventStatus
ReadError = $EventReadError
}
$EventDetails = $SystemEvents |
Select-Object `
@{Name="ComputerName"; Expression={$ComputerName}},
TimeCreated,
Id,
LevelDisplayName,
ProviderName,
Message
$TopEvents = $SystemEvents |
Group-Object Id, ProviderName |
Sort-Object Count -Descending |
Select-Object -First 10 |
ForEach-Object {
$FirstEvent = $_.Group[0]
[PSCustomObject]@{
ComputerName = $ComputerName
EventID = $FirstEvent.Id
Provider = $FirstEvent.ProviderName
Level = $FirstEvent.LevelDisplayName
Count = $_.Count
}
}
$Summary |
Export-Csv -Path "$ReportFolder\Event_Summary_$Date.csv"
-NoTypeInformation `
-Encoding UTF8
$EventDetails |
Export-Csv -Path "$ReportFolder\Event_Detail_$Date.csv"
-NoTypeInformation `
-Encoding UTF8
$TopEvents |
Export-Csv -Path "$ReportFolder\Event_Top10_$Date.csv"
-NoTypeInformation `
-Encoding UTF8
Write-Host ""
Write-Host "===== Event Log Summary ====="
Write-Host ""
$Summary
Write-Host ""
Write-Host "===== Top Events ====="
Write-Host ""
$TopEvents
執行後會得到三份報表
C:\Temp
│
├── Event_Summary_20260917.csv
│
├── Event_Detail_20260917.csv
│
└── Event_Top10_20260917.csv
三份報表分別負責不同用途。
Summary
這台 Server 今天有沒有值得注意的 Event?
例如:
Critical : 0
Error : 3
Warning : 28
Status : Warning
Detail
真正排查時看:
發生時間
Event ID
Provider
Level
Message
Top 10
回答:
今天哪一種 Event 出現最多次?
有 Error 時,不要急著直接修
假設看到:
Event ID 7031
Service Control Manager
Error
第一步最好不是:
看到 Service Error → 直接 Restart Service。
比較合理的排查方式是:
Event 發生什麼?
↓
哪個 Provider?
↓
Event ID?
↓
Message?
↓
發生時間?
↓
同時間還有其他 Event 嗎?
↓
Service 現在狀態?
↓
使用者是否有影響?
Event Log 是:
證據。
不是單看到紅色 Error 就代表我們已經知道 Root Cause。
Event 發生時間非常重要
例如使用者說:
「昨天晚上 10 點左右系統斷掉 5 分鐘。」
這時候比起直接:
找今天所有 Error
更有效的方法是:
21:50
~
22:10
只看那段時間。
PowerShell 可以:
$StartTime = Get-Date "2026-09-16 21:50"
$EndTime = Get-Date "2026-09-16 22:10"
然後:
Get-WinEvent `
-FilterHashtable @{
LogName = "System"
StartTime = $StartTime
EndTime = $EndTime
}
這也是 Event Log 排查很重要的思維:
先把時間範圍縮小。
而不是直接在幾十萬筆 Event 裡找問題。
System 和 Application 不要混在一起看
一般來說,我自己會先這樣區分:
System
│
├── Driver
├── Service
├── Disk
├── Network
├── Windows
└── Hardware / OS
Application
│
├── Application Error
├── .NET
├── SQL
├── IIS Application
└── 各種應用程式
當然實際情況不一定完全這樣。
但至少遇到問題時可以先想:
這比較像 Windows / 系統層問題,還是應用程式問題?
這樣會比全部混在一起找快很多。
我們的 Server Health Check 又多一層了
到 Day 9,目前已經有:
Windows Server
│
┌────────────┼────────────┐
│ │ │
CPU Memory Disk
│ │ │
└────────────┼────────────┘
│
Service
│
Uptime
│
Event Log
│
▼
Health Check
前面的:
CPU / Memory / Disk
比較像:
現在的狀態。
而 Event Log 則開始幫我們回答:
最近發生過什麼事情?
這兩種資料合起來之後,巡檢的價值會高很多。
Day 9 小結
今天主要使用:
Get-WinEvent
並學會用:
-FilterHashtable
縮小 Event 範圍:
@{
LogName = "System"
StartTime = $StartTime
Level = 1, 2, 3
}
再搭配:
Group-Object
Sort-Object
Select-Object
ForEach-Object
把大量 Event 變成:
最近 24 小時有多少 Critical?
有多少 Error?
有多少 Warning?
哪個 Event 發生最多?
是哪個 Provider?
發生在什麼時間?
所以今天真正做的不是:
用 PowerShell 取代 Event Viewer。
而是:
先用 PowerShell 把大量 Event Log 縮小成值得工程師看的範圍。
這對維運工作來說,比單純把所有 Error 全部倒出來實際得多。
Day 10 預告
Day 10|從單台走向多台:PowerShell Remoting 批次巡檢 Windows Server
到 Day 9 為止,我們已經可以檢查:
CPU
Memory
Disk
Service
Uptime
Event Log
但現在還有一個很大的問題:
這些 Script 主要都還是在本機執行。
如果公司有:
SERVER01
SERVER02
SERVER03
SERVER04
...
SERVER50
我們不可能:
RDP SERVER01
執行 Script
RDP SERVER02
再執行一次
RDP SERVER03
再執行一次
所以 Day 10 會正式進入:
Invoke-Command
New-PSSession
Test-WSMan
並開始理解:
Management Server
│
├──── SERVER01
├──── SERVER02
├──── SERVER03
└──── SERVER04
讓一台管理端電腦可以批次取得其他 Windows Server 的狀態。
從 Day 10 開始,我們前面寫的 Health Check 才會真正從「單機 Script」開始變成「企業環境可以擴充的巡檢工具」。