Investigate Slow Apps with Azure Monitor

Use KQL in Azure Monitor Logs to investigate slow web requests.

Introduction

30 Second Summary

When a web app slows down, users feel the wait without seeing the cause. The person investigating needs evidence that points to the slow operation.

In this project, you will investigate a slow web application using Azure Monitor Logs in a free demo environment. You will use Kusto Query Language (KQL) to turn request records into performance evidence ready to share.

What You'll Build

You will run an investigation that transforms noisy request records into a response-time chart where the app's slow periods become visible.

By the end of this project, you'll have:

  • A live request preview where you can inspect request names, URLs, durations, result codes, and client cities.
  • A ranked performance table that filters requests above 150 milliseconds before comparing request counts, average duration, and percentile-based tail latency.
  • A shareable evidence pack containing a 12-hour response-time chart, exported CSV results, and reusable queries in app-performance-investigation.txt.
  • Secret Mission: Build a failed-request bar chart that ranks up to ten request names by failure count.

Are there any prerequisites?

You need a Mac with Safari and TextEdit. A Microsoft account or Microsoft Entra ID identity with access to the Azure portal is enough because no Azure subscription is required.

Before We Start

A useful investigation starts with a clear operational question. This checkpoint lets you define the web application behavior you are investigating and why slow requests matter to users or administrators.

Open Your Zero-Cost Monitoring Lab

Your performance question now needs real telemetry from Azure Monitor Logs. A practice environment removes the access uncertainty before you write any queries.

The Log Analytics demo requires no Azure subscription. It creates no billable resources for this investigation.

By the end of this step, Safari displays the demo query editor in the Azure portal. TextEdit holds a blank plain-text document for your later investigation notes.

In this step, get ready to:
  • Prepare a blank plain-text document in TextEdit.
  • Access the Microsoft Learn tutorial in Safari.
  • Open the Log Analytics demo query editor with the scope set to Demo.
Launch Safari and prepare TextEdit

Plain text keeps your investigation notes free of rich formatting. This gives your later queries a clean local home.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Safari in the search bar.
  • Press Enter to launch Safari.
  • Return to the search bar by pressing Cmd+Space (macOS) or the Windows key (Windows).
  • Type TextEdit in the search bar.
  • Press Enter to launch TextEdit.
  • Click File in the TextEdit menu bar.
  • Select New.

Safari is open. TextEdit shows a new blank document.

  • Click Format in the TextEdit menu bar.
  • Select Make Plain Text.

Your blank TextEdit document is now ready for plain-text investigation notes. Keep it open without saving it yet.

Open the Microsoft Learn tutorial

Microsoft Learn hosts the official tutorial that links to the demo. Access requires a Microsoft account or a Microsoft Entra ID identity that can sign in to the Azure portal.

Signing in can feel like a commitment. The demo still requires no Azure subscription.

  • Switch back to Safari from earlier.
  • Open the official Log Analytics tutorial.
  • Complete any sign-in prompt with a Microsoft account or Microsoft Entra ID identity.

You should return to the Log Analytics tutorial after authentication. Your identity can now access the demo link.

Sign-In Not Completing?

  • Confirm that your identity can access the Azure portal.
  • Reload the tutorial page after completing the sign-in flow.

Ask for help: Help me troubleshoot access to the Microsoft Learn Log Analytics demo. You can also ask the Azure Monitor community.

Enter the Log Analytics demo

Log Analytics is the Azure portal tool for querying data collected by Azure Monitor Logs. The demo opens this tool with sample telemetry ready for investigation.

Why Use the Demo Environment?

The demo gives you immediate query practice without an Azure subscription. It also avoids creating billable resources.

A personal workspace would add setup that this investigation does not need. The demo keeps your attention on interpreting application telemetry.

Before you open the lab, do you expect its scope to show a personal workspace or a shared demo?

  • Locate the Log Analytics demo environment link on the tutorial page.
  • Select the Log Analytics demo environment link.
  • Complete the Azure portal sign-in prompt with the same identity if it appears.
  • Look at the query scope in the upper-left corner of the editor.

Safari should display the Log Analytics query editor. You should see Demo in the upper-left scope.

You have cleared the uncertain-access hurdle. The query editor is ready without a subscription or billable resources.

