PowerShell 技能连载 - 日志记录模式

适用于 PowerShell 5.1 及以上版本

生产级脚本必须有可靠的日志记录——没有日志的脚本就像黑盒,出了问题无从排查。但”加个 Write-Host“和”设计一个日志系统”之间差距巨大。好的日志系统应该支持多级别输出、同时写文件和控制台、自动轮转、结构化格式,而且不影响脚本性能。

本文将讲解 PowerShell 中的日志记录模式,从简单到复杂,适用于不同规模的脚本和自动化场景。

简单日志函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 基础日志函数
function Write-Log {
param(
[Parameter(Mandatory)]
[string]$Message,

[ValidateSet("DEBUG", "INFO", "WARN", "ERROR", "FATAL")]
[string]$Level = "INFO",

[string]$LogPath = "C:\Logs\script.log"
)

$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
$entry = "[$timestamp] [$Level] $Message"

# 控制台输出(带颜色)
$color = switch ($Level) {
"DEBUG" { "DarkGray" }
"INFO" { "White" }
"WARN" { "Yellow" }
"ERROR" { "Red" }
"FATAL" { "Magenta" }
}
Write-Host $entry -ForegroundColor $color

# 文件输出
$logDir = Split-Path $LogPath -Parent
if (-not (Test-Path $logDir)) {
New-Item $logDir -ItemType Directory -Force | Out-Null
}

Add-Content -Path $LogPath -Value $entry -Encoding UTF8
}

# 使用示例
Write-Log -Level INFO "开始部署 MyApp v2.5.0"
Write-Log -Level DEBUG "读取配置文件:C:\MyApp\config.json"
Write-Log -Level WARN "磁盘空间低于 20%"
Write-Log -Level ERROR "数据库连接超时(30s)"
Write-Log -Level FATAL "服务启动失败,中止部署"

执行结果示例:

1
2
3
4
5
[2025-07-15 08:30:15.123] [INFO] 开始部署 MyApp v2.5.0
[2025-07-15 08:30:15.125] [DEBUG] 读取配置文件:C:\MyApp\config.json
[2025-07-15 08:30:15.130] [WARN] 磁盘空间低于 20%
[2025-07-15 08:30:15.135] [ERROR] 数据库连接超时(30s)
[2025-07-15 08:30:15.140] [FATAL] 服务启动失败,中止部署

可配置日志系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
function New-Logger {
<#
.SYNOPSIS
创建可配置的日志记录器
#>
param(
[string]$Name = "Script",

[string]$LogDirectory = "C:\Logs",

[ValidateSet("DEBUG", "INFO", "WARN", "ERROR")]
[string]$MinLevel = "INFO",

[switch]$LogToConsole,

[switch]$LogToFile,

[int]$MaxFileSizeMB = 10,

[int]$MaxFiles = 5
)

$logPath = Join-Path $LogDirectory "$Name-$(Get-Date -Format 'yyyyMMdd').log"

return [PSCustomObject]@{
Name = $Name
LogPath = $logPath
MinLevel = $MinLevel
LogToConsole = $LogToConsole
LogToFile = $LogToFile
MaxFileSizeMB = $MaxFileSizeMB
MaxFiles = $MaxFiles
} | Add-Member -MemberType ScriptMethod -Name Write -Value {
param($Message, $Level = "INFO")

$levelOrder = @{ DEBUG = 0; INFO = 1; WARN = 2; ERROR = 3 }
if ($levelOrder[$Level] -lt $levelOrder[$this.MinLevel]) { return }

$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss.fff"
$entry = "[$timestamp] [$Level] [$($this.Name)] $Message"

if ($this.LogToConsole) {
$color = switch ($Level) {
"DEBUG" { "DarkGray" }
"INFO" { "Cyan" }
"WARN" { "Yellow" }
"ERROR" { "Red" }
}
Write-Host $entry -ForegroundColor $color
}

if ($this.LogToFile) {
# 日志轮转检查
if (Test-Path $this.LogPath) {
$size = (Get-Item $this.LogPath).Length / 1MB
if ($size -ge $this.MaxFileSizeMB) {
$baseName = $this.LogPath -replace '\.log$', ''
for ($i = $this.MaxFiles - 1; $i -ge 1; $i--) {
$old = "$baseName.$i.log"
if (Test-Path $old) {
if ($i -eq $this.MaxFiles - 1) {
Remove-Item $old -Force
} else {
Move-Item $old "$baseName.$($i+1).log" -Force
}
}
}
Move-Item $this.LogPath "$baseName.1.log" -Force
}
}

$logDir = Split-Path $this.LogPath -Parent
if (-not (Test-Path $logDir)) {
New-Item $logDir -ItemType Directory -Force | Out-Null
}
Add-Content -Path $this.LogPath -Value $entry -Encoding UTF8
}
} -Force -PassThru
}

# 创建日志记录器
$log = New-Logger -Name "Deploy" -LogDirectory "C:\Logs\Deploy" `
-LogToConsole -LogToFile -MinLevel DEBUG

$log.Write("部署开始", "INFO")
$log.Write("备份旧版本", "DEBUG")
$log.Write("停止服务", "INFO")
$log.Write("复制文件", "INFO")
$log.Write("磁盘空间不足", "WARN")
$log.Write("服务启动失败", "ERROR")
$log.Write("部署完成", "INFO")

执行结果示例:

1
2
3
4
5
6
7
[2025-07-15 08:30:15.123] [INFO] [Deploy] 部署开始
[2025-07-15 08:30:15.125] [DEBUG] [Deploy] 备份旧版本
[2025-07-15 08:30:15.130] [INFO] [Deploy] 停止服务
[2025-07-15 08:30:15.135] [INFO] [Deploy] 复制文件
[2025-07-15 08:30:15.140] [WARN] [Deploy] 磁盘空间不足
[2025-07-15 08:30:15.145] [ERROR] [Deploy] 服务启动失败
[2025-07-15 08:30:15.150] [INFO] [Deploy] 部署完成

结构化日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# JSON 结构化日志(适合 ELK/Splunk 等日志平台)
function Write-StructuredLog {
param(
[string]$Message,
[string]$Level = "INFO",
[hashtable]$Properties = @{},
[string]$LogPath = "C:\Logs\structured.json"
)

$entry = [ordered]@{
timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fffZ"
level = $Level
message = $Message
script = $MyInvocation.ScriptName
line = $MyInvocation.ScriptLineNumber
computer = $env:COMPUTERNAME
user = $env:USERNAME
}

# 合并额外属性
foreach ($key in $Properties.Keys) {
$entry[$key] = $Properties[$key]
}

$jsonLine = $entry | ConvertTo-Json -Compress
Add-Content -Path $LogPath -Value $jsonLine -Encoding UTF8
}

# 使用结构化日志
Write-StructuredLog -Message "服务健康检查" -Level "INFO" -Properties @{
service = "MyApp"
action = "health_check"
duration_ms = 45
status = "healthy"
}

Write-StructuredLog -Message "部署完成" -Level "INFO" -Properties @{
app_name = "MyApp"
version = "2.5.0"
environment = "production"
duration_s = 120
steps = @("backup", "deploy", "verify")
server = "SRV01"
}

Write-StructuredLog -Message "连接超时" -Level "ERROR" -Properties @{
server = "db-prod-01"
port = 5432
timeout_s = 30
retry_count = 3
error_code = "CONN_TIMEOUT"
}

# 读取结构化日志并分析
$logs = Get-Content "C:\Logs\structured.json" | ConvertFrom-Json
$errors = $logs | Where-Object { $_.level -eq "ERROR" }
Write-Host "`n最近错误:$($errors.Count) 条" -ForegroundColor Red
$errors | Select-Object timestamp, message, server | Format-Table -AutoSize

