Building an Azure App Service Log Viewer in LINQpad
Azure’s built-in log viewing experience leaves a lot to be desired, especially when Application Insights isn’t configured properly or simply isn’t showing what you need. After hitting this frustration one too many times while debugging a microservice issue, I decided to build my own solution using LINQPad and Azure’s APIs. Here’s how I evolved from a manual file-picker script to a fully interactive log viewer, and the adoption challenges I encountered along the way.
Quick link to the download section
The Problem: Azure’s Log Viewing Experience
One day at work, about 9 months ago or so, I was having some issues with one of our micro-services in a deployed AppService in Azure. I needed to go look at the logs on the server to see what the issue was, but I found the experience in Azure really quite lacking, especially if you have Application Insights turned on but it’s showing nothing, or it’s not configured properly, or any other host of reasons.
I knew from using the SCM stuff in Azure you can look at the filesystem on the server, and there’s an API connection you can access to browse the filesystem and view files.
So I put two and two together, made four, and came up with fairly large script to download the logs from the server and present them in LINQpad so I could look at them, run searches, and choose at least how I wanted to display them.
Iteration 1: Manual File Selection
The log files on the server are in the location /vfs/LogFiles
.
There will be multiple files named ‘eventlog.xml’ which are rotated as they get too large. As you can tell from the extension, this is just the event log as XML.
This appears to actually be the same format as the XML you get from out of the Windows Event Viewer. If you open Event Viewer, right click on a log entry and right click, then click copy > copy details as text, you’ll see what the format looks like. Here’s one I just got from my PC (after I opened event viewer, tried to do that, and MMC crashed 🤣, and I had to reopen it.)
<event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<system>
<provider name="Application Hang" guid="{c631c3dc-c676-59e4-2db3-5c0af00f9675}"></provider>
<eventid>1002</eventid>
<version>0</version>
<level>2</level>
<task>101</task>
<opcode>0</opcode>
<keywords>0x8000000000000000</keywords>
<timecreated systemtime="2025-07-20T01:02:29.7624115Z"></timecreated>
<eventrecordid>52356</eventrecordid>
<correlation></correlation>
<execution processid="16204" threadid="19764"></execution>
<channel>Application</channel>
<computer>Crow</computer>
<security userid="S-1-5-18"></security>
</system>
<eventdata>
<data name="AppName">mmc.exe</data>
<data name="AppVersion">10.0.26100.4484</data>
<data name="ProcessId">0x4fb8</data>
<data name="StartTime">0x1dbf911bbd7eb1b</data>
<data name="TerminationTime">4294967295</data>
<data name="ExeFileName">C:\Windows\System32\mmc.exe</data>
<data name="ReportId">26ea9a4e-3d34-4e37-abf1-4c04ddc830a7</data>
<data name="PackageFullName"></data>
<data name="PackageRelativeAppId"></data>
<data name="HangType">Cross-process;Top level window is idle</data>
</eventdata>
</event>
Yes, my PC at home is named ‘Crow’ after the MST3k bot.
I believe that the EventLog
data format is different per event type but I wasn’t really that interested in dealing with all of that. I had examples of data from my server so I just formatted the classes in the script to see what I wanted to get.
Originally, I was using the Azure AppService page where you can download the publish profile, and then I had added the LINQpad file picker control, asking you to download the file manually and pick the file out of your downloads folder.
The format of a pubxml file is also fairly easy to divine from looking at an example.
And that worked and was ‘good enough’ for a while. Then I spent some time trying to update the UI in the script to explain where this file comes from, as when I showed the script to people that was the first question they asked.
But, as they say, “There’s gotta be a better way!”
Iteration 2: Azure CLI Integration
One day I was playing with Azure CLI after I’d discovered what it can do for me and what capabilities it has. I discovered the ‘webapp’ module and had an idea that I could update my UI to use the Util.Cmd
command in LINQpad to call out to Azure CLI instead. So I got that working, and that made it a little better, but…
- It was still shelling out to call
az webapp list
, thenaz webapp get
to get the publish credentials, and then feed that into the script to get to the same place I was before where I could add the credentials to theHttpClient
. - Our laptops at work are now ‘locked down’, so you can’t install your own software, or tools you want to try out, without a JIT timed exception from IT, which is a hassle, and we don’t have a dedicated ‘developer’ image, since the number of people in our Software Engineering department is less than 30. So, when I showed this version to people, they thought it was a little better, but when I explained you needed Azure CLI installed they immediately dismissed it again.
- Plus, if your Azure CLI login had timed out, that was a hassle, and I had to code a bunch of
try
catch
es and annoying UI exceptions to handle this case.
So, again, less than perfect implementation, and no adoption in the company.
Iteration 3: Azure Resource Manager APIs
Cut to 6 months later. I am doing some work on Azure Cloud Services (Extended Support) and I discover the Azure Resource Manager classes in Nuget. I think I had already known they were there but didn’t really try playing with them. I got them to work and used it to make the ESCS query tool I wrote about in a previous post.
So, using these, I played around some more, found it has the webapp/website functionality that links to AppServices, so I was off to the races again, with a much better tool. And now, I can use LINQpad’s built in Util.MSAL.AcquireTokenAsync
function to fetch my azure credentials! Joe Albahari has thoughtfully included a snippet in the samples that show how to make a query you can load for fetching an Azure Token to do work with the ArmClient.
So this got me to this final version which I’m pretty happy with. I have also been playing around a lot with System.Reactive
since learning about LINQpad controls, but I’ve already talked about that earlier too.
I feel like I could write some tutorials on how to use those, but to me they seem…fairly self explanatory? Really not hard to use? I just looked at the definitions in ILSpy, and looked at the examples in the Samples folder. The Linqpad Interactive Regex evaluator kind of gives you what you need. But, I suppose, some people aren’t that adventurous, so maybe some examples would help. Idea for a future post.
Stage | Description |
---|---|
Initial screen | ![]() |
Loading | ![]() |
App service list | ![]() |
Log output | ![]() |
Search | ![]() |
The Real Challenge: Tool Adoption
The current biggest challenge I have with this, and these other tools, is adoption.
Mostly what happens, it seems, is I will find something I need to do during my day, whip up a script for it, and then get fancy and write a UI around it since it’s pretty easy. We have an internal tools repo in Azure Devops for uploading LINQpad scripts. I have been using it for years (again as I have said before) and did a couple of presentations with it at work when I first joined the company. My boss was sufficiently impressed enough to start using it himself, and we started to try and drive adoption of it in the organization. We got a LINQpad 6 corporate key and I was happy that it was seeing some adoption. But since then, a few more versions got released, Joe updated the license each time, so since 8 came out I had to go back to the bat again and try and prove to folks that getting the new version was worth it. Which is, again, sometimes a pain. And I would put scripts in the repo, they would be using functionality that was exclusive to 7 or 8, and then people would complain the scripts don’t work. I would have to ask why and I’d get a screenshot with the compile errors, and explain:
- Me: “You don’t have the latest version.”
- Coworker: “Well, how do you have it and I don’t?”
- Me: “I paid for it.”
- Coworker: “Oh…” and there the conversation would peter out.
I did have to beg and complain about not having the latest version, and explain to management that I had written a tool in it for our internal RBAC that made adding users a pretty simple operation, and that if people didn’t have the latest version they couldn’t run it. Management approved the license upgrade, thankfully.
(I’m not knocking the author here. Much respect for Joe. He’s very responsive, adds features, and supports the tool very well. But he’s just one guy, trying to make a living, and charging what I feel is a super reasonable price for a very powerful tool I love, compared to a lot of other development tools out there.)
This did sort of result in some weird tensions around the office, about, why do we need to write a proper tool as an Angular SPA when Craig already wrote this tool? I had to explain people in the support department, or PMs, or other people in the organization who aren’t in Software Engineering don’t want to install a programmer’s tool just run a script. But that’s a fight for another day…
Download Links
It requires the other queries in this repo:
What I Learned
Building useful tools is only half the battle - distribution and adoption are just as important. This experience taught me that:
- Tool discovery is a real problem in organizations
- Installation friction kills adoption
- Version mismatches create support burden
- Sometimes a “good enough” web app beats a “perfect” desktop tool
Despite the adoption challenges, this tool has saved me hours debugging production issues, and the few colleagues who do use it regularly have found it invaluable.
Sometimes that’s enough.