PowerShell 技能连载 - 结构化日志框架

适用于 PowerShell 5.1 及以上版本

在生产环境中管理 PowerShell 脚本时,Write-HostWrite-Output 这类简单输出方式很快就会暴露出不足:日志难以检索、无法按级别过滤、缺少上下文信息,更无法与 ELK、Splunk 等日志平台对接。当脚本数量增长到数十个、运行频率从手动变为定时任务时,缺乏结构化日志体系将成为运维效率的最大瓶颈。

结构化日志(Structured Logging)是解决这些问题的核心思路。它要求每条日志都携带固定格式的元数据——时间戳、级别、模块名、消息体以及自定义属性,并以 JSON 等机器可读格式输出。这样就能用 ConvertFrom-Json 在脚本内解析,也可以直接推送到日志平台进行全文检索和可视化分析。

本文将从零构建一个轻量但完整的结构化日志框架,涵盖日志级别控制、多目标输出(控制台 + 文件 + JSON 文件)、上下文自动注入、以及运行时的动态配置。

框架核心:日志条目数据结构

一切从定义日志条目的数据结构开始。我们用一个 PowerShell class 来承载每条日志的所有字段,确保输出格式始终一致。

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
enum LogLevel {
Debug
Information
Warning
Error
Critical
}

class LogEntry {
[DateTime] $Timestamp
[LogLevel] $Level
[string] $Message
[string] $Module
[hashtable] $Properties
[string] $CorrelationId

LogEntry(
[LogLevel] $Level,
[string] $Message,
[string] $Module,
[hashtable] $Properties,
[string] $CorrelationId
) {
$this.Timestamp = Get-Date
$this.Level = $Level
$this.Message = $Message
$this.Module = $Module
$this.Properties = $Properties
$this.CorrelationId = $CorrelationId
}

[hashtable] ToHashtable() {
$ht = [ordered]@{
timestamp = $this.Timestamp.ToString('o')
level = $this.Level.ToString()
module = $this.Module
message = $this.Message
correlationId = $this.CorrelationId
}
foreach ($key in $this.Properties.Keys) {
$ht[$key] = $this.Properties[$key]
}
return $ht
}

[string] ToJson() {
return $this.ToHashtable() | ConvertTo-Json -Compress
}
}

上面的代码定义了五个日志级别和一个 LogEntry 类。ToHashtable() 方法将日志转为有序哈希表,方便进一步处理;ToJson() 则输出紧凑的 JSON 字符串,可直接写入日志文件或发送到 HTTP 端点。注意这里使用 foreach 语句遍历属性字典,将自定义字段合并到输出中。

日志写入器:支持多目标输出

有了数据结构之后,需要实现写入器(Writer)来决定日志输出到哪里。下面定义三个写入器:控制台(带颜色)、纯文本文件、以及 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
46
47
48
49
50
51
52
53
54
55
56
57
class LoggerConfig {
[LogLevel] $MinimumLevel = [LogLevel]::Information
[string] $LogDirectory = '.\logs'
[string] $ApplicationName = 'PowerShellApp'
[string] $CorrelationId = ''
[System.Collections.Generic.List[scriptblock]] $Writers

LoggerConfig() {
$this.Writers = [System.Collections.Generic.List[scriptblock]]::new()
if (-not $this.CorrelationId) {
$this.CorrelationId = [guid]::NewGuid().ToString('N').Substring(0, 8)
}
}
}