执行结果示例:

1
2
3
4
最近错误:1 条
timestamp message server
--------- ------- ------
2025-07-15T08:30:20.123Z 连接超时 db-prod-01

日志分析工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 快速分析日志文件
function Get-LogSummary {
param(
[Parameter(Mandatory)]
[string]$LogPath,

[int]$LastN = 100
)

$lines = Get-Content $LogPath -Tail $LastN

$stats = @{
Total = 0
Info = 0
Warning = 0
Error = 0
Debug = 0
}

$errorMessages = @()

foreach ($line in $lines) {
$stats.Total++

if ($line -match '\[INFO\]') { $stats.Info++ }
if ($line -match '\[WARN') { $stats.Warning++ }
if ($line -match '\[ERROR\]') {
$stats.Error++
$errorMessages += $line
}
if ($line -match '\[DEBUG\]') { $stats.Debug++ }
}

Write-Host "日志摘要(最近 $LastN 行):" -ForegroundColor Cyan
Write-Host " 总计:$($stats.Total)"
Write-Host " INFO:$($stats.Info)" -ForegroundColor White
Write-Host " WARN:$($stats.Warning)" -ForegroundColor Yellow
Write-Host " ERROR:$($stats.Error)" -ForegroundColor Red
Write-Host " DEBUG:$($stats.Debug)" -ForegroundColor DarkGray

if ($errorMessages.Count -gt 0) {
Write-Host "`n最近错误:" -ForegroundColor Red
$errorMessages | Select-Object -Last 5 | ForEach-Object {
Write-Host " $_" -ForegroundColor Red
}
}
}

Get-LogSummary -LogPath "C:\Logs\Deploy\Deploy-20250715.log" -LastN 500

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
日志摘要(最近 500 行):
总计:500
INFO:320
WARN:45
ERROR:12
DEBUG:123

最近错误:
[2025-07-15 08:25:10] [ERROR] [Deploy] 数据库连接超时
[2025-07-15 08:30:45] [ERROR] [Deploy] 文件复制失败:拒绝访问
[2025-07-15 08:35:22] [ERROR] [Deploy] 服务启动失败

注意事项

  1. 性能影响:每次 Add-Content 都会打开-写入-关闭文件,高频日志使用 StreamWriter 提升性能
  2. 并发写入:多进程同时写同一日志文件会导致内容交错,使用文件锁或独立日志文件
  3. 日志轮转:不限制日志文件大小会耗尽磁盘空间,务必实现轮转或使用日志框架
  4. 敏感数据:日志中不要记录密码、Token、完整连接字符串等敏感信息
  5. 日志级别:DEBUG 详细记录用于开发排查,生产环境设置为 INFO 或 WARN
  6. UTF-8 BOMAdd-Content -Encoding UTF8 在 PowerShell 5.1 中会添加 BOM,使用 [System.IO.StreamWriter] 可避免

PowerShell 技能连载 - 事件日志与系统监控

适用于 PowerShell 5.1 及以上版本(Windows)

Windows 事件日志是故障排查的”黑匣子”——系统崩溃、应用异常、安全审计、服务启停,几乎所有重要事件都会被记录到事件日志中。对于运维人员来说,能够高效地查询、筛选和分析事件日志是一项必备技能。PowerShell 提供了 Get-WinEvent 命令,其过滤能力远超传统的事件查看器 GUI。

本文将讲解事件日志的查询技巧、自动化监控脚本、性能计数器采集,以及如何构建系统健康检查工具。

事件日志基础查询

Get-WinEvent 是查询事件日志的主力命令,替代了旧版 Get-EventLog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 列出所有可用的日志
Get-WinEvent -ListLog * | Where-Object RecordCount -gt 0 |
Select-Object LogName, RecordCount, FileSize,
@{N='大小MB'; E={[math]::Round($_.FileSize/1MB, 2)}} |
Sort-Object RecordCount -Descending |
Select-Object -First 15 |
Format-Table -AutoSize

# 查询最近的系统日志
Get-WinEvent -LogName System -MaxEvents 10 |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-List

# 查询特定事件 ID
Get-WinEvent -LogName System | Where-Object Id -eq 7036 |
Select-Object -First 5 TimeCreated, Message

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LogName                   RecordCount   大小MB
------- ----------- ------
Security 245812 128.45
Application 87432 42.18
System 45678 18.92
Microsoft-Windows-PowerShell 12345 5.67

TimeCreated Id LevelDisplayName Message
----------- -- ---------------- -------
2025-05-13 08:30:15 7036 信息 服务控制管理器...
2025-05-13 08:30:12 7040 信息 服务控制管理器...
2025-05-13 08:28:45 100 信息 Windows Update...

TimeCreated Message
----------- -------
2025-05-13 08:15:00 服务 Windows Update 已进入运行状态
2025-05-13 08:00:00 服务 WinRM 已进入运行状态

使用 FilterHashtable 高效过滤

Get-WinEventFilterHashtable 参数可以将过滤条件推送到事件日志引擎,比管道 Where-Object 快数倍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 按时间范围过滤(最近 1 小时的错误和警告)
$oneHourAgo = (Get-Date).AddHours(-1)
Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 1, 2, 3
StartTime = $oneHourAgo
} | Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-Table -AutoSize -Wrap

# 按事件 ID 列表过滤
Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = 41, 1074, 6008
} -MaxEvents 20 | Select-Object TimeCreated, Id, Message

# 按提供者(来源)过滤
Get-WinEvent -FilterHashtable @{
LogName = 'Application'
ProviderName = 'Application Error'
StartTime = (Get-Date).AddDays(-7)
} | Select-Object TimeCreated, Id,
@{N='应用程序'; E={$_.Properties[0].Value}},
@{N='版本'; E={$_.Properties[1].Value}} |
Format-Table -AutoSize

# 按关键词过滤(安全审计)
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624, 4625
StartTime = (Get-Date).AddHours(-24)
} | Select-Object TimeCreated, Id,
@{N='用户'; E={$_.Properties[5].Value}},
@{N='来源IP'; E={$_.Properties[18].Value}} |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TimeCreated          Id  LevelDisplayName Message
----------- -- ---------------- -------
2025-05-13 08:25:31 1014 警告 DNS 解析超时
2025-05-13 08:20:15 1001 错误 Windows 错误报告
2025-05-13 08:15:42 702 警告 服务启动失败

TimeCreated Id 应用程序 版本
----------- -- -------- ----
2025-05-13 02:15:00 1000 chrome.exe 124.0.6367
2025-05-12 18:30:22 1000 Outlook.exe 16.0.17928

TimeCreated Id 用户 来源IP
----------- -- ---- ------
2025-05-13 08:30:01 4624 CONTOSO\admin 192.168.1.100
2025-05-13 07:15:22 4625 unknown_user 10.0.0.55

注意Level 值的含义:1 = Critical,2 = Error,3 = Warning,4 = Information,5 = Verbose。

构建自动化监控脚本

