PowerShell 技能连载 - 可观测性与分布式追踪

适用于 PowerShell 7.0 及以上版本

在现代运维和 DevOps 实践中,可观测性(Observability)已成为系统可靠性的基石。与传统的被动监控不同,可观测性强调从系统外部输出推断内部状态的能力,其核心由三大支柱构成:日志(Logging)记录离散事件、指标(Metrics)量化系统状态、追踪(Tracing)串联请求链路。当 PowerShell 脚本从简单的自动化任务演进为跨系统编排工具时,缺乏可观测性就意味着排障时如同大海捞针。

在云原生环境中,一个运维操作可能横跨多个子系统:先调用 Azure API 获取资源列表,再通过 SSH 配置远程服务器,最后更新 CMDB 数据库。如果其中某个环节失败,仅凭零散的 Write-Host 输出几乎无法定位根因。通过引入结构化日志、指标采集和分布式追踪,我们可以为每条执行链路建立完整的”数字指纹”,让问题排查从猜测变为精确诊断。

本文将从三个层面逐步构建 PowerShell 脚本的可观测性体系:首先搭建统一的日志框架,确保所有脚本输出格式一致且可检索;然后实现性能指标采集,量化脚本的资源消耗与执行效率;最后引入分布式追踪机制,打通跨脚本、跨进程的调用链路。

结构化日志框架

良好的日志系统是可观测性的起点。与随意使用 Write-Host 不同,结构化日志要求每条日志都包含时间戳、级别、消息体和上下文信息,并以 JSON 格式输出,便于后续被 ELK、Splunk 或 Azure Log Analytics 等平台直接消费。

以下代码构建了一个轻量级的结构化日志模块,支持多级别控制、上下文注入和双通道输出(控制台 + 文件):

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
102
103
104
105
106
# 构建结构化日志框架
enum LogLevel {
Debug = 0
Information = 1
Warning = 2
Error = 3
Critical = 4
}

function New-StructuredLogger {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$LogName,

[string]$LogDirectory = './logs',

[LogLevel]$MinimumLevel = [LogLevel]::Information,

[hashtable]$DefaultContext = @{}
)

# 确保日志目录存在
if (-not (Test-Path $LogDirectory)) {
New-Item -ItemType Directory -Path $LogDirectory -Force | Out-Null
}

$logFile = Join-Path $LogDirectory "$LogName-$(Get-Date -Format 'yyyyMMdd').json"

# 返回日志记录器对象
[PSCustomObject]@{
Name = $LogName
LogFile = $logFile
MinimumLevel = $MinimumLevel
DefaultContext = $DefaultContext
} | Add-Member -MemberType ScriptMethod -Name 'Write' -Value {
param(
[LogLevel]$Level,
[string]$Message,
[hashtable]$AdditionalContext = @{},
[System.Management.Automation.ErrorRecord]$ErrorRecord = $null
)

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

$entry = [ordered]@{
timestamp = (Get-Date).ToString('o')
level = $Level.ToString()
logger = $this.Name
message = $Message
context = [ordered]@{}
}

# 合并默认上下文和附加上下文
foreach ($key in $this.DefaultContext.Keys) {
$entry.context[$key] = $this.DefaultContext[$key]
}
foreach ($key in $AdditionalContext.Keys) {
$entry.context[$key] = $AdditionalContext[$key]
}

# 异常信息
if ($ErrorRecord) {
$entry.exception = @{
type = $ErrorRecord.Exception.GetType().FullName
message = $ErrorRecord.Exception.Message
stackTrace = $ErrorRecord.ScriptStackTrace
}
}

$jsonLine = $entry | ConvertTo-Json -Depth 5 -Compress

# 写入文件
$jsonLine | Out-File -FilePath $this.LogFile -Append -Encoding utf8

# 控制台着色输出
$color = switch ($Level) {
([LogLevel]::Debug) { 'Gray' }
([LogLevel]::Information) { 'White' }
([LogLevel]::Warning) { 'Yellow' }
([LogLevel]::Error) { 'Red' }
([LogLevel]::Critical) { 'Magenta' }
default { 'White' }
}
Write-Host "[$($Level.ToString().ToUpper())] $Message" -ForegroundColor $color
} -Force -PassThru
}

# 创建日志记录器实例
$logger = New-StructuredLogger `
-LogName 'deployment-pipeline' `
-MinimumLevel ([LogLevel]::Debug) `
-DefaultContext @{ environment = 'production'; host = $env:COMPUTERNAME }

# 记录不同级别的日志
$logger.Write([LogLevel]::Information, '开始部署流程', @{ version = '2.4.1' })
$logger.Write([LogLevel]::Debug, '加载配置文件', @{ configPath = '/etc/app/config.yaml' })
$logger.Write([LogLevel]::Warning, '检测到内存使用率偏高', @{ memoryUsage = '82%' })

# 模拟一个异常场景
try {
throw '数据库连接超时'
} catch {
$logger.Write([LogLevel]::Error, '部署流程中断', @{ step = 'database-migration' }, $_)
}

执行结果示例:

1
2
3
4
[INFORMATION] 开始部署流程
[DEBUG] 加载配置文件
[WARNING] 检测到内存使用率偏高
[ERROR] 部署流程中断

每条日志以 JSON Lines 格式追加到文件中,可被日志平台直接索引。DefaultContext 参数允许注入环境名、主机名等全局上下文,避免在每条日志中重复传递。通过调整 MinimumLevel,可以在生产环境中过滤掉 Debug 级别的日志,减少存储开销。

性能指标采集

指标是可观测性的量化核心。与日志不同,指标关注的是可聚合的数值数据,如执行时长、内存增量、API 调用次数等。以下代码实现了一个轻量级的指标采集器,支持计数器、计时器和仪表盘三种度量类型:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# 性能指标采集器
function New-MetricsCollector {
[CmdletBinding()]
param(
[string]$ServiceName = 'powershell-script',
[string]$OutputPath = './metrics'
)

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

$metrics = [System.Collections.Concurrent.ConcurrentDictionary[string, PSObject]]::new()

$collector = [PSCustomObject]@{
ServiceName = $ServiceName
StartTime = Get-Date
OutputPath = $OutputPath
}

# 计数器:递增统计
$collector | Add-Member -MemberType ScriptMethod -Name 'Increment' -Value {
param([string]$MetricName, [int64]$Value = 1, [hashtable]$Tags = @{})

$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
$metrics.AddOrUpdate(
$MetricName,
{ [PSCustomObject]@{ type = 'counter'; value = $Value; tags = $Tags; updatedAt = $now } },
{ param($key, $existing)
$existing.value += $Value
$existing.updatedAt = $now
$existing
}
)
} -Force

# 计时器:测量执行时长
$collector | Add-Member -MemberType ScriptMethod -Name 'Measure' -Value {
param(
[string]$MetricName,
[scriptblock]$Action,
[hashtable]$Tags = @{}
)

$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
$result = & $Action
$stopwatch.Stop()

$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
$metric = [PSCustomObject]@{
type = 'timer'
value = $stopwatch.ElapsedMilliseconds
unit = 'ms'
tags = $Tags
updatedAt = $now
}
$metrics[$MetricName] = $metric

$result
} -Force

# 仪表盘:记录当前值
$collector | Add-Member -MemberType ScriptMethod -Name 'Gauge' -Value {
param([string]$MetricName, [double]$Value, [string]$Unit = '', [hashtable]$Tags = @{})

$now = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
$metrics[$MetricName] = [PSCustomObject]@{
type = 'gauge'
value = $Value
unit = $Unit
tags = $Tags
updatedAt = $now
}
} -Force

# 导出为 OpenTelemetry 兼容格式
$collector | Add-Member -MemberType ScriptMethod -Name 'Export' -Value {
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$exportFile = Join-Path $this.OutputPath "$($this.ServiceName)-metrics-$timestamp.json"

$exportData = @{
resource = @{
'service.name' = $this.ServiceName
'service.version' = '1.0.0'
'host.name' = $env:COMPUTERNAME
}
scopeMetrics = @(
@{
scope = @{ name = 'powershell-metrics' }
metrics = @()
}
)
timestamp = (Get-Date).ToString('o')
}

foreach ($key in $metrics.Keys) {
$m = $metrics[$key]
$metricEntry = [ordered]@{
name = $key
type = $m.type
value = $m.value
}
if ($m.unit) { $metricEntry.unit = $m.unit }
if ($m.tags.Count -gt 0) { $metricEntry.tags = $m.tags }
$exportData.scopeMetrics[0].metrics += $metricEntry
}

$exportData | ConvertTo-Json -Depth 5 | Out-File -FilePath $exportFile -Encoding utf8
Write-Host "指标已导出到:$exportFile"
Write-Host "共采集 $($metrics.Count) 项指标"
} -Force

$collector
}

# 使用示例:采集脚本执行的各项指标
$metrics = New-MetricsCollector -ServiceName 'backup-job'

# 计数器:统计处理的文件数量
1..10 | ForEach-Object { $metrics.Increment('files.processed') }
$metrics.Increment('errors.total', 2)

# 计时器:测量数据库备份耗时
$dbBackupTime = $metrics.Measure('db.backup.duration', {
Start-Sleep -Milliseconds 150
'backup completed'
}, @{ database = 'app_db' })

# 计时器:测量文件上传耗时
$metrics.Measure('upload.duration', {
Start-Sleep -Milliseconds 80
}, @{ destination = 'azure-blob' })

# 仪表盘:记录当前内存使用
$mem = [System.GC]::GetTotalMemory($false)
$metrics.Gauge('memory.usage.bytes', $mem, 'bytes')
$metrics.Gauge('disk.free.percent', 67.3, 'percent', @{ drive = 'C:' })

# 导出所有指标
$metrics.Export()

执行结果示例:

1
2
指标已导出到:./metrics/backup-job-metrics-20260410-080000.json
共采集 5 项指标

导出的 JSON 遵循 OpenTelemetry 的资源-作用域-指标层级结构,可以被 Prometheus、Grafana 或 Jaeger 等工具链直接消费。Measure 方法使用 System.Diagnostics.Stopwatch 提供毫秒级精度的计时,适用于需要精确度量的 I/O 操作和 API 调用场景。

分布式追踪实现

分布式追踪是可观测性三大支柱中最复杂但也最有价值的部分。一个 Trace 由多个 Span 组成,每个 Span 代表一个操作单元。当 PowerShell 脚本编排多个外部系统调用时,通过 Span 和 Trace ID 将它们串联起来,就能在 Jaeger 或 Zipkin 中看到完整的调用拓扑。

以下代码实现了一个兼容 OpenTelemetry 概念的追踪框架,支持嵌套 Span、上下文传播和 OTLP 格式导出:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# 分布式追踪实现
function New-TraceContext {
[CmdletBinding()]
param(
[string]$TraceId = (1..32 | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) }) -join '',
[string]$ParentSpanId = $null
)

$spanId = (1..16 | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) }) -join ''

[PSCustomObject]@{
traceId = $TraceId
spanId = $spanId
parentSpanId = $ParentSpanId
startTime = [DateTimeOffset]::UtcTime
attributes = [ordered]@{}
events = [System.Collections.Generic.List[PSObject]]::new()
status = 'OK'
}
}

function New-Tracer {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ServiceName,

[string]$ExporterPath = './traces'
)

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

# Span 栈:支持嵌套调用
$spanStack = [System.Collections.Stack]::new()
$completedSpans = [System.Collections.Generic.List[PSObject]]::new()

$tracer = [PSCustomObject]@{
ServiceName = $ServiceName
ExporterPath = $ExporterPath
}

# 开始一个新的 Span
$tracer | Add-Member -MemberType ScriptMethod -Name 'StartSpan' -Value {
param(
[string]$Name,
[hashtable]$Attributes = @{}
)

$parentSpanId = if ($spanStack.Count -gt 0) { $spanStack.Peek().spanId } else { $null }
$traceId = if ($spanStack.Count -gt 0) { $spanStack.Peek().traceId } else { $null }

$span = New-TraceContext -TraceId $traceId -ParentSpanId $parentSpanId
$span | Add-Member -MemberType NoteProperty -Name 'name' -Value $Name

foreach ($key in $Attributes.Keys) {
$span.attributes[$key] = $Attributes[$key]
}

# 注入系统信息
$span.attributes['service.name'] = $this.ServiceName
$span.attributes['host.name'] = $env:COMPUTERNAME

$spanStack.Push($span)
$span
} -Force

# 添加事件到当前 Span
$tracer | Add-Member -MemberType ScriptMethod -Name 'AddEvent' -Value {
param([string]$EventName, [hashtable]$Attributes = @{})

if ($spanStack.Count -eq 0) {
Write-Warning '没有活跃的 Span,无法添加事件'
return
}

$currentSpan = $spanStack.Peek()
$currentSpan.events.Add([PSCustomObject]@{
name = $EventName
timestamp = [DateTimeOffset]::UtcTime.ToString('o')
attributes = $Attributes
})
} -Force

# 结束当前 Span
$tracer | Add-Member -MemberType ScriptMethod -Name 'EndSpan' -Value {
param([string]$Status = 'OK')

if ($spanStack.Count -eq 0) {
Write-Warning '没有活跃的 Span 可以结束'
return
}

$span = $spanStack.Pop()
$endTime = [DateTimeOffset]::UtcTime
$duration = ($endTime - $span.startTime).TotalMilliseconds

$span.status = $Status

$completedSpan = [ordered]@{
traceId = $span.traceId
spanId = $span.spanId
parentSpanId = $span.parentSpanId
name = $span.name
startTime = $span.startTime.ToString('o')
endTime = $endTime.ToString('o')
durationMs = [math]::Round($duration, 2)
status = $span.status
attributes = $span.attributes
events = $span.events.ToArray()
resource = [ordered]@{
'service.name' = $this.ServiceName
'service.version' = '1.0.0'
'telemetry.sdk.name' = 'powershell-custom'
}
}

$completedSpans.Add($completedSpan)
$completedSpan
} -Force

# 导出为 OTLP JSON 格式
$tracer | Add-Member -MemberType ScriptMethod -Name 'Export' -Value {
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$exportFile = Join-Path $this.ExporterPath "$($this.ServiceName)-trace-$timestamp.json"

$otlpExport = @{
resourceSpans = @(
@{
resource = @{
attributes = @(
@{ key = 'service.name'; value = @{ stringValue = $this.ServiceName } }
)
}
scopeSpans = @(
@{
scope = @{ name = 'powershell-tracer' }
spans = $completedSpans
}
)
}
)
}

$otlpExport | ConvertTo-Json -Depth 8 | Out-File -FilePath $exportFile -Encoding utf8
Write-Host "追踪数据已导出到:$exportFile"
Write-Host "共记录 $($completedSpans.Count) 个 Span"

# 输出调用链摘要
Write-Host "`n--- 调用链摘要 ---"
foreach ($s in $completedSpans) {
$indent = if ($s.parentSpanId) { ' ' } else { '' }
Write-Host "$indent[$($s.status)] $($s.name) - $($s.durationMs)ms"
}
} -Force

