Who this is for
You already have the metrics add-on running and dashboards in Grafana. Now the bill or the workspace limits are climbing, queries time out on large clusters, and a recording rule you need is missing. These are the commands and queries we keep at hand.
Wiring and access
# Is the add-on enabled, and which workspace does it write to?
az aks show -g rg-aks -n aks-prod --query azureMonitorProfile -o json
# Enable it against an existing Azure Monitor workspace
az aks update -g rg-aks -n aks-prod --enable-azure-monitor-metrics \
--azure-monitor-workspace-resource-id "$AMW_ID"
# Workspace query endpoint
AMW_QUERY=$(az monitor account show -g rg-monitor -n amw-prod \
--query metrics.prometheusQueryEndpoint -o tsv)
# Collector pods
kubectl -n kube-system get pods -l rsName=ama-metrics -o wide
kubectl -n kube-system get pods | grep ama-metrics
Querying from the CLI needs the Monitoring Data Reader role on the workspace and a token for the Prometheus audience:
TOKEN=$(az account get-access-token --resource https://prometheus.monitor.azure.com \
--query accessToken -o tsv)
promq() {
curl -s -G "$AMW_QUERY/api/v1/query" \
-H "Authorization: Bearer $TOKEN" --data-urlencode "query=$1" | jq '.data.result'
}
promq 'up{job="kube-state-metrics"}'
The endpoint speaks the standard Prometheus HTTP API, so /api/v1/query_range, /api/v1/series and /api/v1/labels work the same way.
Cardinality: find what is inflating active series
Active time series drive both cost and the workspace limits. Check the workspace's ingestion and active-series utilization metrics in the portal, or list them with:
az monitor metrics list-definitions --resource "$AMW_ID" -o table
Then find the source with PromQL. Run these against a short window; on a large workspace they are expensive.
| Question | PromQL |
|---|---|
| Top 20 metric names by series | topk(20, count by (__name__)({__name__=~".+"})) |
| Series per scrape job | sort_desc(count by (job)({__name__=~".+"})) |
| Series per namespace | topk(20, count by (namespace)({__name__=~".+"})) |
| Series one metric produces, by label | count by (le)(http_request_duration_seconds_bucket) |
| Samples per scrape per target | topk(20, scrape_samples_post_metric_relabeling) |
| Targets that grew after a deploy | topk(20, delta(scrape_series_added[1h])) |
Usual offenders:
| Offender | Why it explodes | Fix |
|---|---|---|
| Histograms with many buckets × many routes | buckets × label combinations | Fewer buckets; drop _bucket where you only need _sum/_count |
Labels holding IDs (user_id, request_id, full URL path) | Unbounded values | Fix instrumentation, or drop the label with relabeling |
| cAdvisor per-container metrics on dense nodes | pods × containers × interfaces/devices | Keep only the metrics you chart |
| Short-lived pods (jobs, CI runners) | New pod label value every run | Aggregate in recording rules; drop raw series |
| Pod annotation scraping on everything | Every app's debug metrics ingested | Scrape only namespaces that opt in |
Controlling what gets scraped
The add-on is configured through ConfigMaps in kube-system. The settings ConfigMap (ama-metrics-settings-configmap) turns default targets on or off and holds per-target keep-lists; custom scrape jobs go in ama-metrics-prometheus-config. Neither exists until you create it, and the settings schema has changed between versions (key names such as default-scrape-settings-enabled versus default-targets-scrape-enabled), so start from the template in the current Microsoft documentation for your add-on version.
kubectl -n kube-system get configmap | grep ama-metrics
kubectl -n kube-system get configmap ama-metrics-settings-configmap -o yaml
By default the add-on uses a minimal ingestion profile that keeps a curated set of metrics per default target. Turning that profile off can multiply ingestion for that target; add the specific metrics you need to the keep-list instead.
In custom scrape jobs, drop at ingestion with standard relabeling:
scrape_configs:
- job_name: app-metrics
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: "true"
metric_relabel_configs:
- source_labels: [__name__]
action: drop
regex: "http_request_duration_seconds_bucket|go_gc_.*"
- action: labeldrop
regex: "request_id|user_id"
After applying, the ama-metrics pods restart and pick up the change within a few minutes. Validate with the collector logs:
kubectl -n kube-system logs -l rsName=ama-metrics -c prometheus-collector --tail=100 | grep -iE "error|invalid|config"
Container and label names in the add-on can vary by version; check kubectl describe pod if the selector returns nothing.
Warning
A typo in
metric_relabel_configswithaction: keepkeeps only what matches, which can silently drop every metric for that job. Comparecount by (job)before and after each change.
PromQL operators actually use
Saturation and throttling
| Need | PromQL |
|---|---|
| CPU throttling ratio per container | sum by (namespace,pod,container)(rate(container_cpu_cfs_throttled_periods_total[5m])) / sum by (namespace,pod,container)(rate(container_cpu_cfs_periods_total[5m])) |
| CPU used vs request | sum by (namespace,pod)(rate(container_cpu_usage_seconds_total{container!=""}[5m])) / sum by (namespace,pod)(kube_pod_container_resource_requests{resource="cpu"}) |
| Memory working set vs limit | sum by (namespace,pod,container)(container_memory_working_set_bytes{container!=""}) / sum by (namespace,pod,container)(kube_pod_container_resource_limits{resource="memory"}) |
| Node allocatable CPU requested | sum by (node)(kube_pod_container_resource_requests{resource="cpu"}) / sum by (node)(kube_node_status_allocatable{resource="cpu"}) |
Health
| Need | PromQL |
|---|---|
| Restarts in the last hour | topk(20, increase(kube_pod_container_status_restarts_total[1h])) |
| OOMKilled containers | kube_pod_container_status_last_terminated_reason{reason="OOMKilled"} == 1 |
| Pods not ready | sum by (namespace)(kube_pod_status_ready{condition="false"}) |
| Deployments below desired | kube_deployment_spec_replicas - kube_deployment_status_replicas_available > 0 |
| Nodes not Ready | kube_node_status_condition{condition="Ready",status="true"} == 0 |
| Scrape targets down | up == 0 |
Metric availability depends on the keep-lists above: with the minimal profile some cAdvisor and kube-state-metrics series are not ingested. If a query returns nothing, check the keep-list before assuming the workload is healthy.
Keep queries fast at scale
- Aggregate early.
sum by (namespace)(rate(x[5m]))scans the same data asrate(x[5m])but returns far fewer series to Grafana. - Use recording rules for anything on a shared dashboard or an alert. Managed Prometheus rule groups are Azure resources:
az alerts-management prometheus-rule-group create -g rg-monitor -n aks-prod-recording \
-l westeurope --scopes "$AMW_ID" --cluster-name aks-prod --interval PT1M \
--rules '[{"record":"namespace:container_cpu_usage:rate5m","expression":"sum by (namespace)(rate(container_cpu_usage_seconds_total{container!=\"\"}[5m]))","enabled":true}]'
That command lives in a CLI extension (az extension add --name alertsmanagement); check az alerts-management prometheus-rule-group create --help for the flags in your version, or deploy rule groups with Bicep (Microsoft.AlertsManagement/prometheusRuleGroups).
- Match
rate()windows to the scrape interval. A window at least four times the scrape interval avoids gaps; with 30-second scrapes,[2m]is the practical minimum. - Limit time ranges on dashboards that use high-cardinality metrics. Query limits on the workspace apply per query; check the current Azure Monitor workspace limits page rather than guessing.
Prevent it next time
- Add a weekly "top 20 metrics by series" check and compare against last week.
- Require new scrape jobs to come with a
metric_relabel_configsblock and an expected series count. - Alert on the workspace's active-series utilization before it reaches its limit.
- Put recording rules and scrape config in the same repo as the cluster IaC so changes are reviewed together.