以下是一个实用的系统健康监控脚本,可以定时运行并生成报告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
function Get-SystemHealthReport {
<#
.SYNOPSIS
生成系统健康状态报告
#>
[CmdletBinding()]
param(
[int]$CriticalEventHours = 1,
[int]$DiskWarningThreshold = 80,
[int]$CPUWarningThreshold = 85
)

$report = [ordered]@{}
$report['生成时间'] = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$report['计算机名'] = $env:COMPUTERNAME

# 1. 检查关键事件
$startTime = (Get-Date).AddHours(-$CriticalEventHours)
$criticalEvents = Get-WinEvent -FilterHashtable @{
LogName = 'System', 'Application'
Level = 1, 2
StartTime = $startTime
} -ErrorAction SilentlyContinue

$report['关键事件数'] = $criticalEvents.Count
$report['关键事件'] = $criticalEvents |
Select-Object TimeCreated, LogName, Id, LevelDisplayName,
@{N='消息摘要'; E={$_.Message.Substring(0, [Math]::Min(80, $_.Message.Length))}} |
Format-Table -AutoSize

# 2. 检查磁盘空间
$diskStatus = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object {
$usedPct = [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1)
[PSCustomObject]@{
驱动器 = $_.DeviceID
已用% = $usedPct
可用GB = [math]::Round($_.FreeSpace / 1GB, 2)
状态 = if ($usedPct -gt $DiskWarningThreshold) { 'WARNING' } else { 'OK' }
}
}
$report['磁盘状态'] = $diskStatus | Format-Table -AutoSize

# 3. 检查自动重启的服务
$autoRestartServices = Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = 7036, 7045
StartTime = (Get-Date).AddHours(-24)
} -ErrorAction SilentlyContinue |
Select-Object TimeCreated,
@{N='服务'; E={if ($_.Message -match '(.+?) 服务') {$Matches[1]} else {$_.Message}}} |
Group-Object 服务 |
Where-Object Count -gt 3 |
Select-Object Name, Count

$report['频繁重启服务'] = if ($autoRestartServices) {
$autoRestartServices | Format-Table -AutoSize
} else {
"无异常"
}

# 4. 检查 Windows 更新状态
$lastUpdate = Get-CimInstance Win32_QuickFixEngineering |
Sort-Object InstalledOn -Descending | Select-Object -First 1
$report['最新补丁'] = "$($lastUpdate.HotFixID) ($($lastUpdate.InstalledOn.ToString('yyyy-MM-dd')))"

# 5. 系统运行时间
$os = Get-CimInstance Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
$report['运行时间'] = "$($uptime.Days) 天 $($uptime.Hours) 小时"

# 输出报告
foreach ($key in $report.Keys) {
if ($report[$key] -is [string]) {
Write-Host "${key}: $($report[$key])"
} else {
Write-Host "`n${key}:" -ForegroundColor Cyan
$report[$key]
}
}
}

Get-SystemHealthReport

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
生成时间: 2025-05-13 08:30:00
计算机名: DESKTOP-WORK01

关键事件数: 3

TimeCreated LogName Id LevelDisplayName 消息摘要
----------- ------- -- ---------------- --------
2025-05-13 08:25:31 System 1014 警告 DNS 解析超时: api.example.com
2025-05-13 08:20:15 Application 1001 错误 应用程序错误: chrome.exe

磁盘状态:
驱动器 已用% 可用GB 状态
------ ----- ------ ----
C: 59.8 191.58 OK
D: 68.9 289.33 OK

频繁重启服务: 无异常
最新补丁: KB5036908 (2025-05-10)
运行时间: 9 天 4 小时

性能计数器采集

除了事件日志,Windows 性能计数器是监控系统资源使用情况的另一重要数据源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 查看可用的性能计数器类别
Get-Counter -ListSet * | Select-Object CounterSetName, Description |
Where-Object CounterSetName -match 'Processor|Memory|Disk|Network' |
Format-Table -AutoSize

# 采集关键性能指标
$counters = @(
'\Processor(_Total)\% Processor Time'
'\Memory\Available MBytes'
'\Memory\% Committed Bytes In Use'
'\PhysicalDisk(_Total)\Disk Read Bytes/sec'
'\PhysicalDisk(_Total)\Disk Write Bytes/sec'
'\Network Interface(*)\Bytes Total/sec'
)

# 采集一次
$sample = Get-Counter -Counter $counters
$sample.CounterSamples | ForEach-Object {
[PSCustomObject]@{
计数器 = $_.Path -replace '^\\\\[^\\]+\\', ''
值 = [math]::Round($_.CookedValue, 2)
}
} | Format-Table -AutoSize

