Skip to main content

Monitor your app with Application Insights

When something is slow or broken in production, you need to see what your app is actually doing: which requests fail, how long they take, and where the time goes. This lab connects a web app on Azure App Service to Application Insights, the application performance monitoring (APM) service in Azure Monitor. You use either App Service autoinstrumentation or the Azure Monitor OpenTelemetry Distro to collect live metrics, request and failure analytics, and queryable logs. You also add a deterministic slow endpoint to practice diagnosis safely.

You will connect Application Insights three ways so you can pick the workflow that fits you:

  • Azure Developer CLI (azd) - provision Application Insights in Bicep alongside your app and deploy in one flow.
  • Azure CLI (az) - create the resources explicitly and wire them together with app settings.
  • Azure portal - turn on Application Insights from a blade on your app.

App Service supports autoinstrumentation (also called codeless attach) for .NET, Node.js, Java, and supported Python apps. It injects a monitoring agent at runtime, so you collect telemetry without adding an SDK. This lab's executable sample paths use .NET or Node.js on Linux and include a current runtime support matrix for existing apps.

App Service Labs complements Microsoft Learn

This lab is a hands-on, end-to-end walkthrough. For reference depth on any concept, follow the "Learn more" links to the official Microsoft Learn documentation.

Estimated time: 60 to 75 minutes

Objectives

By the end of this lab you will be able to:

  • Create a workspace-based Application Insights resource and connect it to an App Service app.
  • Connect .NET or Node.js on Linux with one instrumentation method and explain where autoinstrumentation support differs by runtime and OS.
  • Generate normal, failing, and deliberately slow traffic, then diagnose it in Live Metrics, Performance, Failures, and Logs (KQL).
  • Create a Standard availability test that checks the app from multiple Azure locations.
  • Create a metric alert that fires on a symptom your users would notice.

Prerequisites

Before you begin, you will need an Azure subscription with Owner permissions and a GitHub account.

In addition, you will need the following tools installed on your local machine:

Setup Azure CLI

Start by logging into Azure by run the following command and follow the prompts:

az login --use-device-code
tip

You can log into a different tenant by passing in the --tenant flag to specify your tenant domain or tenant ID.

Choose a region and low-cost tier

This lab uses the East US region and the B1 (Basic) Linux App Service tier, a low-cost option that is ideal for learning (about USD 13 per month if you leave it running). Application Insights bills on the data it ingests; the small volume in this lab costs little, and the first 5 GB per month is free. Delete the resource group when you finish (see Clean up) to stop charges.

How Application Insights connects to your app

Application Insights stores its telemetry in a Log Analytics workspace (a workspace-based resource, the current model). Your app sends telemetry to Application Insights using a connection string, which you supply through the APPLICATIONINSIGHTS_CONNECTION_STRING app setting. Depending on the runtime, App Service can attach a managed agent, or your code can initialize the Azure Monitor OpenTelemetry Distro. Choose one method for a given app.

Connection string, not instrumentation key

Always connect with the connection string. Instrumentation keys alone are deprecated because they do not carry the regional ingestion endpoints that newer regions require.

Choose your path

Choose your path
Set these once - every matching step and code sample below follows your choice.
Tooling
Language

The executable paths in this lab use a B1 Linux plan with .NET or Node.js. Pick a tooling path, then choose one of those languages. If you are connecting an existing app that uses another supported runtime or Windows, use the runtime support matrix after provisioning the monitoring resource.

Provision and connect Application Insights

The Azure Developer CLI provisions your infrastructure and deploys your code together. Here you define Application Insights, a Log Analytics workspace, and the web app in Bicep, with the connection string and the auto-instrumentation setting wired in, so the app is monitored the moment it starts.

1. Sign in

azd auth login

2. Create the project structure

Create a folder for the project, then choose your language for the sample app. The rest of the files (Bicep and parameters) are the same for every language.

mkdir monitor-app-insights && cd monitor-app-insights
mkdir infra src

Create azure.yaml in the project root. Set language to match the tab you pick below:

# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: monitor-app-insights
services:
web:
project: ./src
language: dotnet # dotnet or js
host: appservice

Create the sample app - a tiny web app with a home route, a route that fails, and a route that deliberately waits two seconds so you have successful, failed, and slow requests to investigate.