function New-Logger {
param(
[LogLevel] $MinimumLevel = [LogLevel]::Information,
[string] $LogDirectory = '.\logs',
[string] $ApplicationName = 'PowerShellApp'
)
$config = [LoggerConfig]::new()
$config.MinimumLevel = $MinimumLevel
$config.LogDirectory = $LogDirectory
$config.ApplicationName = $ApplicationName

# 控制台写入器
$config.Writers.Add({
param($Entry)
$colorMap = @{
Debug = 'DarkGray'
Information = 'White'
Warning = 'Yellow'
Error = 'Red'
Critical = 'Magenta'
}
$color = $colorMap[$Entry.Level.ToString()]
Write-Host -ForegroundColor $color (
"[$($Entry.Timestamp.ToString('HH:mm:ss'))] " +
"[$($Entry.Level)] [$($Entry.Module)] $($Entry.Message)"
)
}.GetNewClosure())

# JSON 文件写入器
$config.Writers.Add({
param($Entry, $Config)
$dir = $Config.LogDirectory
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
$dateStr = (Get-Date).ToString('yyyyMMdd')
$filePath = Join-Path $dir "$($Config.ApplicationName)-$dateStr.jsonl"
$Entry.ToJson() | Add-Content -Path $filePath -Encoding UTF8
}.GetNewClosure())

return $config
}

这段代码的核心是 LoggerConfig 类:它保存全局配置(最低级别、日志目录、应用名称、关联 ID)和一个写入器列表。New-Logger 函数创建配置实例并默认注册两个写入器——控制台和 JSONL 文件。JSONL(JSON Lines)格式每行一条 JSON 记录,非常适合日志场景,追加写入效率高,读取时逐行 ConvertFrom-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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function Send-Log {
param(
[Parameter(Mandatory)]
[LoggerConfig] $Config,

[Parameter(Mandatory)]
[LogLevel] $Level,

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

[string] $Module = 'General',

[hashtable] $Properties = @{}
)

# 级别过滤
if ($Level -lt $Config.MinimumLevel) {
return
}

# 自动注入上下文属性
$Properties['host'] = $env:COMPUTERNAME
$Properties['user'] = $env:USERNAME
$Properties['psVersion'] = $PSVersionTable.PSVersion.ToString()

$entry = [LogEntry]::new(
$Level,
$Message,
$Module,
$Properties,
$Config.CorrelationId
)

foreach ($writer in $Config.Writers) {
try {
& $writer $entry $Config
} catch {
Write-Warning "日志写入器执行失败: $($_.Exception.Message)"
}
}
}

# 便捷函数
function Write-LogDebug {
param([LoggerConfig] $Config, [string] $Message, [string] $Module = 'General', [hashtable] $Properties = @{})
Send-Log -Config $Config -Level Debug -Message $Message -Module $Module -Properties $Properties
}

function Write-LogInfo {
param([LoggerConfig] $Config, [string] $Message, [string] $Module = 'General', [hashtable] $Properties = @{})
Send-Log -Config $Config -Level Information -Message $Message -Module $Module -Properties $Properties
}

function Write-LogWarning {
param([LoggerConfig] $Config, [string] $Message, [string] $Module = 'General', [hashtable] $Properties = @{})
Send-Log -Config $Config -Level Warning -Message $Message -Module $Module -Properties $Properties
}

function Write-LogError {
param([LoggerConfig] $Config, [string] $Message, [string] $Module = 'General', [hashtable] $Properties = @{})
Send-Log -Config $Config -Level Error -Message $Message -Module $Module -Properties $Properties
}

function Write-LogCritical {
param([LoggerConfig] $Config, [string] $Message, [string] $Module = 'General', [hashtable] $Properties = @{})
Send-Log -Config $Config -Level Critical -Message $Message -Module $Module -Properties $Properties
}

Send-Log 在创建日志条目前自动注入主机名、用户名和 PowerShell 版本三个上下文字段。通过 foreach 语句遍历所有注册的写入器并逐一执行,单个写入器失败不会影响其他写入器。五个便捷函数封装了对应的日志级别,调用时无需手动指定 -Level 参数。

实战:在脚本中使用日志框架

下面模拟一个典型的文件处理脚本,展示日志框架的完整使用方式。

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
# 初始化日志框架(Debug 级别,开发阶段能看到所有日志)
$logger = New-Logger -MinimumLevel Debug -LogDirectory '.\logs' -ApplicationName 'FileProcessor'

Write-LogInfo -Config $logger -Message '文件处理任务启动' -Module 'Main' -Properties @{
taskType = 'BatchImport'
sourcePath = 'D:\Data\Incoming'
}