# 持续采集(每 5 秒采样,共 12 次 = 1 分钟)
$results = Get-Counter -Counter '\Processor(_Total)\% Processor Time' `
-SampleInterval 5 -MaxSamples 12

$results.CounterSamples | ForEach-Object {
[PSCustomObject]@{
时间 = $_.Timestamp.ToString('HH:mm:ss')
CPU使用率 = [math]::Round($_.CookedValue, 1)
}
} | Format-Table -AutoSize

# 计算平均值
$avgCpu = ($results.CounterSamples.CookedValue | Measure-Object -Average).Average
Write-Host "1 分钟平均 CPU 使用率:$([math]::Round($avgCpu, 1))%" -ForegroundColor Cyan

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CounterSetName                 Description
--------------- -----------
Processor 处理器性能计数器
Memory 内存使用情况计数器
PhysicalDisk 物理磁盘性能计数器
Network Interface 网络接口性能计数器

计数器 值
------ --
Processor(_Total)\% Processor Time 12.45
Memory\Available MBytes 8456.00
Memory\% Committed Bytes In Use 65.32
PhysicalDisk(_Total)\Disk Read Bytes/sec 524288.00
PhysicalDisk(_Total)\Disk Write Bytes/sec 262144.00

时间 CPU使用率
---- --------
08:30:00 15.2
08:30:05 22.8
08:30:10 18.5
...
08:30:55 14.3

1 分钟平均 CPU 使用率:17.6%

实时事件监控

通过 Register-WmiEventRegister-CimIndicationEvent,可以对特定事件进行实时监控并触发操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 监控新事件的创建
$query = "SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance ISA 'Win32_NTLogEvent' AND TargetInstance.LogFile = 'System' AND TargetInstance.EventType <= 2"

Register-WmiEvent -Query $query -SourceIdentifier "SystemErrorMonitor" -Action {
$event = $Event.SourceEventArgs.NewEvent.TargetInstance
$body = @"
时间: $($event.TimeGenerated)
事件ID: $($event.EventCode)
类别: $($event.CategoryString)
消息: $($event.Message.Substring(0, [Math]::Min(200, $event.Message.Length)))
"@

# 记录到日志文件
Add-Content -Path "C:\Logs\critical-events.log" -Value $body

# 可以在此处添加邮件通知或 Webhook
Write-Host "检测到关键事件:ID $($event.EventCode)" -ForegroundColor Red
}

# 查看已注册的事件订阅
Get-EventSubscriber

# 手动触发检查
Get-Event -SourceIdentifier "SystemErrorMonitor"

# 取消订阅
Unregister-Event -SourceIdentifier "SystemErrorMonitor"

执行结果示例:

1
2
3
4
5
SubscriptionId   Name                 Enabled
-------------- ---- -------
1 SystemErrorMonitor True

检测到关键事件:ID 1001

注意事项

  1. FilterHashtable 优先:始终使用 -FilterHashtable 而非管道 Where-Object 过滤,前者在日志引擎层面过滤,性能提升可达 10-100 倍
  2. 日志大小管理:定期检查和归档事件日志,避免日志文件过大导致查询变慢。可通过组策略配置日志大小上限和覆盖策略
  3. 安全日志权限:查询安全日志(Security)需要管理员权限
  4. 性能计数器开销:频繁采集性能计数器会消耗 CPU 资源,建议采样间隔不低于 5 秒
  5. 远程查询Get-WinEvent 支持 -ComputerName 参数远程查询,但需要 WinRM 或 RPC 连接
  6. 事件消息截断:某些事件消息很长,使用 $_.Message.Substring(0, N) 截取前 N 个字符用于显示

PowerShell 技能连载 - 文件系统变更监控

适用于 PowerShell 7.0 及以上版本(Windows)

背景

在日常工作和技术运维中,我们经常需要知道某个目录下发生了什么变化:配置文件是否被篡改、日志目录是否有新文件生成、用户文档是否被意外删除。传统的做法是写一个轮询脚本,每隔几秒扫描一次目录,但这种方式既低效又容易遗漏变化。

.NET 提供的 System.IO.FileSystemWatcher 类可以监听文件系统的变更事件,当目标目录中出现文件创建、修改、删除或重命名时,它会立即触发通知。结合 PowerShell 的事件注册机制,我们可以构建出高效、实时的文件系统监控方案——从安全审计到自动构建,再到实时备份,都可以基于这套机制实现。

FileSystemWatcher 基础

FileSystemWatcher 是 .NET 内置的文件系统监控组件。使用时需要指定要监控的目录路径,并配置需要监听的事件类型。下面是最基本的创建和启用方式:

1
2
3
4
5
6
7
8
9
10
11
# 创建 FileSystemWatcher 实例
$watcher = [System.IO.FileSystemWatcher]::new()

# 设置监控路径
$watcher.Path = "C:\Logs"

# 启用变更事件触发
$watcher.EnableRaisingEvents = $true

# 查看当前配置
$watcher | Format-List Path, Filter, EnableRaisingEvents
1
2
3
Path                 : C:\Logs
Filter : *.*
EnableRaisingEvents : True

创建 FileSystemWatcher 后,Path 属性指定了要监控的目录,EnableRaisingEvents 设为 $true 后才会真正开始触发事件。默认情况下 Filter*.*,表示监控所有文件。注意,此时虽然已经启用了事件触发,但我们还没有注册任何事件处理器,所以即使目录发生变化也不会有任何响应。

监控文件创建、修改、删除与重命名

FileSystemWatcher 提供了四类核心事件:CreatedChangedDeletedRenamed。我们可以使用 PowerShell 的 Register-ObjectEvent 来订阅这些事件,并通过 -Action 参数指定事件触发时执行的脚本块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$watcher = [System.IO.FileSystemWatcher]::new("C:\Logs")
$watcher.EnableRaisingEvents = $true

# 注册创建事件
Register-ObjectEvent -InputObject $watcher -EventName Created -SourceIdentifier "FileCreated" -Action {
$name = $Event.SourceEventArgs.Name
$fullPath = $Event.SourceEventArgs.FullPath
$time = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] 文件创建: $name ($fullPath)"
}

# 注册修改事件
Register-ObjectEvent -InputObject $watcher -EventName Changed -SourceIdentifier "FileChanged" -Action {
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$time = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] 文件修改: $name ($changeType)"
}

# 注册删除事件
Register-ObjectEvent -InputObject $watcher -EventName Deleted -SourceIdentifier "FileDeleted" -Action {
$name = $Event.SourceEventArgs.Name
$time = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] 文件删除: $name"
}

# 注册重命名事件
Register-ObjectEvent -InputObject $watcher -EventName Renamed -SourceIdentifier "FileRenamed" -Action {
$oldName = $Event.SourceEventArgs.OldName
$newName = $Event.SourceEventArgs.Name
$time = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] 文件重命名: $oldName -> $newName"
}

Write-Host "已注册全部事件,正在监控 C:\Logs ..."

注册完成后,每当 C:\Logs 目录下发生对应的文件操作,PowerShell 就会在后台执行相应的 Action 脚本块。此时如果我们在另一个终端中操作文件,监控端就会输出对应的日志信息。

1
2
3
4
[2025-04-24 10:15:32] 文件创建: newfile.txt (C:\Logs\newfile.txt)
[2025-04-24 10:15:45] 文件修改: newfile.txt (Changed)
[2025-04-24 10:16:01] 文件重命名: newfile.txt -> report.txt
[2025-04-24 10:16:20] 文件删除: report.txt

需要注意,Changed 事件可能被多次触发——例如用记事本保存文件时,文件的内容和属性可能分别触发一次 Changed 事件。这在后续处理中需要做去重处理。

过滤器与监控范围设置

在实际场景中,我们往往不需要监控目录下的所有文件。FileSystemWatcher 提供了 FilterIncludeSubdirectories 两个属性来精确控制监控范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$watcher = [System.IO.FileSystemWatcher]::new("C:\Project\src")

# 只监控 .log 文件
$watcher.Filter = "*.log"

# 同时监控子目录
$watcher.IncludeSubdirectories = $true

# 设置缓冲区大小(默认 8KB,频繁变更时需要加大)
$watcher.InternalBufferSize = 65536 # 64KB

$watcher.EnableRaisingEvents = $true

# 注册事件,只关注 Created 和 Changed
Register-ObjectEvent -InputObject $watcher -EventName Created -SourceIdentifier "LogCreated" -Action {
$path = $Event.SourceEventArgs.FullPath
$time = $Event.TimeGenerated.ToString("HH:mm:ss.fff")
Write-Host "[$time] 新日志: $path"
}

Register-ObjectEvent -InputObject $watcher -EventName Changed -SourceIdentifier "LogChanged" -Action {
$path = $Event.SourceEventArgs.FullPath
$time = $Event.TimeGenerated.ToString("HH:mm:ss.fff")
Write-Host "[$time] 日志更新: $path"
}

Write-Host "正在监控 C:\Project\src 及子目录中的 *.log 文件..."
1
2
3
[10:20:15.123] 新日志: C:\Project\src\app\debug.log
[10:20:15.456] 日志更新: C:\Project\src\app\debug.log
[10:21:30.789] 新日志: C:\Project\src\api\access.log

Filter 属性只支持简单的通配符模式(如 *.log*.txt),不支持正则表达式或多个扩展名的组合过滤。如果需要同时监控 .log.txt,可以创建多个 FileSystemWatcher 实例,或者在 Action 中用条件判断自行过滤。InternalBufferSize 的设置很重要——当短时间内文件变更过于频繁时,默认的 8KB 缓冲区可能不够用,导致事件丢失。微软建议将其设置为 4KB 的整数倍,最大可到 64KB。

事件注册处理与生命周期管理

PowerShell 的事件订阅不会自动清理,如果不主动注销,它们会一直占用资源。良好的做法是在脚本中记录所有订阅标识,并在不再需要时统一注销。此外,事件处理器中产生的输出不会直接显示在控制台,而是存储在后台作业中,需要用 Receive-Job 来查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 定义所有订阅标识
$subscriptionIds = @(
"FSW_Created",
"FSW_Changed",
"FSW_Deleted",
"FSW_Renamed"
)

$watcher = [System.IO.FileSystemWatcher]::new("C:\Config")
$watcher.EnableRaisingEvents = $true

# 注册事件并收集作业信息
$jobs = @()
$events = @("Created", "Changed", "Deleted", "Renamed")
$ids = $subscriptionIds

for ($i = 0; $i -lt $events.Count; $i++) {
$job = Register-ObjectEvent -InputObject $watcher `
-EventName $events[$i] `
-SourceIdentifier $ids[$i] `
-Action {
$info = @{
Time = $Event.TimeGenerated
Type = $Event.SourceEventArgs.ChangeType
Path = $Event.SourceEventArgs.FullPath
Name = $Event.SourceEventArgs.Name
}
# 将事件信息写入流,方便后续检索
[PSCustomObject]$info
}
$jobs += $job
}

Write-Host "已注册 $($events.Count) 个事件订阅"

# 查看当前所有事件订阅
Get-EventSubscriber | Format-Table SourceIdentifier, SubscriptionId, AutoUnregister

# 查看后台作业中收集到的事件
Start-Sleep -Seconds 5 # 等待一些事件发生
foreach ($job in $jobs) {
$results = Receive-Job -Job $job -ErrorAction SilentlyContinue
if ($results) {
$results | Format-Table Time, Type, Name -AutoSize
}
}
1
2
3
4
5
6
7
8
9
10
11
12
SourceIdentifier SubscriptionId AutoUnregister
----------------- -------------- --------------
FSW_Created 1 False
FSW_Changed 2 False
FSW_Deleted 3 False
FSW_Renamed 4 False

Time Type Name
---- ---- ----
2025/4/24 10:30:15 Created appsettings.json
2025/4/24 10:30:22 Changed appsettings.json
2025/4/24 10:31:05 Renamed old_config.json

使用完毕后,务必清理订阅和作业,否则会造成内存泄漏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 注销所有事件订阅
foreach ($id in $subscriptionIds) {
Unregister-Event -SourceIdentifier $id -ErrorAction SilentlyContinue
}

# 移除后台作业
foreach ($job in $jobs) {
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
}

# 释放 FileSystemWatcher
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()

Write-Host "已清理所有事件订阅和资源"
1
已清理所有事件订阅和资源

实战:自动备份监控目录

将以上知识点整合起来,我们可以构建一个实用的自动备份脚本。当监控目录中的文件发生变更时,自动将最新版本复制到备份目录中,实现近实时的增量备份。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 自动备份脚本配置
$SourcePath = "C:\ImportantDocs"
$BackupPath = "D:\Backup\ImportantDocs"

# 确保备份目录存在
if (-not (Test-Path $BackupPath)) {
New-Item -Path $BackupPath -ItemType Directory -Force | Out-Null
Write-Host "已创建备份目录: $BackupPath"
}

# 创建 FileSystemWatcher
$watcher = [System.IO.FileSystemWatcher]::new($SourcePath)
$watcher.IncludeSubdirectories = $true
$watcher.InternalBufferSize = 65536
$watcher.EnableRaisingEvents = $true

# 用于去重的哈希表(避免短时间内重复备份同一文件)
$recentBackup = @{}

# 注册创建和修改事件
Register-ObjectEvent -InputObject $watcher -EventName Created -SourceIdentifier "Backup_Created" -Action {
$sourceFile = $Event.SourceEventArgs.FullPath
$now = Get-Date

# 去重:5秒内同一文件不重复备份
if ($recentBackup.ContainsKey($sourceFile) -and
($now - $recentBackup[$sourceFile]).TotalSeconds -lt 5) {
return
}
$recentBackup[$sourceFile] = $now

# 计算备份目标路径
$relativePath = $sourceFile.Substring($SourcePath.Length).TrimStart("\")
$targetFile = Join-Path $BackupPath $relativePath

# 确保目标目录存在
$targetDir = Split-Path $targetFile -Parent
if (-not (Test-Path $targetDir)) {
New-Item -Path $targetDir -ItemType Directory -Force | Out-Null
}

# 执行备份
try {
Copy-Item -Path $sourceFile -Destination $targetFile -Force
$timeStr = $now.ToString("HH:mm:ss")
Write-Host "[$timeStr] 备份成功: $relativePath"
}
catch {
Write-Host "[ERROR] 备份失败: $relativePath - $($_.Exception.Message)"
}
}

Register-ObjectEvent -InputObject $watcher -EventName Changed -SourceIdentifier "Backup_Changed" -Action {
$sourceFile = $Event.SourceEventArgs.FullPath
$now = Get-Date

# 去重:5秒内同一文件不重复备份
if ($recentBackup.ContainsKey($sourceFile) -and
($now - $recentBackup[$sourceFile]).TotalSeconds -lt 5) {
return
}
$recentBackup[$sourceFile] = $now

$relativePath = $sourceFile.Substring($SourcePath.Length).TrimStart("\")
$targetFile = Join-Path $BackupPath $relativePath

$targetDir = Split-Path $targetFile -Parent
if (-not (Test-Path $targetDir)) {
New-Item -Path $targetDir -ItemType Directory -Force | Out-Null
}

try {
# 修改时等待文件写入完成
Start-Sleep -Milliseconds 500
Copy-Item -Path $sourceFile -Destination $targetFile -Force
$timeStr = $now.ToString("HH:mm:ss")
Write-Host "[$timeStr] 增量备份: $relativePath"
}
catch {
Write-Host "[ERROR] 备份失败: $relativePath - $($_.Exception.Message)"
}
}

Write-Host "自动备份已启动: $SourcePath -> $BackupPath"
Write-Host "按 Ctrl+C 停止监控..."

# 保持脚本运行
try {
while ($true) {
Start-Sleep -Seconds 1
}
}
finally {
# 清理
Unregister-Event -SourceIdentifier "Backup_Created" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Backup_Changed" -ErrorAction SilentlyContinue
Get-Job | Where-Object { $_.Name -match "^Backup_" } | Remove-Job -Force
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Write-Host "监控已停止,资源已释放"
}
1
2
3
4
5
6
7
已创建备份目录: D:\Backup\ImportantDocs
自动备份已启动: C:\ImportantDocs -> D:\Backup\ImportantDocs
按 Ctrl+C 停止监控...
[10:45:12] 备份成功: report.docx
[10:45:38] 增量备份: report.docx
[10:46:05] 备份成功: subfolder\notes.txt
监控已停止,资源已释放

这个脚本包含了几个实用的设计要点:用哈希表进行去重以避免 Changed 事件的重复触发;在备份修改文件时加入了短暂延迟,确保文件写入完成;在 finally 块中确保资源始终被正确释放。

查看与管理事件订阅

在调试和运维过程中,我们需要随时了解当前有哪些活跃的事件订阅。PowerShell 提供了几个相关命令来管理这些订阅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 列出当前所有事件订阅
Get-EventSubscriber | Format-Table SourceIdentifier, SubscriptionId, EventName -AutoSize

# 查看后台作业状态
Get-Job | Where-Object { $_.JobStateInfo.State -eq "Running" } |
Format-Table Id, Name, State -AutoSize

# 查看某个作业的输出
$job = Get-Job | Where-Object { $_.Name -eq "Backup_Created" } | Select-Object -First 1
if ($job) {
Receive-Job -Job $job -Keep
}

# 紧急清理:注销所有事件订阅
Get-EventSubscriber | Unregister-Event
Get-Job | Remove-Job -Force
Write-Host "已清理全部订阅和作业"
1
2
3
4
5
6
7
8
9
10
11
SourceIdentifier  SubscriptionId EventName
----------------- -------------- ---------
Backup_Created 1 Created
Backup_Changed 2 Changed

Id Name State
--- ---- -----
3 Backup_Created Running
4 Backup_Changed Running

已清理全部订阅和作业

在调试阶段,善用 Get-EventSubscriberGet-Job 可以快速定位问题。如果发现某个订阅不再触发事件,首先检查对应的 Job 是否还在运行状态。

注意事项

  1. 缓冲区溢出导致事件丢失:当短时间内发生大量文件变更时(如批量编译、解压缩),FileSystemWatcher 的内部缓冲区可能溢出。解决方法是增大 InternalBufferSize(最大 64KB),并在 Action 中做轻量处理以尽快释放缓冲区。

  2. Changed 事件重复触发:许多编辑器保存文件时会同时修改文件内容和属性(大小、写入时间等),导致一次保存触发多次 Changed 事件。建议在 Action 中加入时间窗口去重逻辑(如上面备份脚本中的 5 秒内去重)。

  3. 网络路径的可靠性FileSystemWatcher 监控网络共享路径时,网络中断会导致监控失效且不会自动恢复。生产环境中应在循环中检测连接状态并重建 Watcher。

  4. 文件锁定问题:文件刚创建或正在写入时,立即读取可能遇到文件锁定。在 Action 中加入短暂延迟(如 Start-Sleep -Milliseconds 500)可以缓解这个问题。

  5. 资源释放FileSystemWatcher 实现了 IDisposable 接口,使用完毕后必须调用 Dispose() 释放资源,同时用 Unregister-Event 注销事件订阅、用 Remove-Job 清理后台作业,否则会造成内存泄漏。

  6. 跨平台限制FileSystemWatcher 依赖 Windows 的文件系统通知机制,在 Linux/macOS 上的 .NET 虽然也有实现,但行为和可靠性可能有差异。如果需要跨平台方案,可以考虑轮询结合哈希校验的方式。

PowerShell 技能连载 - 系统性能监控实战

适用于 PowerShell 7.0 及以上版本

在服务器运维和 DevOps 实践中,系统性能监控是保障业务稳定运行的基石。无论是排查突发的性能抖动,还是进行容量规划和趋势分析,都需要一套可靠的监控手段。传统的 GUI 工具(如任务管理器、perfmon)虽然直观,但不适合自动化场景和大规模服务器管理。

PowerShell 提供了对 WMI/CIM 类、.NET 性能计数器和系统 API 的完整访问能力,让我们可以用脚本化的方式采集、分析和导出系统性能数据。本文将介绍如何使用 PowerShell 构建一套实用的系统性能监控方案,涵盖 CPU、内存、磁盘、进程、网络等关键指标。

采集 CPU、内存和磁盘基础指标

系统监控的第一步是获取核心资源的使用情况。通过 CIM 类可以高效地采集 CPU 利用率、内存使用率和磁盘空间信息,这些是判断系统健康状态最基本的指标。

下面的函数将 CPU、内存和磁盘三项指标整合到一个对象中,方便后续统一处理和比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function Get-SystemPerformanceSnapshot {
<#
.SYNOPSIS
获取系统核心性能指标的快照
#>
$cpu = Get-CimInstance -ClassName Win32_Processor |
Measure-Object -Property LoadPercentage -Average |
Select-Object -ExpandProperty Average

$os = Get-CimInstance -ClassName Win32_OperatingSystem
$totalMemoryGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
$freeMemoryGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
$usedMemoryPct = [math]::Round(
($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) /
$os.TotalVisibleMemorySize * 100, 1
)

$disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" |
ForEach-Object {
$freeGB = [math]::Round($_.FreeSpace / 1GB, 2)
$totalGB = [math]::Round($_.Size / 1GB, 2)
$usedPct = [math]::Round(($_.Size - $_.FreeSpace) / $_.Size * 100, 1)
[PSCustomObject]@{
Drive = $_.DeviceID
TotalGB = $totalGB
FreeGB = $freeGB
UsedPct = $usedPct
}
}

[PSCustomObject]@{
Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
CpuUsagePct = $cpu
TotalMemoryGB = $totalMemoryGB
FreeMemoryGB = $freeMemoryGB
MemoryUsedPct = $usedMemoryPct
Disks = $disks
}
}

Get-SystemPerformanceSnapshot

执行结果示例:

1
2
3
4
5
6
7
Timestamp     : 2025-04-22 09:15:30
CpuUsagePct : 23
TotalMemoryGB : 31.89
FreeMemoryGB : 14.52
MemoryUsedPct : 54.5
Disks : {@{Drive=C:; TotalGB=476.68; FreeGB=218.35; UsedPct=54.2},
@{Drive=D:; TotalGB=931.51; FreeGB=612.08; UsedPct=34.3}}

可以看到,一条命令就能拿到系统当前的核心资源状态。当 CPU 或内存使用率超过阈值时,运维人员可以第一时间感知并介入。

进程监控与资源排行

系统性能异常往往由个别进程引起。通过分析进程的资源占用情况,可以快速定位问题根源——是内存泄漏、CPU 密集计算,还是磁盘 I/O 瓶颈。

下面这段脚本按 CPU 和内存占用分别列出 Top N 进程,并标记出超出阈值的高消耗进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function Get-TopProcesses {
<#
.SYNOPSIS
获取资源占用最高的进程列表
.PARAMETER Top
返回的进程数量,默认 10
.PARAMETER CpuThreshold
CPU 使用率告警阈值(百分比),默认 80
.PARAMETER MemThresholdMB
内存占用告警阈值(MB),默认 500
#>
param(
[int]$Top = 10,
[double]$CpuThreshold = 80,
[double]$MemThresholdMB = 500
)

$processes = Get-Process | Where-Object { $_.Id -gt 0 } |
Select-Object Id, ProcessName,
@{N='CPU_Sec'; E={[math]::Round($_.CPU, 2)}},
@{N='Memory_MB'; E={[math]::Round($_.WorkingSet64 / 1MB, 2)}},
@{N='Threads'; E={$_.Threads.Count}},
StartTime

$byCpu = $processes | Sort-Object CPU_Sec -Descending | Select-Object -First $Top
$byMem = $processes | Sort-Object Memory_MB -Descending | Select-Object -First $Top

$alerts = $processes | Where-Object {
$_.CPU_Sec -gt $CpuThreshold -or $_.Memory_MB -gt $MemThresholdMB
}

[PSCustomObject]@{
TopByCpu = $byCpu
TopByMem = $byMem
Alerts = $alerts
}
}

$result = Get-TopProcesses -Top 5
$result.TopByCpu | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
 Id ProcessName CPU_Sec Memory_MB Threads StartTime
-- ----------- ------- --------- ------- ---------
4212 chrome 312.45 892.31 38 4/22/2025 8:30:12
3088 devenv 187.20 645.80 27 4/22/2025 8:15:05
1524 postgres 145.67 412.55 16 4/22/2025 7:00:00
892 dwm 120.33 210.40 12 4/22/2025 6:58:01
1024 powershell 98.12 156.70 8 4/22/2025 9:10:30

在日常运维中,将此脚本放入定时任务,每隔几分钟运行一次,就能持续跟踪进程资源变化趋势。当某个进程突然飙升至告警阈值之上,可以及时触发通知。

网络连接统计与异常检测

网络连接状态是排查服务可用性和安全事件的重要依据。大量 TIME_WAIT 连接可能意味着短连接风暴,异常的外连 IP 可能暗示安全风险,某个端口连接数暴涨可能表示正在遭受攻击或业务流量激增。

以下脚本通过 Get-NetTCPConnection 统计连接状态分布和端口连接数,帮助快速发现网络层面的异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function Get-NetworkConnectionStats {
<#
.SYNOPSIS
统计本机 TCP 连接状态和端口分布
#>
$connections = Get-NetTCPConnection |
Where-Object { $_.State -ne 'Bound' }

# 按连接状态分组统计
$stateStats = $connections |
Group-Object State |
Sort-Object Count -Descending |
ForEach-Object {
[PSCustomObject]@{
State = $_.Name
Count = $_.Count
}
}

# 按本地监听端口分组统计
$portStats = $connections |
Where-Object { $_.LocalPort -gt 0 } |
Group-Object LocalPort |
Sort-Object Count -Descending |
Select-Object -First 10 |
ForEach-Object {
[PSCustomObject]@{
Port = $_.Name
Count = $_.Count
}
}

# 统计远端 IP 连接数(排查异常外连)
$remoteIps = $connections |
Where-Object { $_.RemoteAddress -and $_.RemoteAddress -ne '0.0.0.0' -and $_.RemoteAddress -ne '::' } |
Group-Object RemoteAddress |
Sort-Object Count -Descending |
Select-Object -First 5 |
ForEach-Object {
[PSCustomObject]@{
RemoteIP = $_.Name
Count = $_.Count
}
}

[PSCustomObject]@{
ConnectionStates = $stateStats
TopPorts = $portStats
TopRemoteIps = $remoteIps
TotalConnections = $connections.Count
}
}

$stats = Get-NetworkConnectionStats
$stats.ConnectionStates | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
State       Count
----- -----
Established 142
TimeWait 89
CloseWait 23
Listen 18
SynSent 5

当 TIME_WAIT 或 CLOSE_WAIT 连接数异常增多时,往往意味着应用层的连接管理存在问题。结合远端 IP 统计,还能识别出是否存在异常的频繁外连行为。

数据导出为 CSV 和 JSON

监控数据如果不能持久化存储,就只能在当下查看,无法做历史趋势分析。将采集到的性能数据导出为 CSV 或 JSON 格式,既方便导入 Excel 做图表,也便于与 Grafana、ELK 等监控平台集成。

下面这段代码展示了如何将性能快照追加写入 CSV 文件,以及一次性导出为结构化的 JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function Export-PerformanceData {
<#
.SYNOPSIS
将性能数据导出为 CSV 和 JSON 格式
.PARAMETER OutputDir
输出目录路径
#>
param(
[string]$OutputDir = "$HOME\PerfLogs"
)

if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}

$snapshot = Get-SystemPerformanceSnapshot
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"

# 扁平化磁盘数据,便于 CSV 追加
$flatData = [PSCustomObject]@{
Timestamp = $snapshot.Timestamp
CpuUsagePct = $snapshot.CpuUsagePct
MemoryUsedPct = $snapshot.MemoryUsedPct
FreeMemoryGB = $snapshot.FreeMemoryGB
DiskInfo = ($snapshot.Disks | ForEach-Object {
"$($_.Drive)=$($_.UsedPct)%"
}) -join '; '
}

# 追加写入 CSV(适合长期采集)
$csvPath = Join-Path $OutputDir "perf_$(Get-Date -Format 'yyyyMMdd').csv"
$flatData | Export-Csv -Path $csvPath -NoTypeInformation -Append

# 完整快照导出 JSON(适合单次详细记录)
$jsonPath = Join-Path $OutputDir "perf_$timestamp.json"
$snapshot | ConvertTo-Json -Depth 3 | Set-Content -Path $jsonPath

[PSCustomObject]@{
CsvFile = $csvPath
JsonFile = $jsonPath
Written = $true
}
}

Export-PerformanceData

执行结果示例:

1
2
3
CsvFile  : /home/user/PerfLogs/perf_20250422.csv
JsonFile : /home/user/PerfLogs/perf_20250422_091530.json
Written : True

CSV 追加模式让每次采集的行数据逐行写入同一个文件,配合 Excel 或 Python 脚本即可绘制性能趋势曲线。JSON 格式则保留了完整的嵌套结构,适合程序化消费。

持续监控与阈值告警

单次采集只能看到瞬时状态,真正的运维监控需要持续轮询并在指标异常时主动告警。下面这段脚本实现了一个轻量级的持续监控循环,支持 CPU、内存、磁盘三个维度的阈值检测,并在超限时输出告警信息。

你可以将告警逻辑替换为发送邮件、调用 Webhook 或写入事件日志,实现完整的告警链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function Start-PerformanceWatch {
<#
.SYNOPSIS
持续监控系统性能并在超阈值时告警
.PARAMETER IntervalSeconds
采集间隔(秒),默认 30
.PARAMETER CpuThreshold
CPU 使用率告警阈值,默认 85
.PARAMETER MemThreshold
内存使用率告警阈值,默认 90
.PARAMETER DiskThreshold
磁盘使用率告警阈值,默认 90
.PARAMETER DurationMinutes
监控持续时间(分钟),默认 10
#>
param(
[int]$IntervalSeconds = 30,
[double]$CpuThreshold = 85,
[double]$MemThreshold = 90,
[double]$DiskThreshold = 90,
[int]$DurationMinutes = 10
)

$endTime = (Get-Date).AddMinutes($DurationMinutes)
$alertCount = 0

Write-Host "性能监控已启动,持续 $DurationMinutes 分钟,间隔 ${IntervalSeconds}s" `
-ForegroundColor Cyan
Write-Host ("CPU>{0}% | MEM>{1}% | DISK>{2}% 触发告警`n" -f `
$CpuThreshold, $MemThreshold, $DiskThreshold) -ForegroundColor DarkGray

while ((Get-Date) -lt $endTime) {
$snap = Get-SystemPerformanceSnapshot
$alerts = @()

if ($snap.CpuUsagePct -gt $CpuThreshold) {
$alerts += "CPU 使用率 $($snap.CpuUsagePct)% 超过阈值 ${CpuThreshold}%"
}
if ($snap.MemoryUsedPct -gt $MemThreshold) {
$alerts += "内存使用率 $($snap.MemoryUsedPct)% 超过阈值 ${MemThreshold}%"
}
foreach ($disk in $snap.Disks) {
if ($disk.UsedPct -gt $DiskThreshold) {
$alerts += "磁盘 $($disk.Drive) 使用率 $($disk.UsedPct)% 超过阈值 ${DiskThreshold}%"
}
}

$timeStr = Get-Date -Format "HH:mm:ss"
if ($alerts.Count -gt 0) {
$alertCount += $alerts.Count
Write-Host "[$timeStr] ALERT:" -ForegroundColor Red -NoNewline
Write-Host " $($alerts -join ' | ')" -ForegroundColor Yellow
} else {
Write-Host "[$timeStr] OK - CPU:$($snap.CpuUsagePct)% MEM:$($snap.MemoryUsedPct)%" `
-ForegroundColor Green
}

Start-Sleep -Seconds $IntervalSeconds
}

Write-Host "`n监控结束,共触发 $alertCount 条告警。" -ForegroundColor Cyan
}

Start-PerformanceWatch -IntervalSeconds 15 -DurationMinutes 2

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
性能监控已启动,持续 2 分钟,间隔 15s
CPU>85% | MEM>90% | DISK>90% 触发告警

[09:15:00] OK - CPU:23% MEM:54.5%
[09:15:15] OK - CPU:28% MEM:55.1%
[09:15:30] ALERT: CPU 使用率 92% 超过阈值 85%
[09:15:45] OK - CPU:31% MEM:56.2%
[09:16:00] OK - CPU:25% MEM:54.8%
[09:16:15] OK - CPU:22% MEM:53.9%
[09:16:30] OK - CPU:27% MEM:55.5%
[09:16:45] OK - CPU:24% MEM:54.2%

监控结束,共触发 1 条告警。

在实际生产环境中,可以将此脚本作为 Windows 计划任务或 systemd 服务运行,配合邮件通知模块(Send-MailMessage)或 Webhook(Invoke-RestMethod)将告警推送到运维群。

注意事项

  1. CIM vs WMI:优先使用 Get-CimInstance 而非已弃用的 Get-WmiObject,CIM 支持远程会话复用,性能更好
  2. 采集频率:间隔不宜过短(建议不低于 10 秒),频繁采集本身会消耗 CPU,尤其在旧设备上
  3. 远程监控:结合 PowerShell Remoting,可以在一台管理机上统一采集多台服务器的性能数据,使用 -ComputerName 参数即可
  4. 权限要求:部分 CIM 类和性能计数器需要管理员权限才能访问,脚本应以提升权限运行
  5. 数据保留:CSV 追加模式下注意定期归档和清理旧文件,避免单个文件过大影响读取性能
  6. 跨平台差异:PowerShell 7 在 Linux/macOS 上部分 CIM 类不可用,需使用 /proc 文件系统或 Get-Process 等替代方案

PowerShell 技能连载 - Windows 事件日志分析

适用于 PowerShell 5.1 及以上版本

Windows 事件日志是排查系统问题、监控安全事件的重要数据源。无论是排查服务崩溃、追踪登录失败,还是审计权限变更,事件日志都记录着关键的操作痕迹。传统的”事件查看器”图形界面虽然直观,但面对大规模日志筛选和批量分析时效率极低。

PowerShell 提供了强大的事件日志查询能力,可以像操作数据库一样对日志进行精确筛选和统计。本文将介绍如何使用 Get-WinEvent 高效查询事件日志,以及如何构建自动化的日志分析脚本。

基础查询

Get-WinEvent 是查询事件日志的核心 cmdlet,比旧版的 Get-EventLog 性能更好且功能更强。

1
2
3
4
# 查询最近 10 条系统日志
Get-WinEvent -LogName 'System' -MaxEvents 10 |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-Table -AutoSize
1
2
3
4
5
6
TimeCreated          Id LevelDisplayName Message
----------- -- ---------------- -------
2025/4/8 9:45:12 7036 信息 Windows Update 服务已停止...
2025/4/8 9:44:30 7036 信息 DNS Client 服务已停止...
2025/4/8 9:43:15 1001 信息 Windows 错误报告...
2025/4/8 9:42:00 7024 错误 服务意外终止...

可以查看系统中所有可用的日志列表。

1
2
3
4
5
6
7
# 列出所有已注册的事件日志
Get-WinEvent -ListLog '*' |
Where-Object { $_.RecordCount -gt 0 } |
Select-Object LogName, RecordCount, FileSize |
Sort-Object RecordCount -Descending |
Select-Object -First 10 |
Format-Table -AutoSize

使用 FilterHashtable 高效筛选

直接通过管道筛选 Get-WinEvent 的输出效率很低,因为 PowerShell 会先获取所有事件再过滤。推荐使用 -FilterHashtable 参数在服务端进行筛选,大幅提升查询速度。

1
2
3
4
5
6
7
8
9
10
# 查询过去 24 小时内的系统错误日志
$startTime = (Get-Date).AddDays(-1)

Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2 # 2=错误,3=警告,4=信息
StartTime = $startTime
} |
Select-Object TimeCreated, Id, Message |
Format-Table -AutoSize -Wrap

Level 的值含义如下:

级别
1 关键 (Critical)
2 错误 (Error)
3 警告 (Warning)
4 信息 (Information)
0 详细 (Verbose)

按 EventId 精确筛选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查询所有登录失败事件(EventId 4625)
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
} -MaxEvents 20 |
ForEach-Object {
# 从 XML 中提取详细信息
$xml = [xml]$_.ToXml()
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace('evt', 'http://schemas.microsoft.com/win/2004/08/events/event')

[PSCustomObject]@{
时间 = $_.TimeCreated
用户 = $xml.SelectSingleNode('//evt:Data[@Name="TargetUserName"]', $ns).InnerText
来源 IP = $xml.SelectSingleNode('//evt:Data[@Name="IpAddress"]', $ns).InnerText
失败原因 = $xml.SelectSingleNode('//evt:Data[@Name="FailureReason"]', $ns).InnerText
}
} | Format-Table -AutoSize
1
2
3
4
5
时间                用户      来源 IP        失败原因
---- ---- ------- --------
2025/4/8 8:15:23 admin 10.0.0.55 %%2313
2025/4/8 8:12:01 testuser 192.168.1.100 %%2310
2025/4/7 22:30:45 backup 172.16.0.10 %%2313

