Shape a KQL query result-by-result, build an alert on it, then trigger the alert on purpose.
Logs you cannot query are just storage
Every Azure resource emits telemetry, but scattered across a hundred blades it is noise. Observability is the discipline of pooling that telemetry in one place and being able to ask it questions — which is what a Log Analytics workspace and its query language, KQL, are for. This lab uses a data source you already generate for free: the subscription Activity log, the audit trail of every management operation you run. You will pipe it into a workspace, query it, and then turn a query into an alert — the exact shape of production monitoring, where a KQL expression that returns rows is what wakes someone up.
By the end you will have a workspace collecting real data, a KQL query that reads back your own operations, and a scheduled-query alert rule with an action group behind it — a loop you can point at any workload later.
Monitoring is a question you have written down in advance.
You need a free Azure account, the Azure CLI (az), then be signed in with az login. First time? The 15-minute Set up your machine page covers the account, the installs (winget / brew / apt), and sign-in. Prefer zero installs? Run everything in Azure Cloud Shell (Bash), preinstalled and already signed in.
This page is the full lab; the workspace and alert as Bicep are in github.com/kloudcaptain/campux-labs under lab-observability-kql. Run everything in Azure Cloud Shell (Bash). Log Analytics includes a free daily ingestion allowance, and the Activity log is free to stream — this lab stays within it.
A workspace to collect into
Create a resource group and a Log Analytics workspace. The workspace is the pool everything queryable lands in.
# Windows/Git Bash: stop it mangling /subscriptions/... arguments (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1
RG="campux-lab-obs-rg"
az group create -n "$RG" -l eastus
az monitor log-analytics workspace create -g "$RG" -n campux-logs
WS_ID=$(az monitor log-analytics workspace show -g "$RG" -n campux-logs --query id -o tsv)
WS_GUID=$(az monitor log-analytics workspace show -g "$RG" -n campux-logs --query customerId -o tsv)
echo "workspace resource id: $WS_ID"
id (used to wire data in) and the customerId GUID (used to query). Empty for now; the next step gives it something to collect.Stream the Activity log in
A diagnostic setting at the subscription level routes the Activity log into your workspace. From now on, every management operation you run is recorded there.
SUB_ID=$(az account show --query id -o tsv)
az monitor diagnostic-settings subscription create \
--name campux-activity-to-logs \
--location eastus \
--subscription "$SUB_ID" \
--workspace "$WS_ID" \
--logs '[{"category":"Administrative","enabled":true},{"category":"Alert","enabled":true}]'
Now generate a little activity to be sure there is something to find — create and delete a throwaway resource group:
az group create -n campux-obs-ping -l eastus && az group delete -n campux-obs-ping --yes
Microsoft.Resources/subscriptions/resourceGroups/write and …/delete operations.Ask a question in KQL
Query the AzureActivity table straight from the CLI. Read this KQL as a pipeline: take the table, keep the last hour, summarise a count per operation, sort by the busiest.
az monitor log-analytics query -w "$WS_GUID" --analytics-query ' AzureActivity | where TimeGenerated > ago(1h) | summarize count() by OperationNameValue, ActivityStatusValue | sort by count_ desc ' -o table
Turn a query into an alert
An alert is a KQL query Azure runs on a schedule, firing when it returns too many rows. First an action group — who gets told — then the rule itself, watching for failed operations.
# who to notify (put your own email in) az monitor action-group create -g "$RG" -n campux-oncall \ --short-name cxoncall \ --action email me [email protected] AG_ID=$(az monitor action-group show -g "$RG" -n campux-oncall --query id -o tsv) # fire if any Activity-log operation failed in the last 5 minutes az monitor scheduled-query create -g "$RG" -n campux-failed-ops \ --scopes "$WS_ID" \ --condition "count 'placeholder' > 0" \ --condition-query placeholder='AzureActivity | where ActivityStatusValue == "Failure"' \ --evaluation-frequency 5m --window-size 5m \ --severity 2 --action-groups "$AG_ID" \ --description "A management operation failed"
az monitor scheduled-query show -g "$RG" -n campux-failed-ops \
--query "{name:name, enabled:enabled, freq:evaluationFrequency}" -o table
You now have the full loop: data flowing in, a question expressed as KQL, and an action group that turns a non-empty result into an email. Point the same pattern at an app's exceptions or a VM's CPU and you have production monitoring.Tear it down
Delete the resource group, and remove the subscription-level diagnostic setting separately — it lives on the subscription, not in the group.
az monitor diagnostic-settings subscription delete \
--name campux-activity-to-logs --subscription "$SUB_ID" --yes
az group delete -n campux-lab-obs-rg --yes
az group exists -n campux-lab-obs-rg # -> false
What you can now honestly claim
You created a Log Analytics workspace, routed real telemetry into it, queried that telemetry in KQL, and built a scheduled-query alert wired to an action group. That is "observability with Azure Monitor, Log Analytics, and thoughtful alerting" — a line lifted almost word-for-word from the job descriptions — done rather than described. KQL in particular is a transferable superpower: the same language queries Application Insights, Microsoft Sentinel, and Azure Resource Graph, so the query muscle you just built pays off far beyond this one workspace.
- Activity-log data is ingested free, and Log Analytics grants a daily free allowance beyond that, so this lab stays at no cost. A busy production workspace does bill for ingestion and retention — which is exactly why cost-aware query and retention design is part of the observability job.
- There is a short delay — usually a few minutes — between an operation happening and it being queryable, because logs are batched on their way in. If a query returns nothing, that latency is the first thing to suspect, not a broken pipeline.