Your zero-cost monitoring lab is ready. Next, you'll expose the noise in raw request telemetry.

Inspect Raw Request Telemetry

Your zero-cost lab is ready in Safari. The Azure portal query editor is scoped to Demo.

A performance investigation in Azure Monitor Logs starts by confirming what the request data contains. A log schema maps the fields that can answer your operational question.

In this step, you'll inspect AppRequests before previewing its raw records with Kusto Query Language. The preview tests whether a raw table can identify the main performance concern.

In this step, get ready to:
  • Inspect the fields available in the AppRequests schema.
  • Run a raw AppRequests query.
  • Identify why the broad result cannot reveal the main performance concern.
Inspect the AppRequests schema

Log Analytics organizes telemetry into tables. Each table has a schema that lists the fields available for investigation.

  • Return to the demo query editor in Safari.
  • Expand Log Management in the Tables pane.
  • Select AppRequests.
  • Locate Name, Url, DurationMs, ResultCode, and ClientCity.
  • Locate OperationName, _ResourceId, ItemCount, Success, and TimeGenerated.

How does the schema help?

The schema shows which fields you can use before you write a query. DurationMs records the number of milliseconds the application took to handle a request.

TimeGenerated records when request processing started. These fields support duration analysis across time.

Preview raw request telemetry

A table-only KQL query returns raw records from that table. This gives you a baseline view of the available request telemetry.

  • Click inside the query editor.
  • Enter the raw preview query by copying the code below into the editor:
// Query 1: Preview raw application requests
AppRequests

What does this query do?

  • The comment records the purpose of this query for your investigation.
  • AppRequests asks Log Analytics to return raw application request records.

Before you run the query, do you expect the raw records to reveal one clear performance concern?

  • Run the query by selecting Run in the query action bar.

You should see more than zero rows in the results pane. Your first query is working, so the demo is returning sample application requests.

No request records?

  • Confirm that Demo still appears in the upper-left query scope.
  • Check that AppRequests matches the spelling shown in the Tables pane.
  • Select Run again after placing your cursor inside the query.

Ask for help: Help me work out why my AppRequests query returns no records.

Assess the raw result

The successful preview proves that request telemetry is available. Each raw record receives equal weight in the result.

Before you judge the output, do you expect rows with equal weight to point to one clear performance concern?

  • Confirm that the result count is greater than zero.
  • Scan several visible request records.
  • Verify that the results include Name, Url, DurationMs, ResultCode, and ClientCity.
  • Verify that the results include OperationName, _ResourceId, ItemCount, Success, and TimeGenerated.
  • Compare DurationMs values across several OperationName values.

You should see more than zero records across the request fields you inspected. The unfiltered output provides no ranking that identifies the main performance concern.

Your raw telemetry baseline is ready. Next, you'll narrow the records to the requests that deserve performance investigation.

Filter the Slow Requests

The raw AppRequests preview proved that Azure Monitor Logs has request telemetry. The broad result still mixes quick requests with slow ones.

This step uses a Kusto Query Language (KQL) filter to isolate requests above 150 milliseconds. That smaller result makes the performance concern visible.

In this step, get ready to:
  • Add a duration filter to the existing AppRequests query.
  • Compare the filtered record count with the raw result.
  • Inspect requests that cross the performance threshold.
Filter requests by duration

The where operator keeps rows that satisfy a condition. This condition targets DurationMs values above 150.

  • Switch back to the Log Analytics query editor in Safari.
  • Place your cursor after AppRequests in the active Demo query.
  • Add the duration condition so the complete query matches the code below:
AppRequests
| where DurationMs > 150

What does this query do?

  • The AppRequests line selects the application request records.
  • The where operator removes every row that fails its condition.
  • The DurationMs > 150 condition keeps requests that took more than 150 milliseconds.

Before you run the query, pause to predict whether the result count will increase or decrease.

  • Select Run in the query action bar.

The record count should drop below the raw result. Every returned DurationMs value should be greater than 150.

That is the noise cut down. Your results now focus on requests that cross the performance threshold.

Query not running?

  • Check that the filter begins with the pipe in | where DurationMs > 150.
  • Confirm that DurationMs uses the same capitalization as the table schema.

Ask for help: Help me debug my KQL duration filter.

Compare the focused result

A lower record count shows how much noise the filter removed. Individual rows reveal which requests crossed the threshold.

  • Read the filtered record count above the results pane.
  • Compare the filtered count with the unfiltered count from your previous AppRequests run.
  • Scan the visible DurationMs values.
  • Identify the largest visible DurationMs value.
  • Inspect the Name value beside that duration.
  • Inspect the Url value for the same request.

You now have a focused slow-request dataset. Individual rows still cannot show whether an operation is consistently slow.

Recover from an empty result

Demo telemetry changes over time. An empty result can reflect a quiet time window with no requests above the current threshold.

  • Select the time range control if the query returned no rows.
  • Choose Last 7 days.
  • Select Run again.
  • Replace 150 with 5 if the wider time range still returns no rows.
  • Select Run to test the troubleshooting threshold.
  • Inspect the returned records after a successful fallback.
  • Replace 5 with 150 to restore the investigation threshold.

A successful fallback confirms that the demo still contains request telemetry. The standard 150 millisecond threshold remains the investigation target.

Before the final run, pause to predict whether any visible DurationMs values can be at or below 150.

  • Select Run to refresh the restored query.
  • Inspect every visible DurationMs value in the results pane.
  • Compare the final record count with the raw result count.

Every visible DurationMs value should be greater than 150. The result count should remain lower than the raw AppRequests count.

If the restored threshold returns zero rows, your earlier fallback run should show requests above 5 milliseconds. That result confirms that the dynamic demo data is available.

Your filter now isolates requests that cross the performance threshold. Next, you will group those requests to reveal which operations have the strongest performance problem.

Rank Operations by Performance

The slow-request filter in Azure Monitor Logs has done its job. Your result now contains the requests that deserve investigation.

Individual rows still cannot show whether an operation is consistently slow or distorted by outliers. In this step, Kusto Query Language aggregation turns those rows into operation-level evidence. Percentiles reveal the tail latency hidden by an average.

In this step, get ready to:
  • Create an operation-level performance ranking.
  • Compare average latency with percentile-based tail latency.
  • Record the strongest concern for the top operation.
Aggregate requests by operation

Aggregation groups related request records into one row per operation. The resulting table shows whether repeated requests point to a broader performance problem.

  • Switch back to the Demo query editor in Safari.
  • Click inside the active query editor.
  • Press Cmd+A (macOS) or Ctrl+A (Windows) to select the current filter query.
  • Replace the selected query by pasting this operation-performance query:
// Query 3: Rank slow operations
AppRequests
| where DurationMs > 150
| summarize RequestsCount=sum(ItemCount), AverageDuration=avg(DurationMs), percentiles(DurationMs, 50, 95, 99) by OperationName, _ResourceId
| order by AverageDuration desc

What does this query do?

  • The where operator keeps requests whose DurationMs value is greater than 150.
  • The summarize operator groups the remaining records by OperationName plus _ResourceId.
  • RequestsCount totals the ItemCount values within each group.
  • AverageDuration calculates the mean request duration for each operation.
  • percentiles() estimates the 50th, 95th, and 99th percentile durations.
  • order by AverageDuration desc places the largest average duration at the top.

Before you run the query, do you expect the highest-ranked operation to have the largest request count or the largest average duration?

  • Run the aggregation by selecting Run in the query action bar.

You should see one row per operation and resource. The exact values vary because the demo data changes over time.

You have turned a noisy list of requests into an operation-level ranking you can defend.

Seeing an empty or incomplete table?

  • Use the wider time range you established in the previous step before running the exact query again.
  • Confirm that every query line begins with the same pipe character shown in the code block.
  • Confirm that AppRequests appears once at the top of the query.

Ask for help: Help me find why my operation-performance KQL query returns an empty or incomplete table.

Interpret the latency percentiles

An average compresses every duration into one value. Percentile columns show how the typical request compares with the slow edge of the distribution.

How do the percentile columns help?

  • AverageDuration represents the overall mean duration for the operation.
  • percentile_DurationMs_50 represents the middle of the duration distribution.
  • percentile_DurationMs_95 exposes the slower edge experienced by a smaller share of requests.
  • percentile_DurationMs_99 isolates the most extreme tail shown in this table.
  • Locate the first row in the results table.
  • Read its OperationName value.
  • Compare AverageDuration with percentile_DurationMs_50.
  • Compare percentile_DurationMs_95 with AverageDuration.
  • Compare percentile_DurationMs_99 with AverageDuration.
  • Choose whether average latency or tail latency presents the stronger concern.

