Skip to main content

Building an end-to-end monitoring solution with Azure Arc, Log Analytics and Workbooks–Part 4: Data visualisation with Azure Workbooks

In part 1 I explained that we want to setup an application health dashboard to gain insights on the availability and health of the on-premise parts of our applications. Specifically, we want to monitor our application pools, scheduled tasks and windows services. I introduced the overall architecture and explained the building blocks.

Part 2 was all about the data collection part using Azure Arc Data Collection rules.

I continued in Part 3 with our custom table in Log Analytics to persist our data.

And today it is time for Part 4 were I share how visualize all this info using Azure Workbooks.

What we're visualizing

The workbook is the user-facing piece. Our goal is a dashboard that lets an operator quickly answer three questions: What's running? What's stopped or failed? Which machines need attention?

A good health dashboard has two modes: the "glance" mode where an operator can immediately see if anything is wrong, and the "investigate" mode where they can drill into specifics — which machine, which component, what the error message says, when did it fail.

We'll build this as a multi-tab Workbook using different tabs per environment (Development, Test, Acceptance and Production).

 


Each tab combines text explanations, KQL queries, and visualizations. The workbook can be made interactive.

Creating the workbook

Navigate to Azure Monitor > Workbooks in the Azure portal and click New to create a blank workbook.


Workbooks are built from cells. Each cell is either:

  • Text (Markdown) — for headers, explanations, documentation
  • Query (KQL) — to pull data from Log Analytics and display it
  • Parameter — for user inputs like dropdowns, date pickers, or text boxes
  • Metrics — for Azure Monitor metrics (not used in our case)

Building the tab for a specific environment

Each tab is a combination of Problems and the full list of all the details. It surfaces anything that's in a non-healthy state, regardless of component type, so operators can immediately see what needs attention.

We start by adding a Parameters cell. Here we'll configure an environment parameter:

Next we click Add > Add query to create a new query cell.

This query pulls the most recent status for each Application Pool in a specific environment and filters to anything that isn't running or successful:

let latestJobId =
    ServiceHealth_CL
    | top 1 by TimeGenerated desc
    | project JobId;

ServiceHealth_CL
| where JobId in (latestJobId)
| where ResourceType == "AppPool"
| where Environment =~ "{environment}"
| extend ResultJson = parse_json(Result)
| extend State = tostring(ResultJson.State)
| where State == "Stopped"
| project
    Name,
    State
| sort by Name asc


Visualization: Set the visualization type to Grid. This gives you a table view with sortable columns.

 

Make the Status column visually stand out by adding conditional formatting:

  • Click Column Settings in the query cell
  • Select the Status column
  • Click Add rule
  • Set conditions:
    • If Status == "Stopped", set background color to Red and text color to White

This creates an instant visual indicator — red rows mean something is broken.

We repeat the same process for the Windows Services and Scheduled Tasks.

Scheduled refresh

By default, workbooks refresh when you open them or manually click the Refresh button. If you want the workbook to auto-refresh while it's open (useful for NOC-style dashboards displayed on a monitor), set the auto-refresh interval in the workbook settings. Options range from 5 minutes to 1 day.

 


Saving and sharing the workbook

Once you're done building, click Done Editing and then Save. You'll be prompted to choose:

  • Location: Subscription and resource group (determines who can access it via RBAC)
  • Name: The display name for the workbook
  • Type: Private (only you) or Shared (anyone with access to the resource group)

Choose Shared so your team can access it.

There are three ways to share workbooks:

  1. Direct link: Click the Share button and copy the URL. Anyone with access to the workspace can open it.
  2. Pinning to a dashboard: From the workbook, click Pin to add it to an Azure dashboard. This is useful for NOC displays.
  3. Embedding in documentation: Copy the workbook URL and embed it in your team's runbook or operational documentation.

What's next

At this point, you have a fully functional, interactive health dashboard. Operators can open it, see at a glance what's unhealthy, drill into specifics by machine or component type, and investigate historical trends.

In Part 5, we'll tie everything together, cover end-to-end troubleshooting, and discuss extensions like alerting, automation, and integrating with incident management systems.

More information

Azure Workbooks overview - Azure Monitor | Microsoft Learn

Popular posts from this blog

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...

Cache stampede: when our cache turned against us

While investigating some performance issues, we ran into an ASP.NET Core API that cached a fairly expensive aggregation query for 60 seconds. Under normal load, that was fine: one request rebuilds the cache, everyone else reads from it. Under peak load, dozens of requests would arrive in that same expiry window, all see a cache miss, and all fire the same expensive query in parallel. The database didn't like that. That was the moment when our caching layer stopped helping and started hurting. A burst of requests comes in at the same time, all miss the cache, and all go hammer the database or the downstream API at once. That's a cache stampede . The cache was supposed to protect our backend, and for a few hundred milliseconds it did the opposite. Why this happens IMemoryCache.GetOrCreate (and its async sibling) looks like it protects you, but it doesn't add any locking on its own. Look at the naive version: public async Task<Report> GetReportAsync(string key) ...

A complex system designed from scratch never works

A few years ago, I worked as an architect on a big mainframe rewrite. I still count it as one of my failures. Not because the technology was wrong, but because I couldn't convince the management team to simplify the approach. Years later, the organization is still struggling to get the new system up and running. I left the project at the time, because I couldn't put my name behind an approach that would take very long and cost a lot of money without a working system to show for it along the way. Gall’s Law That memory keeps coming back to me, because it's a textbook case of Gall's Law playing out in real life. Gall's Law , from John Gall's Systemantics , states it plainly: A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works, and it cannot be patched to make it work. You have to start over with a simple system that works. What does that mean in practice,...