# 模拟处理一批文件
$files = @('report-q3.csv', 'inventory.json', 'customers.xlsx', 'orders-corrupt.bad')

foreach ($file in $files) {
Write-LogDebug -Config $logger -Message "开始处理文件: $file" -Module 'FileHandler' -Properties @{
fileName = $file
fileSize = (Get-Random -Minimum 1024 -Maximum 10485760)
}

if ($file -match 'corrupt') {
Write-LogError -Config $logger -Message "文件损坏,跳过处理: $file" -Module 'FileHandler' -Properties @{
fileName = $file
errorCode = 'CORRUPT_HEADER'
recoverable = $false
}
continue
}

Write-LogInfo -Config $logger -Message "文件处理完成: $file" -Module 'FileHandler' -Properties @{
fileName = $file
rowsProcessed = (Get-Random -Minimum 100 -Maximum 5000)
durationMs = (Get-Random -Minimum 50 -Maximum 2000)
}
}

Write-LogWarning -Config $logger -Message '批次处理结束,存在跳过的文件' -Module 'Main' -Properties @{
totalFiles = $files.Count
successCount = ($files | Where-Object { $_ -notmatch 'corrupt' }).Count
skippedCount = 1
}

Write-LogInfo -Config $logger -Message '文件处理任务完成' -Module 'Main'

模拟执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
[14:30:01] [Information] [Main] 文件处理任务启动
[14:30:01] [Debug] [FileHandler] 开始处理文件: report-q3.csv
[14:30:01] [Information] [FileHandler] 文件处理完成: report-q3.csv
[14:30:01] [Debug] [FileHandler] 开始处理文件: inventory.json
[14:30:02] [Information] [FileHandler] 文件处理完成: inventory.json
[14:30:02] [Debug] [FileHandler] 开始处理文件: customers.xlsx
[14:30:02] [Information] [FileHandler] 文件处理完成: customers.xlsx
[14:30:02] [Debug] [FileHandler] 开始处理文件: orders-corrupt.bad
[14:30:02] [Error] [FileHandler] 文件损坏,跳过处理: orders-corrupt.bad
[14:30:02] [Warning] [Main] 批次处理结束,存在跳过的文件
[14:30:02] [Information] [Main] 文件处理任务完成

同时,在 .\logs 目录下会生成类似 FileProcessor-20250925.jsonl 的文件,每行是一条完整的 JSON 日志。下面是其中一行记录的格式化展示:

1
{"timestamp":"2025-09-25T14:30:02.1234567+08:00","level":"Error","module":"FileHandler","message":"文件损坏,跳过处理: orders-corrupt.bad","correlationId":"a3f8c1d2","fileName":"orders-corrupt.bad","errorCode":"CORRUPT_HEADER","recoverable":false,"host":"WORKSTATION01","user":"admin","psVersion":"7.4.0"}

查询与分析结构化日志

结构化日志最大的优势在于可以用标准工具进行查询。下面的函数演示如何快速检索 JSONL 日志文件。

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
function Search-LogArchive {
param(
[Parameter(Mandatory)]
[string] $Path,

[ValidateSet('Debug', 'Information', 'Warning', 'Error', 'Critical')]
[string] $Level,

[string] $Module,

[string] $MessagePattern,

[DateTime] $After,

[int] $Latest = 50
)

$records = [System.Collections.Generic.List[psobject]]::new()

$lines = Get-Content -Path $Path -Encoding UTF8

foreach ($line in $lines) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }

try {
$obj = $line | ConvertFrom-Json
} catch {
continue
}

# 按条件过滤
if ($Level -and $obj.level -ne $Level) { continue }
if ($Module -and $obj.module -ne $Module) { continue }
if ($MessagePattern -and $obj.message -notmatch $MessagePattern) { continue }
if ($After -and [DateTime]$obj.timestamp -lt $After) { continue }

$records.Add($obj)
}

# 按时间排序并取最近的记录
$results = $records | Sort-Object { $_.timestamp } -Descending |
Select-Object -First $Latest

return $results
}

# 使用示例:查询今天所有 Error 级别的日志
$errors = Search-LogArchive -Path '.\logs\FileProcessor-20250925.jsonl' -Level Error