错误模式识别与统计

对于运维人员来说,识别反复出现的错误模式比逐条查看日志更有价值。下面展示如何对事件日志进行统计分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 统计过去 7 天系统错误日志的分布
$startTime = (Get-Date).AddDays(-7)

Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2
StartTime = $startTime
} |
Group-Object -Property Id |
Sort-Object Count -Descending |
Select-Object -First 10 Count, Name |
ForEach-Object {
# 获取该 EventId 的描述
$sampleEvent = Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = [int]$_.Name
} -MaxEvents 1

[PSCustomObject]@{
出现次数 = $_.Count
事件ID = $_.Name
示例来源 = $sampleEvent.ProviderName
}
} | Format-Table -AutoSize

按时间段统计事件趋势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 按小时统计错误事件数量
$startTime = (Get-Date).AddDays(-1)

Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2
StartTime = $startTime
} |
Group-Object { $_.TimeCreated.ToString('yyyy-MM-dd HH:00') } |
Sort-Object Name |
ForEach-Object {
[PSCustomObject]@{
时间段 = $_.Name
错误数 = $_.Count
分布 = ('#' * [math]::Min($_.Count, 50))
}
} | Format-Table -AutoSize
1
2
3
4
5
6
7
8
时间段         错误数  分布
------ ------ ----
2025-04-07 10:00 3 ###
2025-04-07 11:00 7 #######
2025-04-07 12:00 2 ##
2025-04-07 14:00 12 ############
2025-04-07 15:00 5 #####
2025-04-08 08:00 4 ####

