← Back to DevBytes

Monitoring App Service: Metrics, Alarms, and Dashboards

Introduction to App Service Monitoring

What is App Service Monitoring?

App Service monitoring refers to the continuous collection, analysis, and visualization of performance data from your web applications and the underlying App Service infrastructure. In platforms like Azure App Service, this includes a rich set of built-in metrics covering CPU usage, memory consumption, HTTP traffic patterns, response times, and error rates. Monitoring combines three essential pillars: metrics (the raw numerical data points), alarms (automated alerts triggered when thresholds are breached), and dashboards (visual canvases that bring metrics together for real-time observation).

Modern App Service platforms surface metrics through multiple channels — the Azure Portal, Azure Monitor, Application Insights, REST APIs, and CLI tools. Each metric is a time-series data point with properties like timestamp, value, and dimension (for example, breaking down HTTP 500 errors by instance or by endpoint). Understanding this data flow is the foundation of operational excellence for any production web workload.

Why Monitoring Matters

Without monitoring, you are flying blind. A seemingly healthy application can silently degrade under load, accumulate memory leaks, or start serving errors to a subset of users without any visible symptom until customer complaints arrive. Effective monitoring gives you:

The cost of not monitoring is measured in downtime hours, lost revenue, and engineering time spent firefighting issues that could have been prevented. A well-instrumented App Service pays for itself many times over.

Understanding Metrics in App Service

Key Metrics to Monitor

App Service exposes dozens of metrics. Here are the ones you absolutely need to track, grouped by what they tell you:

Infrastructure-level metrics (App Service Plan):

Application-level metrics (App Service instance):

Application Insights metrics (if enabled):

Accessing Metrics Programmatically

The Azure Portal provides a rich metrics explorer, but automation demands programmatic access. Here are the primary methods:

Azure CLI — Fetching CPU metrics for the last hour:

#!/bin/bash
# Get the resource ID for your App Service Plan
APP_PLAN_ID=$(az appservice plan show \
  --resource-group myResourceGroup \
  --name myAppServicePlan \
  --query id -o tsv)

# Query CPU percentage metric with 5-minute grain
az monitor metrics list \
  --resource "$APP_PLAN_ID" \
  --metric "CpuPercentage" \
  --aggregation "Average" \
  --interval 5m \
  --start-time "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
  --end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  --output table

Azure PowerShell — Multi-metric query with dimension filtering:

# Connect to Azure account
Connect-AzAccount

# Define parameters
$resourceGroup = "myResourceGroup"
$appServiceName = "myWebApp"
$resourceId = "/subscriptions/xxxx/resourceGroups/$resourceGroup/providers/Microsoft.Web/sites/$appServiceName"