Create src/monitor-app-insights.csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

</Project>

Create src/Program.cs:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Home route - a successful request.
app.MapGet("/", () => Results.Content(
"<h1>Hello from Azure App Service with Application Insights!</h1>", "text/html"));

// A route that fails, so you have a failed request to look at.
app.MapGet("/error", () => Results.Problem("Simulated failure", statusCode: 500));

// A deterministic slow route for a safe performance investigation.
app.MapGet("/slow", async () =>
{
await Task.Delay(TimeSpan.FromSeconds(2));
return Results.Json(new { status = "complete", delayedMs = 2000 });
});

app.Run();

Set language: dotnet in azure.yaml, and in infra/resources.bicep (below) use linuxFxVersion: 'DOTNETCORE|8.0' and set SCM_DO_BUILD_DURING_DEPLOYMENT to 'false' - azd builds and publishes .NET locally, so no server-side build is needed.

Other runtimes and Windows

The copy-paste azd sample is intentionally limited to .NET and Node.js on Linux. Do not select another runtime without also changing the application code, build workflow, plan OS, linuxFxVersion, and instrumentation settings. For an existing app, use the runtime support matrix.

Create infra/main.parameters.json:

{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": { "value": "${AZURE_ENV_NAME}" },
"location": { "value": "${AZURE_LOCATION}" },
"resourceGroupName": { "value": "${AZURE_RESOURCE_GROUP}" }
}
}

Create infra/main.bicep. It runs at subscription scope so azd creates and owns the resource group for this environment:

targetScope = 'subscription'

@description('Name of the azd environment; used to derive resource names.')
param environmentName string

@description('Azure region for all resources.')
param location string

@description('Resource group to create for this environment.')
param resourceGroupName string

resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
name: resourceGroupName
location: location
}

module resources 'resources.bicep' = {
name: 'resources'
scope: rg
params: {
location: location
environmentName: environmentName
}
}

output WEB_URI string = resources.outputs.webUri
output APPLICATIONINSIGHTS_NAME string = resources.outputs.appInsightsName

Create infra/resources.bicep. This creates the workspace, the workspace-based Application Insights resource, the B1 Linux plan, and the web app with the monitoring app settings:

@description('Azure region for all resources.')
param location string

@description('azd environment name used to derive globally unique names.')
param environmentName string

var suffix = uniqueString(subscription().id, resourceGroup().id, environmentName)
var planName = 'plan-${suffix}'
var webName = 'app-${suffix}'
var lawName = 'law-${suffix}'
var appInsightsName = 'appi-${suffix}'

resource law 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: lawName
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
name: appInsightsName
location: location
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: law.id // makes this a workspace-based resource
}
}

resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: planName
location: location
sku: {
name: 'B1'
}
kind: 'linux'
properties: {
reserved: true // required for Linux plans
}
}

resource web 'Microsoft.Web/sites@2023-12-01' = {
name: webName
location: location
kind: 'app,linux'
tags: {
'azd-service-name': 'web' // links this site to the "web" service in azure.yaml
}
properties: {
serverFarmId: plan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'NODE|22-lts'
appSettings: [
{
name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
value: 'true'
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: appInsights.properties.ConnectionString
}
]
}
}
}

output webUri string = 'https://${web.properties.defaultHostName}'
output appInsightsName string = appInsights.name
Per-language changes

This template uses the Azure Monitor OpenTelemetry Distro in the Node.js sample and linuxFxVersion: 'NODE|22-lts' with SCM_DO_BUILD_DURING_DEPLOYMENT set to 'true'. For the .NET sample, change the runtime to DOTNETCORE|8.0, set server-side build to 'false', and add the three Linux .NET autoinstrumentation settings from the runtime support matrix, including XDT_MicrosoftApplicationInsights_PreemptSdk=1. Do not combine those agent settings with an application that initializes the OpenTelemetry Distro.

3. Create an environment and deploy

Give the environment a unique suffix so names do not collide with earlier runs:

SUFFIX=$(openssl rand -hex 3) # 6 lowercase hex chars
azd env new "monitor-appi-${SUFFIX}" --location eastus
azd env set AZURE_RESOURCE_GROUP "rg-appi-${SUFFIX}"
azd up