日志导出与归档

定期归档日志可以防止日志文件过大导致查询变慢,也便于历史回溯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 导出指定时间范围的安全日志为 CSV
$exportParams = @{
LogName = 'Security'
StartTime = (Get-Date).AddDays(-30)
EndTime = (Get-Date).AddDays(-7)
}

$exportPath = "C:\Logs\Security_Export_$(Get-Date -Format 'yyyyMMdd').csv"

Get-WinEvent @exportParams |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Export-Csv -Path $exportPath -NoTypeInformation -Encoding UTF8

Write-Host "日志已导出到:$exportPath"
$fileSize = (Get-Item $exportPath).Length / 1MB
Write-Host "文件大小:$([math]::Round($fileSize, 2)) MB"

使用 FilterXML 进行复杂查询

FilterHashtable 无法满足复杂查询需求时,可以使用 XML 精确定义查询条件。在”事件查看器”中可以先手动筛选,然后切换到”XML”选项卡复制查询语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 使用 XML 筛选:查询多个事件源的组合条件
$query = @'
<QueryList>
<Query Id="0" Path="System">
<Select Path="System">
*[System[Provider[@Name='Service Control Manager'] and (Level=2 or Level=3)]]
and
*[System[TimeCreated[@SystemTime&gt;='2025-04-01T00:00:00']]]
</Select>
</Query>
</QueryList>
'@

Get-WinEvent -FilterXml $query |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-Table -AutoSize -Wrap

构建自动化监控脚本

将日志查询整合到监控脚本中,可以实现自动告警。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 监控脚本:检测磁盘相关错误并在超过阈值时发出警告
$threshold = 5
$lookbackMinutes = 30

$diskErrors = Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2
StartTime = (Get-Date).AddMinutes(-$lookbackMinutes)
} | Where-Object { $_.Message -match 'disk|storage|volume' }

if ($diskErrors.Count -ge $threshold) {
$body = "过去 $lookbackMinutes 分钟内检测到 $($diskErrors.Count) 条磁盘相关错误:`n`n"
$diskErrors | ForEach-Object {
$body += "[$($_.TimeCreated)] EventId: $($_.Id) - $($_.Message.Substring(0, [math]::Min(100, $_.Message.Length)))...`n"
}
Write-Warning "磁盘错误数量超过阈值!"
Write-Host $body
# 实际环境中可调用 Send-MailMessage 或其他通知接口
} else {
Write-Host "磁盘状态正常,过去 $lookbackMinutes 分钟内共 $($diskErrors.Count) 条错误。"
}

注意事项

  • 查询安全日志需要管理员权限,请以”以管理员身份运行”启动 PowerShell
  • Get-WinEvent-FilterHashtableWhere-Object 管道筛选性能好数十倍,应优先使用
  • 查询大量日志时,合理使用 -MaxEvents 参数限制返回数量,避免内存占用过高
  • 对于已归档的 .evtx 文件,可以使用 Get-WinEvent -Path 直接查询
  • 在 PowerShell 7 中,Get-WinEvent 跨平台可用(需安装相应模块),但在 Linux/macOS 上功能有限
  • 建议将常用的日志查询封装成函数,便于在日常运维中复用