Skip to main content
Azure · lesson 9 of 10
Azure intermediate 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 azure-monitor log-analytics kql observability

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.

Frequently Asked Questions

What is the difference between Azure Monitor metrics and Log Analytics?
Metrics are lightweight numeric time series (CPU percent, disk IOPS, request count) collected automatically at the platform level and cheap to query, kept for a fixed retention window. Log Analytics stores richer, queryable log and event data — including custom application logs — in a workspace you provision, queried with Kusto Query Language (KQL), and billed by ingested data volume rather than included for free.
Do I need to install anything on a VM to see its metrics, or its logs?
Platform metrics like CPU percent and network bytes are collected automatically with no agent required. Anything beyond that — memory usage from inside the guest OS, custom event logs, application traces — needs the Azure Monitor Agent installed and configured to send data to a Log Analytics workspace.
What does KQL's summarize operator actually do?
It groups rows by one or more fields (via a `by` clause) and computes an aggregate — count, avg, max, and others — over each group, the same shape as SQL's GROUP BY but with pipe syntax: data flows left to right through operators separated by `|`, each one transforming the output of the one before it.
Why didn't my alert rule fire immediately when CPU crossed the threshold?
Alert rules evaluate on a schedule (as short as one minute, but often five) rather than continuously, and most also require the condition to hold across a rolling evaluation window rather than a single instantaneous sample, specifically to avoid firing on a brief, harmless spike. The delay is the evaluation frequency plus however much of the window the condition needs to be true for.