foreach ($err in $errors) {
Write-Host "[$($err.timestamp)] $($err.message) (module=$($err.module), errorCode=$($err.errorCode))"
}

查询结果示例:

1
[2025-09-25T14:30:02.1234567+08:00] 文件损坏,跳过处理: orders-corrupt.bad (module=FileHandler, errorCode=CORRUPT_HEADER)

Search-LogArchive 函数逐行读取 JSONL 文件,使用 ConvertFrom-Json 解析每条记录后按条件过滤。支持按级别、模块、消息模式和时间范围过滤,返回最近的 N 条匹配记录。由于使用 foreach 语句逐行处理,即使是几十 MB 的日志文件也能保持较低的内存占用。

动态调整日志级别

在生产环境中,经常需要在不重启服务的情况下调整日志级别来排查问题。下面实现一个简单的运行时级别切换机制。

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
function Set-LoggerLevel {
param(
[Parameter(Mandatory)]
[LoggerConfig] $Config,

[Parameter(Mandatory)]
[LogLevel] $Level
)

$oldLevel = $Config.MinimumLevel
$Config.MinimumLevel = $Level

# 级别变更本身也记录到日志中
Send-Log -Config $Config -Level Information -Message "日志级别已变更" `
-Module 'Logger' -Properties @{
oldLevel = $oldLevel.ToString()
newLevel = $Level.ToString()
changedBy = $env:USERNAME
}
}

# 正常运行时使用 Information 级别
$logger = New-Logger -MinimumLevel Information -ApplicationName 'WebMonitor'

Write-LogInfo -Config $logger -Message '站点健康检查通过' -Module 'HealthCheck'
Write-LogDebug -Config $logger -Message '这条不会输出' -Module 'HealthCheck'

# 发现异常,临时切换到 Debug 级别排查
Set-LoggerLevel -Config $logger -Level Debug

Write-LogDebug -Config $logger -Message '现在 Debug 日志可见了' -Module 'HealthCheck'

# 排查完毕,恢复到 Information
Set-LoggerLevel -Config $logger -Level Information

执行结果:

1
2
3
4
[14:35:10] [Information] [HealthCheck] 站点健康检查通过
[14:35:10] [Information] [Logger] 日志级别已变更
[14:35:10] [Debug] [HealthCheck] 现在 Debug 日志可见了
[14:35:10] [Information] [Logger] 日志级别已变更

级别切换操作本身会被记录为一条 Information 日志,包含了变更前后的级别和操作人信息,方便审计追踪。这在多人共享的运维环境中尤为重要。

注意事项

  • 关联 ID 的使用:每个 LoggerConfig 实例会自动生成一个关联 ID(Correlation ID),在分布式系统中用于串联一次完整请求的所有日志。建议在脚本入口处将此 ID 传递给所有子模块和远程调用。
  • JSONL 与普通 JSON 的选择:日志文件推荐使用 JSONL 格式(每行一条 JSON),而非 JSON 数组。JSONL 天然支持追加写入,不会因为进程中断导致文件格式损坏,也方便 Get-Content 逐行处理。
  • 写入器的异常隔离Send-Log 中对每个写入器都使用了 try/catch,确保某个输出目标(如网络端点不可达)不会导致整个日志功能失效。生产环境中务必保留这一保护机制。
  • 大日志文件的查询性能:当日志文件超过 100MB 时,Get-Content 会消耗较多内存。建议按日期滚动日志文件,或使用 System.IO.StreamReader 进行流式读取。
  • 线程安全注意事项:PowerShell 的 foreach 语句和 Add-Content 在单一线程(单一 runspace)中是安全的。但如果使用 ForEach-Object -Parallel 或 Start-Job 等并发机制,需要额外加锁保护文件写入,否则会出现内容交错。
  • 敏感信息过滤:自动注入的上下文属性(主机名、用户名)可能包含敏感信息。在将日志推送到外部系统之前,建议实现一个脱敏写入器,对指定字段进行掩码处理。

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] 可避免