When it finishes, azd prints the app endpoint:

- Endpoint: https://app-<random>.azurewebsites.net/
SUCCESS: Your application was deployed to Azure in 3 minutes 35 seconds.
First azd up can miss the tagged resource

On the very first azd up, azd occasionally reports unable to find a resource tagged with 'azd-service-name: web' because the provisioning outputs are not cached yet. If that happens, run azd deploy once more - the resources already exist and the code deploy completes.

Connect Application Insights by runtime

Autoinstrumentation support differs by language and OS. This reference is for connecting an existing code-based App Service app. The executable provisioning paths above remain limited to .NET and Node.js on Linux.

This lab's .NET path targets ASP.NET Core on modern .NET, which supports autoinstrumentation on Windows and Linux. The required XDT_MicrosoftApplicationInsights_PreemptSdk setting below is specific to ASP.NET Core autoinstrumentation. For classic ASP.NET on .NET Framework, follow the separate settings in the App Service autoinstrumentation reference.

App settingValue
APPLICATIONINSIGHTS_CONNECTION_STRINGyour connection string
ApplicationInsightsAgent_EXTENSION_VERSION~3
XDT_MicrosoftApplicationInsights_Moderecommended
XDT_MicrosoftApplicationInsights_PreemptSdk1
Choose one instrumentation method

Do not combine App Service autoinstrumentation with an Application Insights SDK or the Azure Monitor OpenTelemetry Distro in the same server application. Overlapping instrumentation can duplicate telemetry and increase ingestion cost. Use the OpenTelemetry Distro instead of the managed agent when you need code-level control, then add custom spans or metrics through OpenTelemetry. See Data collection basics.

Add a safe slow endpoint

The azd .NET and Node.js samples already include /slow. It waits for two seconds and returns HTTP 200. The delay is deterministic, makes no external network call, and consumes little CPU, so it is safer and more reproducible than depending on an unreliable public service.

If you brought your own app through the Azure CLI or portal path, add the same behavior in your runtime and redeploy it. Keep this diagnostic route only in a nonproduction environment, or protect it with authentication.

Add this before app.Run():

app.MapGet("/slow", async () =>
{
await Task.Delay(TimeSpan.FromSeconds(2));
return Results.Json(new { status = "complete", delayedMs = 2000 });
});

Generate some traffic

Telemetry only appears once your app handles requests. Send a burst that includes successful, failing, and slow requests. Replace the hostname with your app's:

APP_URL=https://<your-app-name>.azurewebsites.net
for i in $(seq 1 30); do
curl -s -o /dev/null "$APP_URL/"
curl -s -o /dev/null "$APP_URL/error"
done

for i in $(seq 1 6); do
curl --fail --silent --max-time 5 "$APP_URL/slow" --output /dev/null
done

Telemetry reaches Application Insights within a minute or two. Live Metrics is near real time; the aggregated charts and Logs have a short ingestion delay.

Explore your telemetry

Open your Application Insights resource in the Azure portal (search for its name, or open it from the app's Application Insights blade).

Live Metrics

Select Live metrics (under Investigate). Leave the pane open, then run the traffic commands again. Watch request rate, duration, and failures update with about one-second latency. The live feed is sampled and is not retained; use Logs for a durable investigation.

Filter requests to URL containing /slow if your runtime supports Live Metrics filters. Confirm request duration rises while the slow loop runs, then clear the filter. Live Metrics is the fastest way to validate telemetry during a deployment, but it is not the place to calculate a long-term service-level indicator.

Failures and Performance

  • Failures (under Investigate) breaks down failed requests by response code and operation. Your /error requests appear as HTTP 500 responses. Select one to inspect request details. This route returns a failure response but does not throw, so exception telemetry is not expected.
  • Performance shows request duration by operation. Select the /slow operation, select Drill into samples, and open a sample. Its end-to-end transaction should show about two seconds in the request itself and no slow external dependency. That distinction prevents you from blaming a database or remote service when the delay is inside application code.

Logs (KQL)

Select Logs (under Monitoring) to query telemetry with Kusto Query Language (KQL). Run this to count requests by result code:

requests
| summarize count() by resultCode
| order by count_ desc

You should see rows for 200 and 500. To find the slowest requests:

requests
| top 10 by duration desc
| project timestamp, name, url, resultCode, duration, operation_Id

Summarize request latency by operation and include the median, 95th percentile, and 99th percentile:

requests
| where timestamp > ago(30m)
| summarize
requests=count(),
failures=countif(success == false),
p50=percentile(duration, 50),
p95=percentile(duration, 95),
p99=percentile(duration, 99)
by name
| order by p95 desc

The /slow operation should have a p50 and p95 near two seconds. Find only slow successful requests:

requests
| where timestamp > ago(30m)
| where success == true and duration > 1s
| project timestamp, name, url, duration, operation_Id
| order by duration desc

If your app calls a database or HTTP service, correlate requests with dependencies before assigning a cause:

requests
| where timestamp > ago(30m) and duration > 1s
| join kind=leftouter (
dependencies
| project operation_Id, dependencyName=name, target, dependencyDuration=duration, dependencySuccess=success
) on operation_Id
| project timestamp, name, duration, dependencyName, target, dependencyDuration, dependencySuccess
| order by duration desc

For this lab's deterministic /slow route, dependency columns should be empty or unrelated. The request remains slow because the delay is in the endpoint.

Workspace table names

Because this resource is workspace-based, the same data is available in the Log Analytics workspace under the AppRequests table (Application Insights presents it as requests in its own Logs blade). Both work; use whichever surface you are in.

You can verify the same thing from the command line by querying the workspace directly:

WS_ID=$(az monitor log-analytics workspace show \
--resource-group $RG_NAME \
--workspace-name $LAW_NAME \
--query customerId -o tsv)

az monitor log-analytics query \
--workspace "$WS_ID" \
--analytics-query "AppRequests | summarize count() by ResultCode"

Create a Standard availability test

An availability test sends a safe HTTP request from Azure-managed locations on a schedule. Use a Standard test, not the deprecated URL ping test. Standard tests are billed through Azure Monitor, so review the current availability-test pricing and delete the lab resource group when you finish.

The test checks the public home page every five minutes from five locations, expects HTTP 200, validates the TLS certificate, and warns when fewer than seven certificate-validity days remain. Five is the Microsoft-recommended minimum for distinguishing an app problem from a regional network problem; each additional location increases the number of billable test executions.

Append this resource to infra/resources.bicep, then run azd provision:

var availabilityTestName = 'availability-${appInsightsName}'

resource availabilityTest 'Microsoft.Insights/webtests@2022-06-15' = {
name: availabilityTestName
location: location
tags: {
'hidden-link:${appInsights.id}': 'Resource'
}
properties: {
SyntheticMonitorId: availabilityTestName
Name: availabilityTestName
Enabled: true
Frequency: 300
Timeout: 30
Kind: 'standard'
RetryEnabled: true
Locations: [
{ Id: 'us-ca-sjc-azr' }
{ Id: 'us-va-ash-azr' }
{ Id: 'emea-au-syd-edge' }
{ Id: 'us-tx-sn1-azr' }
{ Id: 'emea-nl-ams-azr' }
]
Request: {
RequestUrl: 'https://${web.properties.defaultHostName}/'
HttpVerb: 'GET'
FollowRedirects: true
}
ValidationRules: {
ExpectedHttpStatusCode: 200
SSLCheck: true
SSLCertRemainingLifetimeCheck: 7
}
}
}

The template already defines appInsights, appInsightsName, location, and webUri. Provision the new resource:

azd provision

Wait for at least one five-minute interval. In Availability, select the test to see success percentage, duration, and results by test location. You can also query the results:

availabilityResults
| where timestamp > ago(30m)
| summarize tests=count(), failures=countif(success == false), p95=percentile(duration, 95) by name, location
| order by name asc, location asc

Create a metric alert

Alerts tell you about a problem before your users report it. Create a rule on a symptom users would notice, such as failed requests or slow responses.

Add the alert to your Bicep so it is part of your infrastructure. Append this to infra/resources.bicep, then run azd provision again. It alerts when the average server response time exceeds 3 seconds over 5 minutes:

resource responseTimeAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
name: 'alert-response-time-${appInsightsName}'
location: 'global'
properties: {
severity: 3
enabled: true
scopes: [
appInsights.id
]
evaluationFrequency: 'PT1M'
windowSize: 'PT5M'
criteria: {
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
allOf: [
{
name: 'ResponseTime'
metricNamespace: 'microsoft.insights/components'
metricName: 'requests/duration'
operator: 'GreaterThan'
threshold: 3000
timeAggregation: 'Average'
criterionType: 'StaticThresholdCriterion'
}
]
}
}
}

Confirm the rule exists and is enabled:

az monitor metrics alert list --resource-group $RG_NAME \
--query "[].{name:name, enabled:enabled, severity:severity}" -o table

Verify

Confirm the app is serving traffic and that telemetry is flowing.

  1. The app returns HTTP 200. Check from the command line:

    curl -I https://<your-app-name>.azurewebsites.net

    Expected output (validated during authoring):

    HTTP/1.1 200 OK
    Content-Type: text/html
  2. The diagnostic endpoint is deliberately slow but successful:

    curl --fail --silent \
    --output /dev/null \
    --write-out "HTTP %{http_code} in %{time_total}s\n" \
    "$APP_URL/slow"

    Expect HTTP 200 in about two seconds. A much longer response suggests an unrelated startup or platform issue.

  3. Telemetry is arriving. In the Application Insights Logs blade, run:

    requests
    | summarize requests=count(), p95=percentile(duration, 95) by name, resultCode
    | order by p95 desc

    You should see successful and failed requests, and /slow should have a p95 near two seconds.

  4. The Standard availability test is enabled. In Availability, confirm at least one successful result from each configured location. If the first run has not completed, wait for the five-minute interval and refresh.

  5. The alert rule is enabled. The az monitor metrics alert list output above shows enabled: True.

Cleanup

Delete the Azure resources and the local azd environment:

azd down --purge --force

Summary

In this lab, you connected an App Service web app to Application Insights and confirmed telemetry was flowing. You learned how to:

  • Create a workspace-based Application Insights resource and connect it with the APPLICATIONINSIGHTS_CONNECTION_STRING app setting.
  • Connect .NET or Node.js on Linux with one instrumentation method and identify the supported App Service autoinstrumentation paths for .NET, Node.js, Java, and Python.
  • Diagnose a deterministic slow endpoint in Live Metrics, Performance, and Logs, and distinguish in-process delay from a slow dependency.
  • Use KQL to summarize result codes, latency percentiles, slow requests, dependencies, and availability.
  • Create a multi-location Standard availability test with TLS validation.
  • Create a metric alert on server response time or failed requests.

Troubleshooting

  • No data in Application Insights. Confirm APPLICATIONINSIGHTS_CONNECTION_STRING is set on the app and that you restarted it. Autoinstrumentation only takes effect after a restart and a request. Send more traffic and allow time for ingestion. See Troubleshoot Application Insights autoinstrumentation.
  • Data appears in Logs but not in the classic query API. Workspace-based resources store telemetry in the AppRequests table in the Log Analytics workspace. Query the workspace with az monitor log-analytics query (shown above) or use the Application Insights Logs blade, which exposes the requests alias.
  • azd up reports unable to find a resource tagged with 'azd-service-name: web'. This is the first-run output-cache race. Run azd deploy once more; the resources already exist and the code deploy completes.
  • Wrong runtime instrumented. Confirm the app uses a supported runtime and OS combination and that its exact app settings match the runtime support matrix. Python autoinstrumentation supports Python 3.9 through 3.13 on Linux App Service deployed as code, not Windows or custom containers.
  • /slow is fast or returns 404. Confirm you added the route to the deployed app, not only to a local copy, then redeploy and restart. The expected duration is about two seconds.
  • The Standard availability test is not linked to Application Insights. Confirm the hidden-link:<application-insights-resource-id> tag exists on the web test. In the portal, create the test from the Application Insights Availability pane.
  • Availability results have not appeared. The test runs every five minutes. Confirm the URL is publicly reachable, wait for a complete interval, and check that the selected test locations are valid.
  • Alert never fires. Metric alerts evaluate on a schedule and need enough data in the window. Generate sustained traffic (or lower the threshold) and confirm the rule shows enabled: True.

Learn more