$tracer
}

# 模拟一个完整的多层调用链
$tracer = New-Tracer -ServiceName 'deployment-pipeline'

# 根 Span:整个部署流程
$rootSpan = $tracer.StartSpan('deploy.release', @{ version = '2.4.1'; environment = 'production' })

# 子 Span 1:拉取代码
$pullSpan = $tracer.StartSpan('git.pull', @{ repository = 'app-service' })
Start-Sleep -Milliseconds 50
$tracer.AddEvent('checkout_complete', @{ branch = 'main'; commit = 'a1b2c3d' })
$tracer.EndSpan()

# 子 Span 2:运行测试
$testSpan = $tracer.StartSpan('test.run', @{ framework = 'pytest' })
$tracer.AddEvent('test_started', @{ totalTests = 142 })
Start-Sleep -Milliseconds 120

# 孙 Span:代码覆盖率分析
$coverageSpan = $tracer.StartSpan('test.coverage', @{ threshold = '80%' })
Start-Sleep -Milliseconds 30
$tracer.AddEvent('coverage_calculated', @{ lineCoverage = '87.3%'; branchCoverage = '82.1%' })
$coverageResult = $tracer.EndSpan()

$tracer.AddEvent('test_completed', @{ passed = 140; failed = 2 })
$testResult = $tracer.EndSpan()

# 子 Span 3:部署到生产
$deploySpan = $tracer.StartSpan('deploy.push', @{ target = 'k8s-cluster-01' })
$tracer.AddEvent('rolling_update_started', @{ replicas = 3 })
Start-Sleep -Milliseconds 80
$tracer.AddEvent('health_check_passed')
$deployResult = $tracer.EndSpan()

# 结束根 Span
$tracer.EndSpan()

# 导出追踪数据
$tracer.Export()

执行结果示例:

1
2
3
4
5
6
7
8
9
追踪数据已导出到:./traces/deployment-pipeline-trace-20260410-080000.json
共记录 5 个 Span

--- 调用链摘要 ---
[OK] deploy.release - 284.37ms
[OK] git.pull - 52.14ms
[OK] test.run - 153.89ms
[OK] test.coverage - 33.27ms
[OK] deploy.push - 83.41ms

通过 spanStack 实现的嵌套机制,子 Span 自动继承父 Span 的 traceIdspanId,形成完整的调用树。导出格式兼容 OTLP JSON,可以直接发送到 OpenTelemetry Collector,再由 Collector 转发到 Jaeger、Zipkin 或 Tempo 等后端进行可视化分析。

注意事项

  1. 日志级别与环境匹配:开发环境建议将 MinimumLevel 设为 Debug,生产环境至少设为 Information。过于详细的日志在生产环境会产生大量 I/O 开销,尤其是在高频循环中记录日志时需格外谨慎。

  2. JSON 序列化深度ConvertTo-Json 默认深度仅为 2,嵌套的 hashtablePSCustomObject 会被截断为字符串。在日志和指标导出中务必指定 -Depth 5 或更高,否则结构化数据会丢失。

  3. Trace ID 的唯一性:示例中使用 Get-Random 生成 Trace/Span ID 仅供演示,生产环境建议使用 [guid]::NewGuid().ToString('N') 或基于时间戳的确定性算法,确保全局唯一性。

  4. 大对象内存压力:长时间运行的脚本中,$completedSpans 列表可能累积大量数据。建议设置导出阈值(如每 1000 个 Span 自动导出并清空),或在 EndSpan 时直接流式写入文件,避免内存持续增长。

  5. 线程安全考量ConcurrentDictionary 用于指标存储是线程安全的,但 Span 的栈操作(Push/Pop)不是。如果你的脚本使用 ForEach-Object -Parallel,每个 runspace 应拥有独立的 Tracer 实例,最后再合并导出。

  6. 与 OpenTelemetry Collector 集成:生产环境中,建议将导出的 JSON 文件通过 Filelog Receiver 或 HTTP Receiver 发送到 OpenTelemetry Collector,由 Collector 统一处理采样、批处理和路由,而不是让每个脚本直接对接后端存储。

PowerShell 技能连载 - Azure Monitor 仪表板自动化

适用于 PowerShell 7.0 及以上版本,需要 Az.Monitor 模块

在云原生运维中,Azure Monitor 仪表板是将海量监控数据转化为可视化洞察的核心工具。通过仪表板,运维团队可以一目了然地掌握虚拟机 CPU 利用率、存储账户延迟、应用网关吞吐量等关键指标,从而快速定位性能瓶颈和潜在故障。然而,当企业规模扩展到数十个订阅、上百个资源时,手动在 Azure 门户中拖拽创建仪表板不仅耗时,而且难以保证一致性。

PowerShell 提供了完整的 Azure Dashboard JSON 模板操控能力,结合 Az.Monitor 模块,我们可以将仪表板的创建、修改和部署完全纳入基础设施即代码(IaC)流程。这意味着每套环境都能拥有标准化的监控视图,变更可追溯、可审计、可回滚,大幅降低人为失误风险。

本文将围绕三个核心场景展开:动态构建仪表板 JSON 模板、配置指标告警与自动通知、以及跨订阅批量部署标准化仪表板,帮助你建立一套完整的 Azure Monitor 仪表板自动化工作流。

仪表板 JSON 模板构建

Azure Dashboard 的底层是一个 JSON 文档,定义了每个磁贴(tile)的类型、位置、大小和数据源。我们可以用 PowerShell 哈希表和 ConvertTo-Json 动态生成这个结构,实现参数化的仪表板模板。

以下函数封装了仪表板 JSON 的构建逻辑,支持自定义标题、订阅 ID 和资源组参数,并预置了 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
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
102
103
104
105
106
107
108
function New-AzDashboardTemplate {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$DashboardName,

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

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

[int]$RefreshIntervalSeconds = 300
)

$cpuTile = @{
position = @{ x = 0; y = 0; colSpan = 6; rowSpan = 4 }
metadata = @{
type = 'Extension/Microsoft_Azure_Monitoring/PartType/MetricsChartPart'
inputs = @(
@{
name = 'query'
value = @{
id = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachines"
chartType = 0
metrics = @(
@{ name = 'Percentage CPU'; resourceId = "/subscriptions/$SubscriptionId" }
)
timespan = @{ duration = 'PT1H' }
interval = 'PT5M'
}
}
)
settings = @{
content = @{
options = @{
chart = @{
groupBy = $null
topRows = 10
}
}
}
}
}
}

$memTile = @{
position = @{ x = 6; y = 0; colSpan = 6; rowSpan = 4 }
metadata = @{
type = 'Extension/Microsoft_Azure_Monitoring/PartType/MetricsChartPart'
inputs = @(
@{
name = 'query'
value = @{
id = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Compute/virtualMachines"
chartType = 0
metrics = @(
@{ name = 'Available Memory Bytes'; resourceId = "/subscriptions/$SubscriptionId" }
)
timespan = @{ duration = 'PT1H' }
interval = 'PT5M'
}
}
)
}
}

$dashboard = @{
id = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Portal/dashboards/$DashboardName"
name = $DashboardName
type = 'Microsoft.Portal/dashboards'
location = 'global'
tags = @{ createdBy = 'PowerShell-Automation'; createdAt = (Get-Date -Format 'yyyy-MM-dd') }
properties = @{
lenses = @{
'0' = @{
order = 0
parts = @($cpuTile, $memTile)
}
}
metadata = @{
model = @{
editable = $true
timeRange = @{
value = @{ relative = @{ duration = 24; timeUnit = 1 } }
type = 'MsPortalFx.Composition.Configuration.ValueTypes.TimeRangeType.Relative'
}
filter = @{
value = $null
type = 'MsPortalFx.Composition.Configuration.ValueTypes.FilterType.Callout'
}
}
}
}
}

# 深度设置为 10 层,确保嵌套结构完整输出
$dashboard | ConvertTo-Json -Depth 10
}

# 生成仪表板 JSON
$json = New-AzDashboardTemplate `
-DashboardName 'prod-vm-monitor' `
-SubscriptionId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' `
-ResourceGroupName 'rg-production'

$json | Out-File -FilePath './prod-vm-monitor-dashboard.json' -Encoding utf8
Write-Host "仪表板 JSON 已生成,文件大小:$((Get-Item './prod-vm-monitor-dashboard.json').Length) 字节"

执行结果示例:

1
仪表板 JSON 已生成,文件大小:2847 字节

生成的 JSON 文件可以直接通过 Azure 门户导入,也可以用 New-AzPortalDashboard 或 REST API 部署到指定资源组。通过修改 $cpuTile$memTile 中的指标名称,你可以快速扩展出网络吞吐量、磁盘 I/O 等更多监控视图。

指标告警与自动通知

仪表板负责展示,告警负责驱动行动。在 Azure Monitor 体系中,指标告警规则(Metric Alert Rule)配合操作组(Action Group),可以在指标突破阈值时自动触发邮件、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
# 先确保已登录并选择正确的订阅
# Connect-AzAccount
# Set-AzContext -SubscriptionId 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

$ResourceGroup = 'rg-production'
$ActionGroupName = 'ag-ops-team'
$AlertRuleName = 'alert-vm-cpu-high'

# 创建操作组:邮件通知 + Webhook
$emailReceiver = New-AzActionGroupReceiver `
-Name 'ops-email' `
-EmailAddress 'ops-team@contoso.com'

$webhookReceiver = New-AzActionGroupReceiver `
-Name 'incident-webhook' `
-WebhookUri 'https://hooks.example.com/azure-alerts'

$actionGroup = Set-AzActionGroup `
-ResourceGroupName $ResourceGroup `
-Name $ActionGroupName `
-ShortName 'OpsTeam' `
-Receiver $emailReceiver, $webhookReceiver

Write-Host "操作组已创建:$($actionGroup.Name)"

# 获取操作组的 ARM ID,告警规则需要引用
$actionGroupId = $actionGroup.Id

# 创建指标告警规则:CPU 使用率超过 85% 持续 5 分钟触发
$targetVm = Get-AzVM -ResourceGroupName $ResourceGroup -Name 'vm-web-01'
$vmResourceId = $targetVm.Id

$criteria = New-AzMetricAlertRuleV2Criteria `
-MetricName 'Percentage CPU' `
-TimeAggregation 'Average' `
-Operator 'GreaterThan' `
-Threshold 85

$actionGroupObject = New-AzMetricAlertRuleV2ActionGroup `
-ActionGroupId $actionGroupId

# 设置告警规则的维度过滤(可选)
$dimension = New-AzMetricAlertRuleV2DimensionSelection `
-DimensionName 'VMName' `
-ValuesToInclude '*'

$alert = Add-AzMetricAlertRuleV2 `
-Name $AlertRuleName `
-ResourceGroupName $ResourceGroup `
-WindowSize 'PT5M' `
-Frequency 'PT1M' `
-TargetResourceId $vmResourceId `
-Condition $criteria `
-ActionGroup $actionGroupObject `
-Severity 2 `
-Description "VM $($targetVm.Name) CPU 使用率超过 85% 持续 5 分钟"

Write-Host "告警规则已创建:$($alert.Name)"
Write-Host "目标资源:$vmResourceId"
Write-Host "阈值条件:Average Percentage CPU > 85%"
Write-Host "评估窗口:5 分钟,评估频率:1 分钟"

执行结果示例:

1
2
3
4
5
操作组已创建:ag-ops-team
告警规则已创建:alert-vm-cpu-high
目标资源:/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-production/providers/Microsoft.Compute/virtualMachines/vm-web-01
阈值条件:Average Percentage CPU > 85%
评估窗口:5 分钟,评估频率:1 分钟

告警触发后,Azure 会同时向 ops-team@contoso.com 发送告警邮件,并向 https://hooks.example.com/azure-alerts 推送 Webhook 请求。你可以将 Webhook 对接到企业微信、飞书、Slack 等即时通讯平台,实现秒级告警触达。

多订阅仪表板批量部署

在企业级场景中,运维团队通常需要管理多个订阅(开发、测试、预生产、生产),每个订阅都应部署一套标准化的监控仪表板。手动逐个部署显然不现实,下面通过参数化模板和循环部署来解决这个问题:

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
# 定义目标订阅列表和对应的资源配置
$subscriptions = @(
@{
SubscriptionId = 'aaaa1111-bbbb-2222-cccc-3333dddd4444'
Name = 'production'
ResourceGroup = 'rg-production'
VmPrefix = 'vm-prod'
AlertEmail = 'prod-ops@contoso.com'
}
@{
SubscriptionId = 'eeee5555-ffff-6666-gggg-7777hhhh8888'
Name = 'staging'
ResourceGroup = 'rg-staging'
VmPrefix = 'vm-stg'
AlertEmail = 'staging-ops@contoso.com'
}
@{
SubscriptionId = 'iiii9999-jjjj-0000-kkkk-1111llll2222'
Name = 'development'
ResourceGroup = 'rg-dev'
VmPrefix = 'vm-dev'
AlertEmail = 'dev-ops@contoso.com'
}
)

$deployResults = [System.Collections.Generic.List[PSObject]]::new()

foreach ($sub in $subscriptions) {
Write-Host "`n--- 正在处理订阅:$($sub.Name) ---" -ForegroundColor Cyan

# 切换到目标订阅
$context = Set-AzContext -SubscriptionId $sub.SubscriptionId
Write-Host " 已切换到订阅:$($context.Subscription.Name)"

# 生成仪表板 JSON
$dashboardName = "$($sub.Name)-standard-dashboard"
$dashboardJson = New-AzDashboardTemplate `
-DashboardName $dashboardName `
-SubscriptionId $sub.SubscriptionId `
-ResourceGroupName $sub.ResourceGroup

# 将 JSON 部署为 Azure 仪表板
$tempFile = New-TemporaryFile
$dashboardJson | Out-File -FilePath $tempFile.FullName -Encoding utf8

# 使用 REST API 部署仪表板
$token = (Get-AzAccessToken).Token
$uri = "https://management.azure.com/subscriptions/$($sub.SubscriptionId)/resourceGroups/$($sub.ResourceGroup)/providers/Microsoft.Portal/dashboards/$dashboardName`?api-version=2020-09-01-preview"

$headers = @{
Authorization = "Bearer $token"
'Content-Type' = 'application/json'
}

$response = Invoke-RestMethod -Uri $uri -Method Put -Headers $headers -Body $dashboardJson
Write-Host " 仪表板已部署:$dashboardName"

# 为该订阅创建操作组和告警规则
$agName = "ag-$($sub.Name)-ops"
$emailReceiver = New-AzActionGroupReceiver -Name 'team-email' -EmailAddress $sub.AlertEmail
$actionGroup = Set-AzActionGroup `
-ResourceGroupName $sub.ResourceGroup `
-Name $agName `
-ShortName "$($sub.Name)Ops" `
-Receiver $emailReceiver

Write-Host " 操作组已创建:$agName"

# 记录部署结果
$deployResults.Add([PSCustomObject]@{
Subscription = $sub.Name
Dashboard = $dashboardName
ActionGroup = $agName
Status = 'Deployed'
DeployTime = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
})
}

# 输出部署汇总
Write-Host "`n========== 部署汇总 ==========" -ForegroundColor Yellow
$deployResults | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
--- 正在处理订阅:production ---
已切换到订阅:production-subscription
仪表板已部署:production-standard-dashboard
操作组已创建:ag-production-ops

--- 正在处理订阅:staging ---
已切换到订阅:staging-subscription
仪表板已部署:staging-standard-dashboard
操作组已创建:ag-staging-ops

--- 正在处理订阅:development ---
已切换到订阅:development-subscription
仪表板已部署:development-standard-dashboard
操作组已创建:ag-dev-ops

========== 部署汇总 ==========
Subscription Dashboard ActionGroup Status DeployTime
------------ --------- ----------- ------ ----------
production production-standard-dashboard ag-production-ops Deployed 2026-04-07 10:30:15
staging staging-standard-dashboard ag-staging-ops Deployed 2026-04-07 10:30:42
development development-standard-dashboard ag-dev-ops Deployed 2026-04-07 10:31:08

这个脚本的核心思路是将订阅特定的参数(资源组名、VM 前缀、告警邮箱)外置到配置数组中,然后通过循环统一调用前面定义的模板生成函数。你可以进一步将 $subscriptions 数组替换为从 CSV 或 Azure Key Vault 读取的配置,实现真正的配置与代码分离。

注意事项

  1. Az.Monitor 模块版本:建议使用 Az.Monitor 4.0 以上版本,旧版本的 Add-AzMetricAlertRuleV2 参数名和行为有差异。安装前先执行 Get-InstalledModule Az.Monitor 确认当前版本。

  2. 仪表板 JSON 深度问题:Azure Dashboard 的 JSON 嵌套层级较深(通常 8-10 层),使用 ConvertTo-Json 时必须指定 -Depth 10,否则内层数据会被截断为字符串。

  3. REST API Token 有效期:通过 Get-AzAccessToken 获取的 Bearer Token 默认有效期 1 小时。如果批量部署涉及大量订阅,建议在循环内每次都重新获取 Token,避免中途过期导致 401 错误。

  4. 告警规则配额限制:每个 Azure 订阅的指标告警规则数量有上限(默认 5000 条),跨订阅批量创建前应检查 Get-AzMetricAlertRuleV2 的返回数量,避免超出配额。

  5. 操作组的 Webhook 超时:Azure Action Group 发送 Webhook 时,超时时间为 10 秒。如果你的下游服务响应较慢,建议在中间加一个队列服务(如 Azure Functions + Service Bus),避免 Webhook 调用失败。

  6. 仪表板权限控制:通过 PowerShell 创建的仪表板默认只有创建者有编辑权限。如果需要团队成员共同维护,应通过 Azure RBAC 为资源组级别的 Microsoft.Portal/dashboards 资源分配 ContributorReader 角色。

PowerShell 技能连载 - Azure Monitor 告警自动化

适用于 PowerShell 7.0 及以上版本

Azure Monitor 是 Azure 的统一监控平台,负责收集、分析和处理来自云资源与应用的指标和日志数据。随着云基础设施规模的不断增长,运维团队需要面对成百上千个资源的健康状态监控需求,手动在门户中逐个配置告警规则既繁琐又容易遗漏关键阈值。

通过 PowerShell 与 Az 模块的结合,我们可以将告警规则的创建、通知动作组的配置以及告警历史查询全部脚本化。这不仅大幅提升了部署效率,还让告警策略成为代码的一部分,可以纳入版本控制和 CI/CD 流水线进行审计与回滚。

本文将围绕三个核心场景展开:指标告警规则的批量创建、基于 Log Analytics 日志查询的告警与通知配置、以及告警生命周期管理与健康合规审计。掌握这些技巧后,你可以轻松实现可观测性即代码(Observability as Code)的实践。

创建指标告警规则

在 Azure Monitor 中,指标告警(Metric Alert)是最常用的监控手段之一。当某个资源的性能指标(如 CPU 使用率、内存占用、磁盘 I/O)超过预设阈值时,系统会自动触发告警。下面我们通过 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
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
function New-AzMetricAlertRule {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string]$ResourceGroupName,

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

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

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

[Parameter(Mandatory)]
[double]$Threshold,

[Parameter()]
[string]$Operator = 'GreaterThan',

[Parameter()]
[string]$TimeAggregation = 'Average',

[Parameter()]
[string]$WindowSize = 'PT5M',

[Parameter()]
[string]$EvaluationFrequency = 'PT1M',

[Parameter()]
[string]$Severity = '2'
)

$condition = New-AzMetricAlertRuleV2Criteria `
-MetricName $MetricName `
-TimeAggregation $TimeAggregation `
-Operator $Operator `
-Threshold $Threshold

$params = @{
ResourceGroupName = $ResourceGroupName
Name = $AlertName
TargetResourceId = $TargetResourceId
Criterion = $condition
WindowSize = $WindowSize
EvaluationFrequency = $EvaluationFrequency
Severity = $Severity
AutoMitigate = $true
}

if ($PSCmdlet.ShouldProcess($AlertName, '创建指标告警规则')) {
Add-AzMetricAlertRuleV2 @params
}
}

# 定义标准告警模板
$alertTemplates = @(
@{
Name = 'cpu-high'
Metric = 'Percentage CPU'
Threshold = 85
Window = 'PT5M'
Severity = '2'
}
@{
Name = 'memory-high'
Metric = 'Available Memory Bytes'
Threshold = 1073741824 # 1 GB
Window = 'PT5M'
Severity = '2'
}
@{
Name = 'disk-read-latency'
Metric = 'Average Read Latency'
Threshold = 30 # 毫秒
Window = 'PT10M'
Severity = '3'
}
)