# Fetch multiple metrics in one call
$metrics = Get-AzMetric `
  -ResourceId $resourceId `
  -MetricName @("Http5xx", "Requests", "AverageResponseTime") `
  -TimeGrain "PT5M" `
  -StartTime (Get-Date).AddHours(-6) `
  -AggregationType "Total","Average"

# Display results
foreach ($metric in $metrics.Data) {
    Write-Host "Metric: $($metric.TimeStamp) - $(($metric | Select-Object).Total)"
}

Azure Monitor REST API — Generic HTTP query:

# Authenticate and get token (using service principal)
TOKEN=$(curl -s -X POST \
  -d "client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&resource=https://management.azure.com&grant_type=client_credentials" \
  https://login.microsoftonline.com/$TENANT_ID/oauth2/token \
  | jq -r .access_token)

# Query metrics for the last 30 minutes
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://management.azure.com/subscriptions/$SUB_ID/resourceGroups/$RG/providers/Microsoft.Web/sites/$APP_NAME/providers/microsoft.insights/metrics?api-version=2018-01-01&metricnames=Http5xx,Requests&aggregation=Total,Average×pan=2024-01-01T00:00:00Z/2024-01-01T00:30:00Z&interval=PT5M" \
  | jq '.value[] | {name: .name.value, data: .timeseries[0].data}'

Application Insights KQL — Querying telemetry directly:

// P95 response time by operation over the last hour
requests
| where timestamp > ago(1h)
| summarize 
    P50 = percentile(duration, 50),
    P95 = percentile(duration, 95),
    P99 = percentile(duration, 99),
    RequestCount = count()
  by operation_Name
| order by P95 desc

// Dependency failures grouped by target service
dependencies
| where timestamp > ago(24h)
| where success == false
| summarize FailureCount = count(), 
    AvgDuration = avg(duration) 
  by target, dependencyType
| order by FailureCount desc

Setting Up Alarms (Alerts)

Alert Types and Rules

Alarms transform passive metrics into active notifications. Azure Monitor supports several alert types relevant to App Service:

Each alert rule needs an action group defining what happens when the alert fires: email, SMS, push notification, webhook call to PagerDuty or Opsgenie, or triggering an Azure Function for auto-remediation.

Creating Alerts with Infrastructure as Code

ARM Template — CPU alert with email notification:

{
  "type": "Microsoft.Insights/metricAlerts",
  "apiVersion": "2018-03-01",
  "name": "high-cpu-alert-appservice",
  "location": "global",
  "properties": {
    "description": "Alert when CPU exceeds 85% for 10 minutes",
    "severity": 2,
    "enabled": true,
    "scopes": [
      "[resourceId('Microsoft.Web/serverFarms', 'myAppServicePlan')]"
    ],
    "evaluationFrequency": "PT5M",
    "windowSize": "PT10M",
    "criteria": {
      "allOf": [
        {
          "metricName": "CpuPercentage",
          "metricNamespace": "Microsoft.Web/serverFarms",
          "operator": "GreaterThan",
          "threshold": 85,
          "timeAggregation": "Average",
          "dimensions": []
        }
      ],
      "odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria"
    },
    "actions": [
      {
        "actionGroupId": "[resourceId('Microsoft.Insights/actionGroups', 'ops-team-notifications')]",
        "webHookProperties": {}
      }
    ]
  }
}

Azure CLI — Creating a dynamic threshold alert:

# Create a dynamic threshold alert for HTTP 5xx errors
az monitor metrics alert create \
  --resource-group "myResourceGroup" \
  --name "dynamic-http5xx-alert" \
  --scopes "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/myWebApp" \
  --metric "Http5xx" \
  --aggregation "Total" \
  --operator "GreaterThan" \
  --dynamic-sensitivity "High" \
  --evaluation-frequency "5m" \
  --window-size "15m" \
  --severity 3 \
  --action-group "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Insights/actionGroups/ops-team" \
  --description "Dynamic alert on HTTP 5xx spikes"

Log search alert — KQL-based alert for exception bursts:

# Create a log search alert rule using Application Insights data
az monitor scheduled-query create \
  --resource-group "myResourceGroup" \
  --name "exception-burst-alert" \
  --scopes "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Insights/components/myAppInsights" \
  --description "Alert when exception count exceeds 50 in 5 minutes" \
  --query 'exceptions | where timestamp > ago(5m) | summarize count() | where count_ > 50' \
  --evaluation-frequency "5m" \
  --window-size "5m" \
  --severity 2 \
  --action-groups "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Insights/actionGroups/ops-team"

Bicep — Modern declarative alert rule:

resource cpuAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
  name: 'high-memory-alert'
  location: 'global'
  properties: {
    description: 'Memory percentage exceeds 90%'
    severity: 2
    enabled: true
    scopes: [appServicePlan.id]
    evaluationFrequency: 'PT5M'
    windowSize: 'PT15M'
    criteria: {
      'allOf': [
        {
          metricName: 'MemoryPercentage'
          metricNamespace: 'Microsoft.Web/serverFarms'
          operator: 'GreaterThan'
          threshold: 90
          timeAggregation: 'Average'
        }
      ]
      'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
    }
    actions: [
      {
        actionGroupId: actionGroup.id
      }
    ]
  }
}

Action Groups — Defining What Happens When Alerts Fire

An action group is a reusable collection of notification channels. Here's how to create one with email, webhook, and Azure Function integration:

az monitor action-group create \
  --resource-group "myResourceGroup" \
  --name "ops-team-critical" \
  --short-name "ops" \
  --action email ops-email ops-alerts@company.com \
  --action webhook pagerduty \
    "https://events.pagerduty.com/integration/xxx/enqueue" \
    "pd_service=prod-app-service" \
  --action azurefunction auto-scale-function \
    "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/AutoRemediation/functions/AutoScaleOut"

Building Effective Dashboards

Dashboard Design Principles

A dashboard is not a data dump — it's a curated visual narrative of your application's health. Follow these principles:

Creating Dashboards Programmatically

Azure Dashboard JSON — Full dashboard definition:

{
  "location": "australiaeast",
  "tags": {
    "environment": "production",
    "team": "platform-engineering"
  },
  "properties": {
    "lenses": [
      {
        "order": 0,
        "parts": [
          {
            "position": { "x": 0, "y": 0, "rowSpan": 2, "colSpan": 6 },
            "metadata": {
              "inputs": [],
              "type": "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart",
              "settings": {
                "content": {
                  "resourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Web/sites/prod-api",
                  "metrics": [
                    {
                      "name": "Http5xx",
                      "aggregation": "Sum",
                      "namespace": "Microsoft.Web/sites"
                    }
                  ],
                  "title": "HTTP 5xx Errors - Last 24 Hours",
                  "timeRange": { "type": "Relative", "value": "P1D" },
                  "chartType": "Area"
                }
              }
            }
          },
          {
            "position": { "x": 6, "y": 0, "rowSpan": 2, "colSpan": 6 },
            "metadata": {
              "inputs": [],
              "type": "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart",
              "settings": {
                "content": {
                  "resourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Web/serverFarms/prod-plan",
                  "metrics": [
                    {
                      "name": "CpuPercentage",
                      "aggregation": "Average",
                      "namespace": "Microsoft.Web/serverFarms"
                    },
                    {
                      "name": "MemoryPercentage",
                      "aggregation": "Average",
                      "namespace": "Microsoft.Web/serverFarms"
                    }
                  ],
                  "title": "CPU & Memory - App Service Plan",
                  "timeRange": { "type": "Relative", "value": "P1D" },
                  "chartType": "Line"
                }
              }
            }
          },
          {
            "position": { "x": 0, "y": 2, "rowSpan": 3, "colSpan": 12 },
            "metadata": {
              "inputs": [],
              "type": "Extension/Microsoft_Azure_Monitoring/AppInsights_QueryResultsTile",
              "settings": {
                "content": {
                  "query": "requests | where timestamp > ago(24h) | summarize avg(duration) by bin(timestamp, 1h) | order by timestamp asc | render timechart",
                  "title": "Average Response Time Trend (Hourly)",
                  "appInsightsResourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Insights/components/prod-appinsights"
                }
              }
            }
          },
          {
            "position": { "x": 0, "y": 5, "rowSpan": 2, "colSpan": 4 },
            "metadata": {
              "inputs": [],
              "type": "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart",
              "settings": {
                "content": {
                  "resourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Web/sites/prod-api",
                  "metrics": [
                    { "name": "Requests", "aggregation": "Sum", "namespace": "Microsoft.Web/sites" }
                  ],
                  "title": "Request Volume",
                  "timeRange": { "type": "Relative", "value": "P7D" },
                  "chartType": "Bar"
                }
              }
            }
          },
          {
            "position": { "x": 4, "y": 5, "rowSpan": 2, "colSpan": 4 },
            "metadata": {
              "inputs": [],
              "type": "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart",
              "settings": {
                "content": {
                  "resourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Web/sites/prod-api",
                  "metrics": [
                    { "name": "AverageResponseTime", "aggregation": "Average", "namespace": "Microsoft.Web/sites" }
                  ],
                  "title": "Response Time P50",
                  "timeRange": { "type": "Relative", "value": "P7D" }
                }
              }
            }
          },
          {
            "position": { "x": 8, "y": 5, "rowSpan": 2, "colSpan": 4 },
            "metadata": {
              "inputs": [],
              "type": "Extension/Microsoft_Azure_Monitoring/Monitoring_AlertsList",
              "settings": {
                "content": {
                  "title": "Active Alerts",
                  "filter": {
                    "resourceGroup": "prod-rg",
                    "severity": [0, 1, 2]
                  }
                }
              }
            }
          }
        ]
      }
    ]
  }
}

Azure CLI — Creating a dashboard from JSON file:

az dashboard create \
  --resource-group "myResourceGroup" \
  --name "production-app-dashboard" \
  --location "australiaeast" \
  --definition @dashboard-definition.json \
  --tags "environment=production team=platform"

Azure PowerShell — Scripted dashboard creation with dynamic resource IDs:

# Dynamically resolve resource IDs and build dashboard
$appServiceId = (Get-AzWebApp -ResourceGroupName "prod-rg" -Name "prod-api").Id
$planId = (Get-AzAppServicePlan -ResourceGroupName "prod-rg" -Name "prod-plan").Id
$appInsightsId = (Get-AzApplicationInsights -ResourceGroupName "prod-rg" -Name "prod-appinsights").Id

$dashboard = @{
    location = "australiaeast"
    properties = @{
        lenses = @(
            @{
                order = 0
                parts = @(
                    @{
                        position = @{ x = 0; y = 0; rowSpan = 2; colSpan = 6 }
                        metadata = @{
                            type = "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart"
                            settings = @{
                                content = @{
                                    resourceId = $appServiceId
                                    metrics = @(
                                        @{ name = "Http5xx"; aggregation = "Sum"; namespace = "Microsoft.Web/sites" }
                                    )
                                    title = "HTTP 5xx Errors"
                                    timeRange = @{ type = "Relative"; value = "P1D" }
                                }
                            }
                        }
                    }
                )
            }
        )
    }
}

$dashboardJson = $dashboard | ConvertTo-Json -Depth 10
New-AzDashboard -ResourceGroupName "prod-rg" -Name "production-app-dashboard" -Definition $dashboardJson

Pin Visualizations from Metrics Explorer

The fastest way to build dashboards is pinning live charts directly from the Azure Portal's Metrics Explorer, then exporting the dashboard JSON for version control. Once pinned, you can retrieve the dashboard definition programmatically:

# Export existing dashboard as JSON for source control
az dashboard show \
  --resource-group "myResourceGroup" \
  --name "production-app-dashboard" \
  --output json > dashboard-export.json

# You can now commit this to Git and redeploy it to other environments

Best Practices for App Service Monitoring

1. Establish a Baseline Before Setting Alerts

Deploy your application, let it run under normal load for at least 48 hours, and collect metric data before configuring threshold alerts. Without a baseline, you risk setting thresholds too low (alert fatigue from false positives) or too high (missing real incidents). Use dynamic threshold alerts during this learning period — they adapt automatically.

2. Implement the "Three Pillars" of Alerting

Every production App Service should have alerts covering availability (HTTP 5xx rate > threshold), performance (response time P95 > SLA target), and capacity (CPU or memory > 85%). These three pillars catch the vast majority of production incidents before they escalate.

3. Use Multi-Stage Alert Escalation

Create at least two severity levels: Warning (severity 3-4) for conditions that need attention within hours, and Critical (severity 0-1) for conditions requiring immediate response. Route warnings to email or Slack during business hours; route critical alerts to PagerDuty with on-call rotation 24/7. A sample alert matrix:

# Warning level - CPU > 70% for 15 minutes during business hours
az monitor metrics alert create \
  --name "cpu-warning" --severity 3 \
  --threshold 70 --window-size "15m" \
  --action-group "slack-notification-channel"

# Critical level - CPU > 90% for 5 minutes at any time
az monitor metrics alert create \
  --name "cpu-critical" --severity 1 \
  --threshold 90 --window-size "5m" \
  --action-group "pagerduty-oncall-rotation"

4. Correlate Deployments with Metrics

Tag every deployment with a marker in Application Insights or annotate your dashboards with deployment events. When a metric anomaly appears, the first question is always "what changed?" Having deployment markers visible on the same timeline as metrics cuts root cause investigation from hours to minutes:

// Application Insights - track deployments as custom events
// Send this from your CI/CD pipeline after each deployment
customEvents
| where name == "Deployment"
| project timestamp, 
    DeploymentVersion = customDimensions.Version, 
    DeploymentUser = customDimensions.User,
    Environment = customDimensions.Environment
| order by timestamp desc

// Query to correlate deployments with error spikes
let deployments = customEvents
  | where name == "Deployment"
  | project DeploymentTime = timestamp, Version = customDimensions.Version;
requests
| where timestamp > ago(24h)
| summarize ErrorCount = countif(resultCode startswith "5") by bin(timestamp, 5m)
| extend Deployment = tostring(deployments | where DeploymentTime between (bin_start(timestamp, 5m) .. bin_end(timestamp, 5m)) | project Version)
| order by timestamp asc

5. Monitor the Monitor

Alert rules themselves can fail — action groups may have expired webhook URLs, or metric ingestion might be throttled. Create a "meta-alert" that fires when expected heartbeat metrics are missing, indicating a monitoring pipeline failure:

// Detect missing heartbeat — no requests logged in 10 minutes
// This catches App Service outages AND monitoring failures
let heartbeat = requests
  | where timestamp > ago(10m)
  | count;
let threshold = 1;
heartbeat | where count_ < threshold
| project "No requests received in last 10 minutes — possible outage or monitoring failure"

6. Right-Size Your Metric Granularity

Balance granularity against cost and noise. For production workloads, 5-minute intervals provide sufficient resolution for alerting while keeping data volume manageable. Use 1-minute granularity only for critical, high-throughput services where rapid detection matters. For long-term trend analysis (capacity planning), aggregate to 1-hour or 1-day buckets and store in a separate analytics database to avoid cluttering your operational dashboards.

7. Build Runbook-Linked Dashboards

Every dashboard tile that represents a potential incident should link to a corresponding runbook. Add markdown tiles with direct links to your incident response procedures, playbooks, and relevant Azure Monitor workbooks. A well-built operations dashboard answers not just "what is happening?" but also "what do I do about it?"

8. Automate Remediation Where Safe

For well-understood conditions, wire alerts directly to Azure Functions or Logic Apps that perform automatic remediation — scaling out when queue length grows, restarting a single instance when memory exceeds threshold, or clearing a cache when disk space runs low. Always pair automated actions with notifications so the on-call team is informed:

# Azure Function triggered by alert webhook for auto-scale
# function.json binding
{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["post"],
      "route": "auto-scale-out"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

# run.csx - Parse alert context and scale out
using System.Net;
using System.Text;

public static async Task Run(
    HttpRequestMessage req, 
    ILogger log)
{
    var content = await req.Content.ReadAsStringAsync();
    dynamic alert = JsonConvert.DeserializeObject(content);
    
    string metricName = alert.data.essentials.metricName;
    double threshold = alert.data.essentials.monitoringThreshold;
    
    log.LogInformation($"Alert received: {metricName} exceeded {threshold}");
    
    // Call Azure Management API to scale out
    // ... scale operation logic ...
    
    return new HttpResponseMessage(HttpStatusCode.OK);
}

9. Version-Control Your Monitoring Configuration

Treat alert rules, action groups, and dashboard definitions as code. Store ARM templates or Bicep files in the same repository as your application code. Deploy monitoring configuration alongside each release. This ensures monitoring evolves with your application and prevents configuration drift between environments. A typical monitoring deployment pipeline:

# Deploy monitoring infrastructure via Azure DevOps pipeline
# Step 1: Deploy action groups
az deployment group create \
  --resource-group "prod-rg" \
  --template-file "monitoring/action-groups.bicep" \
  --parameters environment=production

# Step 2: Deploy alert rules
az deployment group create \
  --resource-group "prod-rg" \
  --template-file "monitoring/alert-rules.bicep" \
  --parameters @alert-params.json

# Step 3: Deploy dashboards
az deployment group create \
  --resource-group "prod-rg" \
  --template-file "monitoring/dashboards.bicep"

# Step 4: Validate all alerts are active
az monitor metrics alert list \
  --resource-group "prod-rg" \
  --query "[?enabled==true].name" -o tsv

10. Regularly Review and Prune

Schedule a quarterly review of all alert rules. Disable or adjust rules that have fired zero times (they may be misconfigured) or fired too frequently (causing alert fatigue). Archive dashboards that are no longer actively used. Monitoring configuration rots just like code — keep it maintained.

Conclusion

Monitoring your App Service through metrics, alarms, and dashboards transforms your application from a black box into a transparent, manageable system. The journey starts with understanding which metrics matter for your specific workload, then methodically building alert rules that wake you up before users notice a problem, and finally crafting dashboards that give your entire team a shared view of system health. The code examples in this tutorial — from Azure CLI queries to full ARM template deployments — give you the practical tools to implement a production-grade monitoring setup. Start with the three-pillar alerting foundation (availability, performance, capacity), version-control everything as infrastructure-as-code, and iterate based on what your real production data teaches you. A well-monitored App Service is the bedrock of reliable cloud operations.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles