Azure · lesson 9 of 10
Also part of: Multi-Cloud
Azure Monitor Basics
Read a VM's CPU metrics, query its logs with Kusto (KQL) in Log Analytics, and wire both into an alert rule that fires on a real threshold breach.
By SkillByExample · Updated
Azure Monitor covers two different kinds of data that get confused for each other: metrics (automatically collected numeric time series, cheap and fast to query) and logs in a Log Analytics workspace (richer event and text data you provision and query with KQL). Alerts can be built on either.
Reading platform metrics with no setup required
Assuming learn-vm from the virtual-machines lesson still exists:
az monitor metrics list \
--resource "/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/resourceGroups/learn-rg/providers/Microsoft.Compute/virtualMachines/learn-vm" \
--metric "Percentage CPU" \
--interval PT1M \
--output table
$ az monitor metrics list --resource ".../virtualMachines/learn-vm" --metric "Percentage CPU" --interval PT1M --output table
Timestamp Name Average
-------------------- ---------------- ---------
2026-09-12T16:00:00Z Percentage CPU 2.1
2026-09-12T16:01:00Z Percentage CPU 2.4
2026-09-12T16:02:00Z Percentage CPU 97.8
2026-09-12T16:03:00Z Percentage CPU 95.3
2026-09-12T16:04:00Z Percentage CPU 3.0
No agent, no workspace — Percentage CPU is collected by the platform for every VM
automatically. The spike at 16:02-16:03 is what the alert rule below is built to catch.
Provisioning a Log Analytics workspace and querying it
az monitor log-analytics workspace create \
--resource-group learn-rg \
--workspace-name learn-logs
$ az monitor log-analytics workspace create --resource-group learn-rg --workspace-name learn-logs
{
"customerId": "4e8a1f2c-9d3b-4a17-8c5f-2d6a0b9e7f52",
"name": "learn-logs",
"provisioningState": "Succeeded",
"sku": { "name": "PerGB2018" }
}
Once the VM is connected via Azure Monitor Agent (az vm extension set with the
AzureMonitorLinuxAgent extension, and a data collection rule pointing at this workspace),
its performance counters and syslog land in the Perf and Syslog tables. Query the same
CPU spike, now via KQL instead of the metrics API:
Perf
| where ObjectName == "Processor" and CounterName == "% Processor Time"
| where TimeGenerated > ago(10m)
| summarize AvgCPU = avg(CounterValue) by bin(TimeGenerated, 1m)
| order by TimeGenerated desc
$ az monitor log-analytics query --workspace 4e8a1f2c-9d3b-4a17-8c5f-2d6a0b9e7f52 --analytics-query "Perf | where ObjectName == 'Processor' and CounterName == '% Processor Time' | where TimeGenerated > ago(10m) | summarize AvgCPU = avg(CounterValue) by bin(TimeGenerated, 1m) | order by TimeGenerated desc" --output table
TimeGenerated AvgCPU
-------------------- --------
2026-09-12T16:04:00Z 3.1
2026-09-12T16:03:00Z 95.1
2026-09-12T16:02:00Z 97.6
2026-09-12T16:01:00Z 2.3
summarize ... by bin(TimeGenerated, 1m) buckets rows into one-minute windows before
averaging — the KQL equivalent of GROUP BY over a truncated timestamp, and the standard
shape for turning raw per-sample rows into a time series.
An alert rule on the metric
az monitor metrics alert create \
--resource-group learn-rg \
--name high-cpu-alert \
--scopes "/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/resourceGroups/learn-rg/providers/Microsoft.Compute/virtualMachines/learn-vm" \
--condition "avg Percentage CPU > 90" \
--window-size 5m \
--evaluation-frequency 1m \
--severity 2
$ az monitor metrics alert create --resource-group learn-rg --name high-cpu-alert --scopes ".../virtualMachines/learn-vm" --condition "avg Percentage CPU > 90" --window-size 5m --evaluation-frequency 1m --severity 2
{
"name": "high-cpu-alert",
"severity": 2,
"enabled": true,
"windowSize": "PT5M",
"evaluationFrequency": "PT1M",
"criteria": {
"allOf": [
{ "metricName": "Percentage CPU", "operator": "GreaterThan", "threshold": 90.0 }
]
}
}
--window-size 5m means the average has to stay above 90 across a rolling 5-minute window,
not just in a single 1-minute sample — the single-minute spike seen above (97.8 at
16:02) alone wouldn’t fire this rule; it needs sustained load.
az monitor metrics alert show --resource-group learn-rg --name high-cpu-alert --query "{enabled:enabled, status:'-'}"
az monitor activity-log alert list --resource-group learn-rg --output table
$ az monitor metrics alert show --resource-group learn-rg --name high-cpu-alert --query "enabled"
true
Practice
1. Query the last hour of CPU metrics as JSON instead of a table, for scripting.
az monitor metrics list --resource ".../virtualMachines/learn-vm" --metric "Percentage CPU" --interval PT5M --start-time 2026-09-12T15:00:00Z --output json --query "value[0].timeseries[0].data"
$ az monitor metrics list ... --output json --query "value[0].timeseries[0].data"
[
{ "timeStamp": "2026-09-12T15:55:00Z", "average": 2.8 },
{ "timeStamp": "2026-09-12T16:00:00Z", "average": 2.1 },
{ "timeStamp": "2026-09-12T16:05:00Z", "average": 45.2 }
]
--output json plus a --query slice is the pattern for feeding metric data into a script
or another tool, rather than reading it as a table meant for a human.
2. Write a KQL query that counts events per hour instead of averaging a metric.
Syslog
| where TimeGenerated > ago(24h)
| summarize EventCount = count() by bin(TimeGenerated, 1h), SeverityLevel
| order by TimeGenerated desc
TimeGenerated SeverityLevel EventCount
-------------------- --------------- -----------
2026-09-12T16:00:00Z info 142
2026-09-12T16:00:00Z warning 3
2026-09-12T15:00:00Z info 138
count() with no argument counts rows per group, the KQL equivalent of COUNT(*) — grouping
by both a time bucket and a category field is the standard shape for an events-over-time
breakdown.
3. Add an action group to the alert so it actually notifies someone instead of just recording a fired state.
az monitor action-group create --resource-group learn-rg --name ops-email --short-name opsemail --action email ops reader@example.com
az monitor metrics alert update --resource-group learn-rg --name high-cpu-alert --add-action ops-email
$ az monitor action-group create --resource-group learn-rg --name ops-email --short-name opsemail --action email ops reader@example.com
{
"name": "ops-email",
"emailReceivers": [{ "name": "ops", "emailAddress": "reader@example.com" }]
}
An alert rule without an action group still logs a fired state you can see in the portal, but nothing is sent anywhere — the action group is what turns a recorded state change into an actual notification.
4. Check the alert's fired history to confirm it actually triggered during the CPU spike.
az monitor metrics alert show --resource-group learn-rg --name high-cpu-alert --query "name"
az rest --method get --url "https://management.azure.com/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/providers/Microsoft.AlertsManagement/alerts?api-version=2019-05-05-preview&alertRuleName=high-cpu-alert"
$ az rest --method get --url "https://management.azure.com/.../alerts?..."
{
"value": [
{
"properties": {
"essentials": {
"alertState": "New",
"severity": "Sev2",
"startDateTime": "2026-09-12T16:04:00Z"
}
}
}
]
}
startDateTime: 16:04 — one minute after the spike ended at 16:03 in the metrics table
above, consistent with a 5-minute window needing enough sustained samples above the
threshold before the rolling average itself crosses 90.
5. Delete the alert rule and the Log Analytics workspace once you're done experimenting.
az monitor metrics alert delete --resource-group learn-rg --name high-cpu-alert
az monitor log-analytics workspace delete --resource-group learn-rg --workspace-name learn-logs --yes --force
$ az monitor metrics alert delete --resource-group learn-rg --name high-cpu-alert
$ az monitor log-analytics workspace delete --resource-group learn-rg --workspace-name learn-logs --yes --force
--force skips the default soft-delete period on the workspace (normally 14 days,
recoverable) and removes it immediately — useful for a lesson environment, risky on
anything you might need to recover ingested log data from later.
Next: Cost Management — reading a cost analysis breakdown, the Well-Architected Framework’s cost pillar, and a concrete pay-as-you-go vs. reserved instance comparison.