# 获取目标资源并批量创建告警
$vms = Get-AzVM -ResourceGroupName 'prod-rg'
foreach ($vm in $vms) {
$resourceId = $vm.Id
foreach ($template in $alertTemplates) {
New-AzMetricAlertRule `
-ResourceGroupName 'prod-rg' `
-TargetResourceId $resourceId `
-AlertName "$($vm.Name)-$($template.Name)" `
-MetricName $template.Metric `
-Threshold $template.Threshold `
-WindowSize $template.Window `
-Severity $template.Severity
}
Write-Host "已为 VM [$($vm.Name)] 创建 $($alertTemplates.Count) 条告警规则"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
已为 VM [web-frontend-01] 创建 3 条告警规则
已为 VM [web-frontend-02] 创建 3 条告警规则
已为 VM [api-backend-01] 创建 3 条告警规则
已为 VM [db-primary-01] 创建 3 条告警规则

Location Name Severity IsEnabled
-------- ---- -------- ---------
eastasia web-frontend-01-cpu-high 2 True
eastasia web-frontend-01-memory-high 2 True
eastasia web-frontend-01-disk-read-latency 3 True

通过模板化的方式,我们为每个虚拟机统一创建了 CPU、内存和磁盘延迟三个维度的告警。AutoMitigate 参数设为 $true 表示当指标恢复到阈值以下时告警会自动解除,避免告警堆积。

日志查询告警与动作组配置

指标告警关注的是实时数值,而日志查询告警(Log Alert)则更适合检测复杂模式和趋势。结合 Log Analytics 的 Kusto 查询语言(KQL),我们可以编写更灵活的检测逻辑。同时,动作组(Action Group)定义了告警触发后的通知方式,包括邮件、短信、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
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
function New-AzLogAlertWithActionGroup {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ResourceGroupName,

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

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

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

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

[Parameter()]
[int]$ThresholdValue = 0,

[Parameter()]
[string]$Operator = 'GreaterThan',

[Parameter()]
[string]$TimeRange = 'PT1H',

[Parameter()]
[string]$EvaluationFrequency = 'PT30M',

[Parameter()]
[string]$Severity = '2'
)

# 创建日志查询计划
$schedule = New-AzScheduledQueryRuleScheduleObject `
-FrequencyInMinutes ([int]$EvaluationFrequency.TrimStart('PT','M')) `
-TimeWindowInMinutes ([int]$TimeRange.TrimStart('PT','H') * 60)

# 创建评估条件
$condition = New-AzScheduledQueryRuleConditionObject `
-Query $Query `
-TimeAggregationMethod 'Count' `
-Operator $Operator `
-Threshold $ThresholdValue

# 创建告警规则并关联动作组
$params = @{
ResourceGroupName = $ResourceGroupName
Name = $AlertName
Scopes = @($WorkspaceId)
Severity = $Severity
Enabled = $true
EvaluationSchedule = $schedule
ConditionAllOf = $condition
ActionGroupId = $ActionGroupId
}

New-AzScheduledQueryRule @params
}

# 创建动作组(邮件 + Webhook 通知)
$actionEmail = @{
Name = 'notify-ops-team'
EmailAddress = 'ops-team@contoso.com'
}
$actionWebhook = @{
Name = 'trigger-incident-system'
ServiceUri = 'https://hooks.incident.contoso.com/api/alert'
}

$actionGroup = Set-AzActionGroup `
-ResourceGroupName 'monitoring-rg' `
-Name 'ops-oncall-action-group' `
-EmailReceiver $actionEmail `
-WebhookReceiver $actionWebhook

# 检测 HTTP 5xx 错误激增的日志告警
$errorQuery = @"
let threshold = 50;
AppRequests
| where TimeGenerated > ago(1h)
| where ResultCode startswith '5'
| summarize ErrorCount = count() by bin(TimeGenerated, 5m)
| where ErrorCount > threshold
| order by TimeGenerated desc
"@

New-AzLogAlertWithActionGroup `
-ResourceGroupName 'monitoring-rg' `
-AlertName 'http-5xx-spike-detection' `
-Query $errorQuery `
-WorkspaceId '/subscriptions/xxxx/resourceGroups/monitoring-rg/providers/Microsoft.OperationalInsights/workspaces/prod-la' `
-ActionGroupId $actionGroup.Id `
-ThresholdValue 0 `
-Operator 'GreaterThan' `
-TimeRange 'PT1H' `
-EvaluationFrequency 'PT15M' `
-Severity '1'

Write-Host "日志告警规则已创建,动作组 ID: $($actionGroup.Id)"

执行结果示例:

1
2
3
4
5
6
7
8
9
Name                 : http-5xx-spike-detection
Enabled : True
Severity : 1
Query : let threshold = 50;AppRequests| where ...
EvaluationFrequency : PT15M
WindowSize : PT1H
ActionGroup : /subscriptions/xxxx/resourceGroups/monitoring-rg/providers/microsoft.insights/actionGroups/ops-oncall-action-group

日志告警规则已创建,动作组 ID: /subscriptions/xxxx/resourceGroups/monitoring-rg/providers/microsoft.insights/actionGroups/ops-oncall-action-group

日志告警与动作组的组合使得告警触发后能同时发送邮件通知运维团队,并通过 Webhook 自动触发事件管理系统创建工单。Severity = '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
function Get-AzAlertHealthReport {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$ResourceGroupName,

[Parameter()]
[datetime]$Since = (Get-Date).AddDays(-7)
)

# 查询近期的告警历史
$alerts = Get-AzAlert -ResourceGroupName $ResourceGroupName `
-TimeRange "Custom" `
-StartTime $Since `
-EndTime (Get-Date)

$summary = $alerts | Group-Object Severity | ForEach-Object {
[PSCustomObject]@{
Severity = switch ($_.Name) {
'Sev0' { 'Critical'; break }
'Sev1' { 'Error'; break }
'Sev2' { 'Warning'; break }
'Sev3' { 'Informational'; break }
default { $_.Name }
}
Count = $_.Count
}
}

# 获取所有告警规则并检查合规性
$rules = Get-AzMetricAlertRuleV2 -ResourceGroupName $ResourceGroupName
$compliantRules = $rules | Where-Object {
$_.Criteria.AllOf.Count -gt 0 -and
$_.WindowSize -match '^PT[0-9]+M$' -and
$_.Actions.Count -gt 0
}

[PSCustomObject]@{
Period = "$($Since.ToString('yyyy-MM-dd')) ~ $((Get-Date).ToString('yyyy-MM-dd'))"
TotalAlerts = $alerts.Count
SeverityBreakdown = $summary
TotalRules = $rules.Count
CompliantRules = $compliantRules.Count
ComplianceRate = if ($rules.Count -gt 0) {
[math]::Round($compliantRules.Count / $rules.Count * 100, 1)
} else { 0 }
}
}

# 批量静默告警(用于计划维护窗口)
function Disable-AzAlertRulesForMaintenance {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[string[]]$ResourceGroupNames,

[Parameter()]
[string]$Reason = '计划维护窗口'
)

$disabledRules = @()

foreach ($rg in $ResourceGroupNames) {
$rules = Get-AzMetricAlertRuleV2 -ResourceGroupName $rg
foreach ($rule in $rules) {
if ($PSCmdlet.ShouldProcess($rule.Name, "禁用告警规则($Reason)")) {
Update-AzMetricAlertRuleV2 `
-ResourceGroupName $rg `
-Name $rule.Name `
-Enabled $false
$disabledRules += $rule.Name
}
}
}

Write-Host "已禁用 $($disabledRules.Count) 条告警规则。原因:$Reason"
Write-Host "维护完成后请运行 Enable-AzAlertRulesForMaintenance 重新启用。"

return $disabledRules
}

# 生成健康报告
$report = Get-AzAlertHealthReport -ResourceGroupName 'prod-rg'
$report | Format-List

Write-Host "`n--- 告警严重级别分布 ---"
$report.SeverityBreakdown | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Period            : 2025-12-31 ~ 2026-01-07
TotalAlerts : 47
SeverityBreakdown : {@{Severity=Critical; Count=3}, @{Severity=Error; Count=12}, @{Severity=Warning; Count=28}, @{Severity=Informational; Count=4}}
TotalRules : 24
CompliantRules : 21
ComplianceRate : 87.5

--- 告警严重级别分布 ---
Severity Count
-------- -----
Critical 3
Error 12
Warning 28
Informational 4

健康报告展示了过去一周的告警概况,包括各级别的告警数量和告警规则的合规率。合规检查验证了每条规则是否配置了有效的检测条件、合理的时间窗口和关联的动作组。合规率低于 100% 的规则需要人工审查并补充缺失的配置项。

注意事项

  1. Az 模块版本要求:本文使用的 Az.MonitorAz.ScheduledQueryRule 模块需要 4.0 以上版本。建议运行 Update-Module Az.Monitor 确保使用最新版本,旧版本的部分参数名和 API 行为存在差异。

  2. 权限配置:执行告警规则管理操作需要订阅级别的 Monitoring Contributor 角色权限。如果仅需要查询告警历史,Monitoring Reader 角色即可满足。生产环境中应遵循最小权限原则分配角色。

  3. 告警阈值调优:初次部署告警规则后,建议先设置较宽的阈值并配合低严重级别运行一周,收集实际触发数据后再逐步收紧阈值。过于敏感的阈值会导致告警疲劳,降低团队对真实问题的响应速度。

  4. 动作组测试:创建动作组后务必使用 Test-AzActionGroup 或手动触发测试告警,验证邮件和 Webhook 通知链路是否畅通。很多告警沉默事故的根因都是动作组配置错误导致通知未能送达。

  5. 维护窗口静默:批量禁用告警规则时,建议将禁用操作记录到变更管理系统中,并设置自动提醒在维护结束后重新启用。也可以利用告警处理规则(Alert Processing Rule)的静默功能替代直接禁用,这样不需要修改原始规则配置。

  6. 日志查询性能:Log Analytics 查询的执行成本与扫描的数据量成正比。在高频评估(如每 5 分钟一次)的场景下,查询中应尽量添加时间过滤条件和索引列过滤,避免全表扫描导致额外的计费开销和延迟。

PowerShell 技能连载 - Azure Monitor 告警

适用于 PowerShell 5.1 及以上版本

Azure Monitor 是微软 Azure 平台的核心可观测性服务,提供指标采集、日志分析、告警通知等一站式监控能力。在云原生架构日益复杂的今天,运维团队往往需要管理数十甚至上百个 Azure 资源的监控策略。手动在 Azure 门户中逐一配置告警规则既耗时又容易遗漏,而通过 PowerShell 自动化管理告警,可以实现告警策略的标准化、版本化和批量部署。

本文将介绍如何使用 PowerShell 和 Az 模块操作 Azure Monitor 告警,包括查看现有告警规则、创建指标告警、配置操作组(Action Group)实现通知推送,以及批量管理告警规则。这些方法适用于日常运维自动化,也能与基础设施即代码(IaC)流程相结合,确保监控策略随应用部署同步更新。

在开始之前,请确保已安装 Az PowerShell 模块并完成 Azure 身份认证。所有示例基于 Azure 资源管理器(ARM)REST API,需要拥有目标资源组或订阅级别的 Monitoring Contributor 权限。

连接 Azure 并获取监控资源

第一步是连接 Azure 账户并获取目标资源的信息。告警规则需要绑定到具体的 Azure 资源(如虚拟机、App Service、SQL 数据库等),因此我们先确认订阅上下文并查询需要监控的资源列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 安装 Az 监控相关模块
Install-Module -Name Az.Accounts, Az.Monitor, Az.Resources -Force -Scope CurrentUser

# 连接 Azure 账户
Connect-AzAccount

# 获取当前订阅信息
$context = Get-AzContext
$subscriptionId = $context.Subscription.Id
Write-Host ("当前订阅: {0} ({1})" -f $context.Subscription.Name, $subscriptionId)

# 查询目标资源组中的虚拟机
$resourceGroupName = "rg-production"
$vms = Get-AzVM -ResourceGroupName $resourceGroupName

Write-Host "`n===== 资源组中的虚拟机 =====" -ForegroundColor Cyan
foreach ($vm in $vms) {
$status = (Get-AzVM -ResourceGroupName $resourceGroupName -Name $vm.Name -Status).Statuses[1].DisplayStatus
Write-Host (" {0,-30} {1}" -f $vm.Name, $status)
}

# 选取第一台虚拟机的资源 ID 作为后续示例
$targetResourceId = $vms[0].Id
Write-Host ("`n目标资源 ID: {0}" -f $targetResourceId)

执行结果示例:

1
2
3
4
5
6
7
8
9
当前订阅: Production-Subscription (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)

===== 资源组中的虚拟机 =====
vm-web-01 VM running
vm-web-02 VM running
vm-db-01 VM running
vm-api-01 VM deallocated

目标资源 ID: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-production/providers/Microsoft.Compute/virtualMachines/vm-web-01

连接成功后,我们得到了目标资源的完整 ID。后续创建告警规则时,需要将这个资源 ID 作为监控的目标范围(Scope)。如果需要监控整个资源组或订阅级别的指标,可以使用资源组 ID 或订阅 ID 作为范围。

创建指标告警规则

Azure Monitor 的指标告警(Metric Alert)是最常用的告警类型,它基于资源发出的性能指标数据进行评估。以下代码演示如何为虚拟机创建 CPU 使用率告警,当 CPU 平均使用率连续 5 分钟超过 85% 时触发告警。

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
# 定义告警规则参数
$alertRuleName = "cpu-high-vm-web-01"
$alertDescription = "虚拟机 CPU 使用率连续 5 分钟超过 85%,可能影响应用性能"
$windowSize = "PT5M"
$evaluationFrequency = "PT1M"
$threshold = 85
$operator = "GreaterThan"
$aggregation = "Average"
$severity = 2

# 获取目标虚拟机的资源 ID
$targetVm = Get-AzVM -ResourceGroupName $resourceGroupName -Name "vm-web-01"

# 创建告警条件
$condition = New-AzMetricAlertRuleV2Criteria `
-MetricName "Percentage CPU" `
-TimeAggregation $aggregation `
-Operator $operator `
-Threshold $threshold

# 创建告警规则
$alertRule = New-AzMetricAlertRuleV2 `
-Name $alertRuleName `
-ResourceGroupName $resourceGroupName `
-WindowSize $windowSize `
-Frequency $evaluationFrequency `
-TargetResourceId $targetVm.Id `
-Condition $condition `
-Severity $severity `
-Description $alertDescription `
-Enabled

if ($alertRule) {
Write-Host "指标告警规则创建成功!" -ForegroundColor Green
Write-Host (" 规则名称: {0}" -f $alertRule.Name)
Write-Host (" 目标资源: {0}" -f $targetVm.Name)
Write-Host (" 监控指标: Percentage CPU")
Write-Host (" 聚合方式: {0}" -f $aggregation)
Write-Host (" 阈值: {0} {1}" -f $operator, $threshold)
Write-Host (" 检测窗口: {0}" -f $windowSize)
Write-Host (" 评估频率: {0}" -f $evaluationFrequency)
Write-Host (" 严重级别: Sev{0}" -f $severity)
}

执行结果示例:

1
2
3
4
5
6
7
8
9
指标告警规则创建成功!
规则名称: cpu-high-vm-web-01
目标资源: vm-web-01
监控指标: Percentage CPU
聚合方式: Average
阈值: GreaterThan 85
检测窗口: PT5M
评估频率: PT1M
严重级别: Sev2

指标告警规则的几个关键参数需要根据实际场景调优。WindowSize(检测窗口)决定了评估指标的时间跨度,Frequency(评估频率)决定了多久检查一次条件。窗口越大越不容易产生误报,但响应时间也会变长。对于关键业务系统,建议采用较短的窗口(如 PT1M 到 PT5M),配合适度的阈值,在灵敏度和稳定性之间取得平衡。

配置操作组实现告警通知

告警规则触发后,需要有渠道将通知送达运维人员。Azure Monitor 的操作组(Action Group)定义了告警触发时的响应动作,包括发送邮件、短信、Webhook、调用 Azure Function 等。以下代码展示如何创建一个操作组,包含邮件通知和 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
# 创建邮件接收人
$emailReceiver = New-AzActionGroupReceiver `
-Name "ops-team-email" `
-EmailAddress "ops-team@example.com"

# 创建 Webhook 接收(可对接企业 IM、事件管理平台)
$webhookReceiver = New-AzActionGroupReceiver `
-Name "incident-webhook" `
-WebhookUri "https://hooks.example.com/azure-alerts/incident"

# 创建操作组
$actionGroupName = "ag-production-critical"
$actionGroupShortName = "prod-crit"

$actionGroup = Set-AzActionGroup `
-Name $actionGroupName `
-ResourceGroupName $resourceGroupName `
-ShortName $actionGroupShortName `
-Receiver $emailReceiver, $webhookReceiver

if ($actionGroup) {
Write-Host "操作组创建成功!" -ForegroundColor Green
Write-Host (" 操作组名称: {0}" -f $actionGroup.Name)
Write-Host (" 短名称: {0}" -f $actionGroup.ShortName)
Write-Host (" 接收人数量: {0}" -f $actionGroup.Receivers.Count)
Write-Host "`n 接收人详情:" -ForegroundColor Cyan
foreach ($receiver in $actionGroup.Receivers) {
Write-Host (" - {0}: {1}" -f $receiver.Name, $receiver.EmailAddress)
}
}

# 将操作组关联到告警规则
$actionGroupId = (Get-AzActionGroup -ResourceGroupName $resourceGroupName -Name $actionGroupName).Id

# 创建动作引用
$action = New-AzAlertRuleAction -ActionGroupId $actionGroupId

Write-Host "`n操作组已就绪,可关联到告警规则。" -ForegroundColor Green
Write-Host (" Action Group ID: {0}" -f $actionGroupId)

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
操作组创建成功!
操作组名称: ag-production-critical
短名称: prod-crit
接收人数量: 2

接收人详情:
- ops-team-email: ops-team@example.com
- incident-webhook:

操作组已就绪,可关联到告警规则。
Action Group ID: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-production/providers/microsoft.insights/actionGroups/ag-production-critical

操作组设计为独立于告警规则的资源,这意味着一个操作组可以被多条告警规则复用。建议按照团队或响应级别来组织操作组,例如”生产环境-紧急”、”生产环境-警告”、”测试环境-通知”等。当团队成员变动时,只需修改操作组即可,无需逐条更新告警规则。

批量查询和管理告警规则

在大型 Azure 环境中,告警规则可能多达数十甚至上百条。手动逐条检查既不现实也容易遗漏。以下代码展示如何批量查询告警规则的状态,并生成一份告警规则清单报告。

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
# 查询资源组中所有指标告警规则
$alertRules = Get-AzMetricAlertRuleV2 -ResourceGroupName $resourceGroupName

Write-Host "===== 告警规则清单 =====" -ForegroundColor Cyan
Write-Host ("资源组: {0}" -f $resourceGroupName)
Write-Host ("规则总数: {0}" -f $alertRules.Count)
Write-Host ""

# 构建告警规则报告
$reportData = foreach ($rule in $alertRules) {
$targetResources = @()
foreach ($scope in $rule.Scopes) {
$parts = $scope -split "/"
$resourceName = $parts[-1]
$targetResources += $resourceName
}

$criteria = $rule.Criteria
$metricName = if ($criteria.AllOf) { $criteria.AllOf[0].MetricName } else { "N/A" }
$thresholdValue = if ($criteria.AllOf) { $criteria.AllOf[0].Threshold } else { "N/A" }
$operatorValue = if ($criteria.AllOf) { $criteria.AllOf[0].OperatorProperty } else { "N/A" }

[PSCustomObject]@{
规则名称 = $rule.Name
严重级别 = "Sev{0}" -f $rule.Severity
启用状态 = if ($rule.Enabled) { "已启用" } else { "已禁用" }
监控指标 = $metricName
阈值条件 = "{0} {1}" -f $operatorValue, $thresholdValue
检测窗口 = $rule.WindowSize
目标资源 = ($targetResources -join ", ")
操作组 = if ($rule.Actions.Count -gt 0) { "已配置 ({0} 个)" -f $rule.Actions.Count } else { "未配置" }
}
}

# 按严重级别排序并输出
$sortedReport = $reportData | Sort-Object 严重级别, 规则名称

foreach ($item in $sortedReport) {
Write-Host "---" -ForegroundColor Gray
Write-Host (" 规则名称: {0}" -f $item.规则名称)
Write-Host (" 严重级别: {0}" -f $item.严重级别)
Write-Host (" 启用状态: {0}" -f $item.启用状态)
Write-Host (" 监控指标: {0}" -f $item.监控指标)
Write-Host (" 阈值条件: {0}" -f $item.阈值条件)
Write-Host (" 检测窗口: {0}" -f $item.检测窗口)
Write-Host (" 目标资源: {0}" -f $item.目标资源)
Write-Host (" 操作组: {0}" -f $item.操作组)
}

# 导出 CSV 报告
$reportPath = Join-Path $env:TEMP "AlertRules-Report-{0:yyyyMMdd}.csv" -f (Get-Date)
$sortedReport | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8
Write-Host ("`n报告已导出: {0}" -f $reportPath) -ForegroundColor Green

# 检查未配置操作组的规则
$noActionRules = $reportData | Where-Object { $_.操作组 -eq "未配置" }
if ($noActionRules) {
Write-Host "`n[警告] 以下告警规则未配置操作组,触发后不会发送通知:" -ForegroundColor Yellow
foreach ($rule in $noActionRules) {
Write-Host (" - {0}" -f $rule.规则名称) -ForegroundColor Yellow
}
}

执行结果示例:

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
===== 告警规则清单 =====
资源组: rg-production
规则总数: 8

---
规则名称: cpu-high-vm-api-01
严重级别: Sev2
启用状态: 已启用
监控指标: Percentage CPU
阈值条件: GreaterThan 90
检测窗口: 00:05:00
目标资源: vm-api-01
操作组: 已配置 (1 个)
---
规则名称: cpu-high-vm-web-01
严重级别: Sev2
启用状态: 已启用
监控指标: Percentage CPU
阈值条件: GreaterThan 85
检测窗口: 00:05:00
目标资源: vm-web-01
操作组: 已配置 (2 个)
---
规则名称: disk-space-vm-db-01
严重级别: Sev3
启用状态: 已启用
监控指标: OsDisk.Used
阈值条件: GreaterThan 80
检测窗口: 00:10:00
目标资源: vm-db-01
操作组: 未配置

报告已导出: /tmp/AlertRules-Report-20251106.csv

[警告] 以下告警规则未配置操作组,触发后不会发送通知:
- disk-space-vm-db-01

批量审查告警规则是运维巡检的重要环节。脚本中特别加入了”未配置操作组”的检测逻辑——一条没有操作组的告警规则即使被触发,也不会通知任何人,形同虚设。建议将此脚本纳入定期巡检流程,确保所有告警规则都处于有效工作状态。

使用日志查询告警

除了指标告警,Azure Monitor 还支持基于 Log Analytics 日志查询的告警。日志告警使用 KQL(Kusto Query Language)编写查询条件,能够对结构化日志数据进行复杂的关联分析。以下示例展示如何通过 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
41
42
43
44
45
46
47
48
# 定义日志告警参数
$logAlertName = "anomalous-signin-detection"
$logAlertDescription = "检测 1 小时内同一账户从不同地区登录的行为,可能表示凭据泄露"
$workspaceResourceId = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.OperationalInsights/workspaces/law-production"

# 构建 KQL 查询
$kqlQuery = @"
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType == 0
| summarize LoginCount = count(), DistinctLocations = dcount(Location),
Locations = make_set(Location, 10) by UserPrincipalName
| where DistinctLocations > 2
| project TimeGenerated = now(), UserPrincipalName, LoginCount, DistinctLocations, Locations
| order by DistinctLocations desc
"@

# 创建日志查询告警的条件
$schedule = New-AzScheduledQueryRuleScheduleObject `
-FrequencyInMinutes 15 `
-TimeWindowInMinutes 60

$conditionObject = New-AzScheduledQueryRuleConditionObject `
-Query $kqlQuery `
-TimeWindow (New-TimeSpan -Minutes 60)

# 创建日志告警规则
$logAlert = New-AzScheduledQueryRule `
-Name $logAlertName `
-ResourceGroupName $resourceGroupName `
-Location "eastus" `
-DisplayName "异常登录行为检测" `
-Description $logAlertDescription `
-Severity 2 `
-Enabled `
-Schedule $schedule `
-Condition $conditionObject `
-Scope $workspaceResourceId

if ($logAlert) {
Write-Host "日志查询告警规则创建成功!" -ForegroundColor Green
Write-Host (" 规则名称: {0}" -f $logAlert.Name)
Write-Host (" 显示名称: {0}" -f $logAlert.DisplayName)
Write-Host (" 严重级别: Sev{0}" -f $logAlert.Severity)
Write-Host (" 查询频率: 每 {0} 分钟" -f $schedule.FrequencyInMinutes)
Write-Host (" 查询窗口: {0} 分钟" -f $schedule.TimeWindowInMinutes)
Write-Host (" 目标工作区: {0}" -f ($workspaceResourceId -split "/")[-1])
}

执行结果示例:

1
2
3
4
5
6
7
日志查询告警规则创建成功!
规则名称: anomalous-signin-detection
显示名称: 异常登录行为检测
严重级别: Sev2
查询频率: 每 15 分钟
查询窗口: 60 分钟
目标工作区: law-production

日志告警的灵活性远高于指标告警,但代价是更高的 Log Analytics 查询成本。编写 KQL 查询时,务必使用 where 子句尽早过滤数据,减少扫描的数据量。TimeWindow 参数决定查询回溯的时间范围,Frequency 参数决定查询的执行间隔,两者的设置需要平衡检测时效性和运行成本。

注意事项

  1. 权限配置:操作 Azure Monitor 告警需要 Microsoft.Insights/metricAlerts/*Microsoft.Insights/actionGroups/* 等权限。建议创建 Azure 自定义角色,仅授予 Monitoring Contributor 级别的权限,避免使用 Owner 或 Contributor 等过宽的角色。在多团队协作环境中,通过角色划分明确告警管理的责任边界。

  2. 告警疲劳治理:阈值设置不当会导致大量无效告警(Alert Fatigue),使运维人员逐渐忽视告警通知。建议先以较宽松的阈值试运行一周,观察触发频率后再逐步收紧。对于非关键指标,可以设置较高的严重级别(Sev3 或 Sev4),减少高频告警的干扰。

  3. API 版本管理Az.Monitor 模块中的 cmdlet 对应不同版本的 ARM API,行为可能随模块更新而变化。生产脚本中应固定 Az 模块版本(如 RequiredVersion),并在升级前在测试环境中验证兼容性。同时关注 Azure 更新公告,了解 Breaking Change。

  4. 操作组测试:创建操作组后,务必使用 Azure 门户的”测试操作组”功能或手动触发一条测试告警,确认邮件和 Webhook 通知能够正常送达。Webhook 端点可能存在防火墙规则或认证配置问题,仅靠创建成功的返回值无法验证端到端的可达性。

  5. 成本控制:日志告警基于 Log Analytics 查询,每次执行都会消耗数据扫描量。高频的日志告警(如每分钟执行一次复杂查询)可能产生显著的额外费用。建议在 Azure Cost Management 中设置预算告警,监控 Monitor 服务的月度开支,并对查询频率和复杂度进行成本效益评估。

  6. 告警规则即代码:将告警规则的定义保存在 JSON 或 PowerShell 脚本文件中,通过 Git 进行版本管理。这样不仅能追踪每次阈值调整的历史,还能在灾难恢复场景中快速重建完整的监控体系。结合 Azure DevOps Pipeline 或 GitHub Actions,可以实现告警规则的自动部署和审批流程。

PowerShell 技能连载 - Prometheus 指标采集

适用于 PowerShell 7.0 及以上版本

在云原生可观测性体系中,Prometheus 已经成为指标采集与监控的事实标准。它的数据模型基于时间序列,每条指标由指标名称和一组键值对标签唯一标识。当我们需要在运维自动化脚本中采集系统指标、将业务应用的性能数据推送到 Prometheus Pushgateway、或者从 Prometheus Server 查询历史数据做容量规划时,直接通过 HTTP API 与 Prometheus 交互是最灵活的方式。

PowerShell 7 内置的 Invoke-RestMethod 对 JSON 的原生支持,使其非常适合与 Prometheus 的 RESTful API 和文本暴露格式(text-based exposition format)打交道。无需安装额外的 SDK,只需几行脚本就能完成指标采集、推送和查询。本文将从三个场景出发:采集本地系统指标并写入 Prometheus 格式文件、推送自定义指标到 Pushgateway、以及从 Prometheus Server 执行 PromQL 查询并分析结果。

场景一:采集本地系统指标并输出 Prometheus 格式

Prometheus 的文本暴露格式是一种人类可读的纯文本协议。每条指标以 # TYPE 声明类型,紧随其后的行是具体的指标值。下面的脚本通过 .NET 的 System.Diagnostics.ProcessPerformanceCounter 类采集 CPU、内存和磁盘指标,然后输出符合 Prometheus 标准的文本格式。

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
function Get-SystemPrometheusMetrics {
[CmdletBinding()]
param(
[Parameter()]
[string]$InstanceHostname = $env:COMPUTERNAME
)

# 采集 CPU 使用率(通过 WMI/CIM,跨平台兼容)
$cpuUsage = 0
if ($IsWindows -or $PSEdition -eq 'Desktop') {
$cpu = Get-CimInstance -ClassName Win32_Processor -ErrorAction SilentlyContinue
if ($cpu) {
$cpuUsage = [math]::Round(($cpu | Measure-Object -Property LoadPercentage -Average).Average, 2)
}
} else {
# Linux/macOS:通过 top 命令获取
$topOutput = top -bn1 | Select-String '^%?Cpu'
if ($topOutput -match '(\d+\.?\d*)\s*id') {
$cpuUsage = [math]::Round(100 - [double]$Matches[1], 2)
}
}

# 采集内存使用情况
$osInfo = if ($IsWindows -or $PSEdition -eq 'Desktop') {
$os = Get-CimInstance -ClassName Win32_OperatingSystem
@{
TotalBytes = $os.TotalVisibleMemorySize * 1KB
UsedBytes = ($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) * 1KB
FreeBytes = $os.FreePhysicalMemory * 1KB
UsedPercent = [math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize * 100, 2)
}
} else {
$memInfo = Get-Content /proc/meminfo
$total = [int]($memInfo | Select-String 'MemTotal:\s+(\d+)' | ForEach-Object { $_.Matches[0].Groups[1].Value }) * 1KB
$available = [int]($memInfo | Select-String 'MemAvailable:\s+(\d+)' | ForEach-Object { $_.Matches[0].Groups[1].Value }) * 1KB
@{
TotalBytes = $total
UsedBytes = $total - $available
FreeBytes = $available
UsedPercent = [math]::Round(($total - $available) / $total * 100, 2)
}
}

# 采集磁盘使用情况(根分区 / 系统盘)
$drive = if ($IsWindows -or $PSEdition -eq 'Desktop') {
Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' |
Sort-Object -Property Size -Descending | Select-Object -First 1
} else {
$dfOutput = df / | Select-Object -Last 1
$parts = $dfOutput -split '\s+'
[PSCustomObject]@{
Size = [int64]$parts[1] * 1KB
FreeSpace = [int64]$parts[3] * 1KB
VolumeName = '/'
}
}
$diskTotal = $drive.Size
$diskFree = if ($drive.FreeSpace -is [long]) { $drive.FreeSpace } else { $drive.FreeSpace }
$diskUsedPercent = [math]::Round(($diskTotal - $diskFree) / $diskTotal * 100, 2)

# 获取当前时间戳(Unix 纪元秒)
$timestamp = [int64][double]::Parse(
(Get-Date -UFormat '%s'), [System.Globalization.CultureInfo]::InvariantCulture
)

# 组装 Prometheus 文本格式指标
$labels = "instance=`"$InstanceHostname`""
$lines = @(
'# HELP system_cpu_usage_percent CPU usage percentage'
'# TYPE system_cpu_usage_percent gauge'
"system_cpu_usage_percent{$labels} $cpuUsage $timestamp"
''
'# HELP system_memory_total_bytes Total physical memory in bytes'
'# TYPE system_memory_total_bytes gauge'
"system_memory_total_bytes{$labels} $($osInfo.TotalBytes) $timestamp"
''
'# HELP system_memory_used_bytes Used physical memory in bytes'
'# TYPE system_memory_used_bytes gauge'
"system_memory_used_bytes{$labels} $($osInfo.UsedBytes) $timestamp"
''
'# HELP system_memory_used_percent Memory usage percentage'
'# TYPE system_memory_used_percent gauge'
"system_memory_used_percent{$labels} $($osInfo.UsedPercent) $timestamp"
''
'# HELP system_disk_total_bytes Total disk space in bytes'
'# TYPE system_disk_total_bytes gauge'
"system_disk_total_bytes{$labels} $diskTotal $timestamp"
''
'# HELP system_disk_used_percent Disk usage percentage'
'# TYPE system_disk_used_percent gauge'
"system_disk_used_percent{$labels} $diskUsedPercent $timestamp"
)

return $lines -join "`n"
}

# 采集并输出指标
$metrics = Get-SystemPrometheusMetrics
Write-Output $metrics

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# HELP system_cpu_usage_percent CPU usage percentage
# TYPE system_cpu_usage_percent gauge
system_cpu_usage_percent{instance="WEB-SVR01"} 23.45 1729137600

# HELP system_memory_total_bytes Total physical memory in bytes
# TYPE system_memory_total_bytes gauge
system_memory_total_bytes{instance="WEB-SVR01"} 34359738368 1729137600

# HELP system_memory_used_bytes Used physical memory in bytes
# TYPE system_memory_used_bytes gauge
system_memory_used_bytes{instance="WEB-SVR01"} 20132659200 1729137600

# HELP system_memory_used_percent Memory usage percentage
# TYPE system_memory_used_percent gauge
system_memory_used_percent{instance="WEB-SVR01"} 58.59 1729137600

# HELP system_disk_total_bytes Total disk space in bytes
# TYPE system_disk_total_bytes gauge
system_disk_total_bytes{instance="WEB-SVR01"} 536870912000 1729137600

# HELP system_disk_used_percent Disk usage percentage
# TYPE system_disk_used_percent gauge
system_disk_used_percent{instance="WEB-SVR01"} 72.31 1729137600

场景二:推送自定义指标到 Pushgateway

短生命周期的任务(如批处理脚本、CI/CD 构建流水线)运行时间很短,Prometheus 的默认拉取模式可能来不及采集。Pushgateway 提供了一种推送模式,允许脚本在任务完成时主动将指标推送到中间网关,等待 Prometheus 定期拉取。下面的脚本演示了如何将构建流水线的执行时长和成功率推送到 Pushgateway。

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
function Push-PrometheusMetric {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[uri]$PushgatewayUrl,

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

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

[Parameter(Mandatory)]
[double]$MetricValue,

[Parameter()]
[string]$MetricType = 'gauge',

[Parameter()]
[string]$HelpText = $MetricName,

[Parameter()]
[hashtable]$Labels
)

# 构建 Pushgateway 的推送 URL
# 路径格式:/metrics/job/<job_name>/label_key/label_value
$pushPath = "/metrics/job/$JobName"
foreach ($key in $Labels.Keys) {
$encodedKey = [uri]::EscapeDataString($key)
$encodedValue = [uri]::EscapeDataString($Labels[$key])
$pushPath += "/$encodedKey/$encodedValue"
}

$fullUrl = New-Object System.Uri -ArgumentList $PushgatewayUrl, $pushPath

# 获取 Unix 时间戳
$timestamp = [int64][double]::Parse(
(Get-Date -UFormat '%s'), [System.Globalization.CultureInfo]::InvariantCulture
)

# 构建标签字符串(不包含 job,job 已在 URL 路径中)
$labelPairs = foreach ($key in $Labels.Keys) {
"$key=`"$($Labels[$key])`""
}
$labelStr = $labelPairs -join ','

# 组装 Prometheus 文本格式
$body = @(
"# HELP $MetricName $HelpText"
"# TYPE $MetricName $MetricType"
"${MetricName}{$labelStr} $MetricValue $timestamp"
) -join "`n"

# 发送推送请求
try {
$response = Invoke-RestMethod -Method Post -Uri $fullUrl -Body $body `
-ContentType 'text/plain; version=1.0.4; charset=utf-8' `
-ErrorAction Stop

Write-Verbose "指标推送成功: $MetricName = $MetricValue -> $fullUrl"
return $true
}
catch {
Write-Error "指标推送失败: $($_.Exception.Message)"
return $false
}
}

# 示例:推送 CI/CD 构建指标
$pushgateway = 'http://prometheus-pushgateway.monitoring.svc.cluster.local:9091'
$buildLabels = @{
branch = 'main'
pipeline = 'deploy-production'
stage = 'build'
runner = 'ps-runner-01'
}

# 推送构建时长(秒)
$buildDuration = Get-Random -Minimum 120 -Maximum 480
Push-PrometheusMetric -PushgatewayUrl $pushgateway `
-JobName 'ci_build_pipeline' `
-MetricName 'ci_build_duration_seconds' `
-MetricValue $buildDuration `
-MetricType 'gauge' `
-HelpText 'Duration of CI build in seconds' `
-Labels $buildLabels

# 推送构建结果(1=成功, 0=失败)
Push-PrometheusMetric -PushgatewayUrl $pushgateway `
-JobName 'ci_build_pipeline' `
-MetricName 'ci_build_success' `
-MetricValue 1 `
-MetricType 'gauge' `
-HelpText 'Whether the CI build succeeded (1=yes, 0=no)' `
-Labels $buildLabels

Write-Host "构建指标已推送到 Pushgateway"

执行结果示例:

1
构建指标已推送到 Pushgateway

可以通过以下命令验证 Pushgateway 中存储的指标:

1
2
3
# 查询 Pushgateway 中所有指标组
$groups = Invoke-RestMethod -Uri "$pushgateway/api/v1/metrics"
$groups.data | ConvertTo-Json -Depth 5 | Select-Object -First 30

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"type": "gauge",
"help": "Duration of CI build in seconds",
"metrics": [
{
"labels": {
"branch": "main",
"instance": "",
"job": "ci_build_pipeline",
"pipeline": "deploy-production",
"runner": "ps-runner-01",
"stage": "build"
},
"value": "347"
}
]
}

场景三:查询 Prometheus Server 并分析指标数据

Prometheus 提供了丰富的 HTTP Query API,支持即时查询(instant query)和范围查询(range query)。下面的脚本封装了两个查询函数,分别用于获取某一时刻的指标快照和一段时间内的时序数据,并将结果转换为 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
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
function Invoke-PrometheusQuery {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[uri]$PrometheusUrl,

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

[Parameter()]
[datetime]$Time = (Get-Date)
)

# 即时查询:获取指定时刻的指标值
$timestamp = [int64][double]::Parse(
$Time.ToUniversalTime().Subtract([datetime]::new(1970, 1, 1)).TotalSeconds,
[System.Globalization.CultureInfo]::InvariantCulture
)

$queryParams = @{
query = $Query
time = $timestamp
}

$response = Invoke-RestMethod -Method Get `
-Uri "$PrometheusUrl/api/v1/query" `
-Body $queryParams

if ($response.status -ne 'success') {
throw "Prometheus 查询失败: $($response.errorType) - $($response.error)"
}

# 将查询结果转换为 PowerShell 对象
$results = foreach ($result in $response.data.result) {
$labels = $result.metric
$value = $result.value

[PSCustomObject]@{
MetricName = $labels['__name__']
Labels = $labels
Timestamp = [datetimeoffset]::FromUnixTimeSeconds([int64]$value[0]).DateTime
Value = [double]$value[1]
}
}

return $results
}

function Invoke-PrometheusRangeQuery {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[uri]$PrometheusUrl,

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

[Parameter(Mandatory)]
[datetime]$StartTime,

[Parameter(Mandatory)]
[datetime]$EndTime,

[Parameter()]
[string]$Step = '5m'
)

# 范围查询:获取时间区间内的指标时序
$startTs = [int64][double]::Parse(
$StartTime.ToUniversalTime().Subtract([datetime]::new(1970, 1, 1)).TotalSeconds,
[System.Globalization.CultureInfo]::InvariantCulture
)
$endTs = [int64][double]::Parse(
$EndTime.ToUniversalTime().Subtract([datetime]::new(1970, 1, 1)).TotalSeconds,
[System.Globalization.CultureInfo]::InvariantCulture
)

$queryParams = @{
query = $Query
start = $startTs
end = $endTs
step = $Step
}

$response = Invoke-RestMethod -Method Get `
-Uri "$PrometheusUrl/api/v1/query_range" `
-Body $queryParams

if ($response.status -ne 'success') {
throw "Prometheus 范围查询失败: $($response.errorType) - $($response.error)"
}

# 将时序数据转换为扁平化的 PowerShell 对象列表
$results = foreach ($series in $response.data.result) {
$labels = $series.metric
$values = $series.values

foreach ($pair in $values) {
[PSCustomObject]@{
MetricName = $labels['__name__']
Instance = $labels['instance']
Job = $labels['job']
Timestamp = [datetimeoffset]::FromUnixTimeSeconds([int64]$pair[0]).DateTime
Value = [double]$pair[1]
}
}
}

return $results
}

# 即时查询:获取所有实例的 CPU 使用率
$prometheus = 'http://prometheus.monitoring.svc.cluster.local:9090'
$cpuData = Invoke-PrometheusQuery -PrometheusUrl $prometheus `
-Query '100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)'

$cpuData | Select-Object MetricName, Instance, Value, Timestamp |
Sort-Object Value -Descending |
Format-Table -AutoSize

# 范围查询:获取过去 1 小时内存使用率的时序数据
$rangeData = Invoke-PrometheusRangeQuery -PrometheusUrl $prometheus `
-Query '(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100' `
-StartTime (Get-Date).AddHours(-1) `
-EndTime (Get-Date) `
-Step '5m'

# 计算统计摘要
$rangeData | Group-Object Instance | ForEach-Object {
$values = $_.Group.Value
[PSCustomObject]@{
Instance = $_.Name
Min = [math]::Round(($values | Measure-Object -Minimum).Minimum, 2)
Max = [math]::Round(($values | Measure-Object -Maximum).Maximum, 2)
Avg = [math]::Round(($values | Measure-Object -Average).Average, 2)
Samples = $values.Count
}
} | Sort-Object Avg -Descending | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
MetricName                        Instance            Value Timestamp
---------- -------- ----- ---------
node_cpu_seconds_total web-svr01:9100 78.34 10/17/2025 8:00:00 AM
node_cpu_seconds_total db-svr01:9100 45.21 10/17/2025 8:00:00 AM
node_cpu_seconds_total api-svr01:9100 32.67 10/17/2025 8:00:00 AM
node_cpu_seconds_total monitor-svr01:9100 12.45 10/17/2025 8:00:00 AM

Instance Min Max Avg Samples
--------- --- --- --- -------
web-svr01:9100 55.32 82.17 68.45 12
db-svr01:9100 40.11 62.89 51.33 12
api-svr01:9100 28.45 48.22 37.61 12
monitor-svr01:9100 8.12 22.34 15.88 12

注意事项

  1. 指标命名规范:Prometheus 指标名称应遵循 namespace_subsystem_name_unit 的命名约定。例如 node_memory_MemAvailable_bytes 分别代表命名空间(node)、子系统(memory)、度量项(MemAvailable)和单位(bytes)。使用一致的命名规范能让 PromQL 查询更简洁,也便于 Grafana 面板复用。

  2. 时间戳精度与同步:推送指标时附带的时间戳必须是 Unix 纪元秒数(float64)。确保运行 PowerShell 的主机时间已通过 NTP 同步,否则 Prometheus 可能因时间偏移而拒绝数据。在推送模式下可以省略时间戳,让 Pushgateway 自动使用接收时间。

  3. Pushgateway 数据清理:Pushgateway 不会自动清除已推送的指标,即使对应的任务已经停止运行。这会导致 Prometheus 持续采集到过期的静态数据。建议在任务结束后调用 Pushgateway 的 DELETE API 清理指标组,或在推送时设置合理的标签(如 instance)以便批量清理。

  4. PromQL 注入风险:如果 PromQL 查询字符串包含用户输入(如主机名、应用名称),必须进行转义和校验,防止注入攻击。PromQL 本身不支持 SQL 式的注入,但恶意的标签值可能导致查询结果被篡改或返回大量数据耗尽 Prometheus Server 内存。

  5. 大范围查询的性能:范围查询(query_range)的 step 参数直接影响返回的数据点数量。公式为 (end - start) / step。查询 7 天的数据、step 设为 15 秒将返回约 4 万个数据点,可能使 PowerShell 的对象处理变慢。建议根据查询时长合理设置 step:1 小时用 1m,1 天用 5m,7 天用 15m

  6. 认证与网络安全:生产环境的 Prometheus 通常部署在内部网络,可能需要 mTLS 或 Bearer Token 认证。使用 Invoke-RestMethod 时通过 -Headers @{Authorization = 'Bearer <token>'} 传递令牌,通过 -SkipCertificateCheck 处理自签证书(仅限内部测试环境)。建议将凭据存储在 PowerShell SecretManagement 模块中,不要硬编码在脚本里。