A high average close to the 50th percentile points to broadly slow behavior. A much larger 95th or 99th percentile points to severe delays at the tail.

Verify the ordered result

The descending sort provides the final proof that the first row has the largest average duration. The column set confirms that the aggregation produced every required performance measure.

Before you rerun the query, do you expect AverageDuration to decrease as you move down the table?

  • Rerun the completed query by selecting Run.

You should see OperationName, _ResourceId, RequestsCount, AverageDuration, percentile_DurationMs_50, percentile_DurationMs_95, and percentile_DurationMs_99. The AverageDuration values should descend from the first row.

Your ranking now distinguishes broadly slow operations from operations with severe tail latency. Next, you will chart response time over a 12-hour window and preserve the evidence outside the demo.

Chart the Trend and Save Evidence

Your ranked Azure Monitor Logs table identifies the slowest operations by average duration. The percentile columns expose tail latency.

However, the table cannot show whether response time stays steady or arrives in spikes. A time-series chart reveals that pattern across 12 hours.

A CSV export preserves the displayed rows. A local file in TextEdit keeps your reusable KQL investigation available after you leave the demo.

In this step, get ready to:
  • Chart average request duration across a 12-hour window.
  • Export the displayed results as CSV evidence.
  • Save all four investigation queries in app-performance-investigation.txt.
Chart response time over 12 hours

Ten-minute bins turn individual requests into a readable trend. Grouping by _ResourceId keeps telemetry from different resources in separate series.

  • Create a new query tab from the query tab bar.
  • Add the response-time trend by entering this KQL:
// Query 4: Chart the response-time trend
AppRequests
| where TimeGenerated > ago(12h)
| summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId
| render timechart

What does this query do?

  • The TimeGenerated filter keeps requests from the latest 12-hour window.
  • The summarize operator calculates avgRequestDuration for each 10-minute time bin.
  • The _ResourceId grouping creates a separate series for each resource.
  • The render timechart operator displays those averages as a time chart.

Before you run the query, predict whether response time will stay flat or change across the 12-hour window.

  • Select Run in the action bar.

You'll see a time-series chart with avgRequestDuration plotted in 10-minute intervals across the 12-hour window.

Good, your performance investigation now shows how response time moves over time.

Chart missing or empty?

  • Check that the query tab contains only the response-time trend query.
  • Compare each pipe with the query above because a missing pipe breaks the operator sequence.

Ask for help: Help me debug why my Azure Monitor Logs trend query is not rendering a time-series chart.

Export the displayed evidence

The chart helps you interpret the trend. The CSV keeps the displayed result set available outside the demo.

  • Select Share in the action bar above the results.
  • Select Export to CSV - displayed columns.

You'll see a completed CSV download in Safari.

Your displayed results are now portable evidence that you can share outside the demo environment.

CSV download missing?

  • Wait for the chart query to finish before selecting Share again.
  • Confirm that you selected Export to CSV - displayed columns from the sharing options.

Ask for help: Help me find why my displayed Log Analytics results did not download as a CSV file.

Save the reusable investigation

The demo environment does not preserve saved queries. Your plain-text file becomes the reusable copy of the complete investigation.

  • Switch back to the blank TextEdit document from earlier.
  • Add the four main investigation queries by pasting this content:
// Query 1: Preview raw application requests
AppRequests

// Query 2: Isolate requests slower than 150 milliseconds
AppRequests
| where DurationMs > 150

// Query 3: Rank slow operations
AppRequests
| where DurationMs > 150
| summarize RequestsCount=sum(ItemCount), AverageDuration=avg(DurationMs), percentiles(DurationMs, 50, 95, 99) by OperationName, _ResourceId
| order by AverageDuration desc

// Query 4: Chart the response-time trend
AppRequests
| where TimeGenerated > ago(12h)
| summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId
| render timechart

How do the four queries work together?

  • Query 1 previews raw application requests to confirm that telemetry is available.
  • Query 2 keeps requests whose DurationMs value is greater than 150.
  • Query 3 groups slow requests into an operation-level performance ranking.
  • Query 4 shows how average request duration changes over time.

✔️ Awesome, I've got everything!

Your four queries are in place. Continue to save the investigation file.

ⓧ I'd like to double check the full code

Compare your TextEdit document with this complete investigation file.

// Query 1: Preview raw application requests
AppRequests

// Query 2: Isolate requests slower than 150 milliseconds
AppRequests
| where DurationMs > 150

// Query 3: Rank slow operations
AppRequests
| where DurationMs > 150
| summarize RequestsCount=sum(ItemCount), AverageDuration=avg(DurationMs), percentiles(DurationMs, 50, 95, 99) by OperationName, _ResourceId
| order by AverageDuration desc

// Query 4: Chart the response-time trend
AppRequests
| where TimeGenerated > ago(12h)
| summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId
| render timechart

This reference preserves the full progression from raw telemetry to a time-based performance view.

  • Select File in the macOS menu bar.
  • Select Save.
  • Name the document app-performance-investigation.txt.
  • Choose a location on your Mac.
  • Select Save in the save dialog.

Before the final check, predict which chart, download, and local file should now exist.

  • Use the query result area to confirm that the response-time chart remains visible.
  • Check Safari's completed downloads for a CSV file.
  • Read the TextEdit window title to confirm that it shows app-performance-investigation.txt.

Strong finish. Your time chart, CSV export, and local query file now form a repeatable performance investigation package.

Missing one of your evidence files?

  • Confirm that TextEdit saved the document under the exact app-performance-investigation.txt filename.
  • Repeat the export from the chart result if the CSV does not appear in Safari's completed downloads.

Ask for help: Help me verify why my CSV export or TextEdit investigation file is missing.

Secret mission

Find the Most Common Failed Requests

Your performance view shows where requests are slow. Now add a reliability view that ranks the request names with the most failures. Then compare the leading failure with your performance findings.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your local evidence, pause your work, or delete the files entirely. The Log Analytics demo created no personal Azure resources, so this project has no ongoing costs.

Resources you used:

  • Main performance investigation file (app-performance-investigation.txt).
  • Secret Mission query file (app-performance-investigation-secret.txt).
  • CSV evidence exported from the 12-hour response-time results.

Keep everything running

No action is needed. Choose this option if you want to revisit the investigation.

  • Keep app-performance-investigation.txt as your reusable performance investigation.
  • Keep app-performance-investigation-secret.txt as your reusable reliability investigation.
  • Keep the exported CSV as evidence you can review or share.

Pause - I'll come back to this later

Pausing means closing the open project windows while preserving every saved file.

  • Close the Azure portal demo query tabs in Safari.
  • Close the investigation documents in TextEdit.
  • Leave the two text files in their current saved locations.
  • Leave the exported CSV in its current download location.

Delete - I don't want to use this again

Removing the evidence is safe because the project created no personal Azure resources. Emptying the Mac trash permanently deletes the local files you choose.

  • Close the Azure portal demo query tabs in Safari.
  • Close the investigation documents in TextEdit.
  • Move app-performance-investigation.txt to the Mac trash from the location where you saved it.
  • Move app-performance-investigation-secret.txt to the Mac trash from the location where you saved it.
  • Move the exported CSV evidence to the Mac trash from its download location.
  • Confirm that the Mac trash contains both text files plus the exported CSV evidence.
  • Confirm that every other file in the Mac trash is safe to delete.
  • Empty the Mac trash.

You should no longer find the two text files or the exported CSV evidence on your Mac.

Nice Work!

Nice Work!

Investigation complete! You transformed noisy request telemetry in Azure Monitor Logs into performance evidence using Log Analytics. Your charts plus exported results now show where slow requests occur.

You learned how to:

  • Use Kusto Query Language to inspect AppRequests telemetry. You also isolated requests with a DurationMs value greater than 150.
  • Build an operation performance ranking with request counts plus average duration. You compared those averages with 50th, 95th, and 99th percentile latency.
  • Render a 12-hour response-time trend to reveal changes over time. You preserved the evidence as a CSV export plus reusable queries in app-performance-investigation.txt.
  • Secret Mission: Ranked the most common failed request names in a bar chart. You recorded whether the top failure also appeared in the performance analysis.

Ready to quiz yourself?