Skip to main content

Posts

Showing posts from July, 2026

Reading a Profiler trace without guessing

Note: this is part 3 of a series on the Azure Monitor Profiler. Part 1 covered what the Profiler is, part 2 covered enabling it. This post is about the part that actually matters: making sense of a trace once you have one. Having profiler traces is only half the job. I've seen people enable the profiler, open a trace, stare at a wall of unfamiliar method names, and close the tab. The trace explorer isn't self-explanatory the first time - it took me a few real incidents before the views clicked. This post is the walkthrough I wish I'd had. Getting to a trace From your Application Insights resource: Go to the Performance tab Pick an operation from the list or leave Overall selected Click on Profiler traces   Pick one of the captured requests, ideally one with a longer duration than usual - that's where you'll actually find something. Once you're in a trace, you get two views of the same call stack data: Flame graph - the full...

Enabling the Application Insights Profiler

Note: this is part 2 of a series on the Azure Monitor Profiler. Part 1 covered what the Profiler is and where to find the results - this post covers actually turning it on. There are two ways to enable the profiler: through app settings on App Service, or by wiring it into your code directly. Which one you need depends on where your app runs and how much control you want over the setup. Option 1: codeless enablement on App Service If your app runs on App Service (Windows) and your Application Insights resource is in the same subscription, this is the easiest path - no code changes, no redeploy. From the portal: In your App Service instance, select Monitoring > Application Insights Select Turn on Application Insights , then Enable Scroll down to the .NET or .NET Core tab Set Collection level to Recommended Under Profiler and Code Optimizations , select On Apply , then confirm with Yes Or skip the portal entirely and set the app settings directly...

Our P95 spiked, now what?

Yesterday one of our Application Insights dashboard showed a P95 latency spike and we had no idea why. The telemetry told us that a request was slow. But it didn't tell us why . Was it a database call? A CPU-bound loop? Lock contention? A GC pause? The naive approach would have to been to add logging statements around the code you suspect, redeploy, wait for the issue to reproduce, and repeat. In production, that's slow and it doesn't scale - you're guessing, and every guess costs a deployment cycle. We stayed away from all that guess work and reached out to the Application Insights Profiler . Instead of reasoning from logs, you get actual flame graphs of real production requests, showing exactly where time was spent. What the profiler actually captures The profiler runs as an agent alongside your application and periodically captures traces of live requests - not synthetic load, actual production traffic. For each captured request, it builds a trace you can in...

.NET Aspire: The price of forgetting WithReference

Recently I lost way more time than I'd like to admit on an error that turned out to be one missing line of code. The symptom looked like a networking problem. The cause was a missing WithReference call in my Aspire AppHost. Here's the exception my proxy threw the moment it tried to forward a request to my API: System.Net.Http.HttpRequestException: No such host is known. (api:443) ---> System.Net.Sockets.SocketException (11001): No such host is known. at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken) at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token) at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage initialRequest, Boolean async, CancellationToken cancellationToken) --- End of inner exception stack trace --- at System.Net.Http.HttpConnectionPool....

The mysterious .dev.localhost checkbox

When you create a new ASP.NET Core project in Visual Studio, there's a checkbox that's easy to click past: "Use the .dev.localhost TLD in the application URL." I always want to understand what a checkbox actually does before I tick it, so let's dig into this one. The problem it solves When you're working on more than one local web project, they all end up living at the same address: localhost . Only the port number tells them apart. Open your browser's address bar with three projects running and you'll see localhost:5001 , localhost:5215 , localhost:7099 — and you have no idea which is which until you actually look at the page. There's a second, less visible issue: because everything shares the localhost name, cookies and other domain-scoped browser storage are also shared across all your local apps. That's not something you usually want when you're testing. What .dev.localhost actually is .localhost is a reserved top-level dom...

Talking to Copilot like a caveman

  I think that everyone who uses AI recognizes the following pattern; you ask an LLM a simple question and it answers like it's writing a blog post: introduction, context, three examples, a closing summary. Fine for a first read, expensive when you're chaining calls or running an agent loop all day. The trick to avoid this is called "caveman prompting". You tell the model to drop articles, pleasantries and filler, and answer in short, blunt fragments. It sounds silly. But it works up to a point. A first attempt: just say "be concise" Most people's first instinct is a one-line system prompt: Be concise. No fluff. This already gets you a good chunk of the savings. In benchmarks I've seen floating around, a plain "be concise, return structured output" instruction accounts can already give you a nice reduction. It's the cheapest fix and most people stop here, which is reasonable. The caveman approach The caveman skill takes...

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,...

Fixing "Filename too long" errors on Windows with Git

There's a moment when you clone or pull a repository on Windows and Git throws an error like this: error: unable to create file some/very/deeply/nested/path/to/a/file.ts: Filename too long Nothing wrong with your code, nothing wrong with the repo. It's Windows. Why does this happen? Windows has a default path length limitation of 260 characters (the infamous MAX_PATH ). Git operations that create files with a full path longer than that — cloning, checking out, pulling — will fail with this error. Repositories with deeply nested folder structures (think node_modules, or generated code) hit this constantly. The fix: enable long paths in Git Git has a config setting for exactly this: core.longpaths . You have two ways to set it, depending on your rights on the machine. System-wide (requires Administrator privileges): git config --system core.longpaths true User-level (no Administrator required): git config --global core.longpaths true If y...

YARP and Aspire: "https+http scheme is not supported"

Recently I was wiring up a YARP reverse proxy in front of a couple of Aspire-managed services: an API and an Angular frontend. Aspire gives you service discovery for free, so the obvious move is to point your YARP clusters at the logical service names instead of hardcoded URLs. My first attempt looked like this: "Clusters": { "api-cluster": { "Destinations": { "api-destination": { "Address": "https+http://api" } } }, "frontend-cluster": { "Destinations": { "frontend-destination": { "Address": "https+http://angular-frontend" } } } } The https+http:// scheme is the standard Aspire service discovery convention: try HTTPS first, fall back to HTTP. It works fine when you're resolving endpoints through HttpClient . Unfortunately YARP doesn’t like this configuration. After setting it up with these values ...

The role of ActivitySource in OpenTelemetry for .NET

While doing some pair programming to integrate OpenTelemetry tracing to a .NET application, we had a discussion on how to use the ActivitySource . It looks simple. You new one up, give it a name, start an activity, done. The discussion started when we added a second ActivitySource with the exact same name in a different class. This made us wonder: "Are we duplicating traces now? Is this a memory leak? Do we need a singleton?" So we decided to dig deeper. This post is what we learned… What ActivitySource actually is ActivitySource is part of System.Diagnostics , not part of the OpenTelemetry NuGet packages. Microsoft built tracing primitives directly into the BCL, and OpenTelemetry's .NET SDK simply listens to them. This is why you can add distributed tracing to a library without taking a dependency on OpenTelemetry at all. An ActivitySource is a factory for Activity objects, and an Activity is .NET's name for what OpenTelemetry calls a span.(don’t ask m...

Combining Google Stitch with the GitHub Copilot Coding Agent

UI generation and background coding agents are two of the "AI" tools that really changed my way of working. Together, they close a gap that's been annoying me for a while: the coding agent still needs someone to describe what the UI should look like, and that someone is usually me, typing a wall of text into an issue and hoping for the best. Google Stitch generates UI screens (HTML/CSS, Tailwind, Flutter, SwiftUI, whatever…) from a prompt or a sketch. The GitHub Copilot coding agent picks up an issue and produces a pull request in the background, without you sitting in the editor. In this post we look at how to connect the two through MCP, so the coding agent stops guessing at layout, spacing and colors, and starts reading an actual design spec. Here's how to wire it up, and where it still needs a human in the loop. My first approach: screenshots in the issue body My first attempt was to design something in Stitch, paste a screenshot into a GitHub issue, and ...

Dependency-Track: Error occurred decrypting the OSS Index API Token

We run Dependency-Track for vulnerability analysis across our projects. One day, the OSS Index integration stopped working. No API calls, no analysis results, just this in the logs: An error occurred decrypting the OSS Index API Token; Skipping [projectName=Balansen - BatchConsole, vulnAnalysisLevel=PERIODIC_ANALYSIS, projectUuid=14ba5633-58c2-45fe-9f04-e4ab0b28375e, projectVersion=DEV] javax.crypto.BadPaddingException: Given final block not properly padded. Such issues can arise if a bad key is used during decryption. BadPaddingException is Java's polite way of saying: "I tried to decrypt this with a key, and it's the wrong key." The API token itself was fine. The key used to decrypt it wasn't. The wrong assumption Our first instinct was to re-enter the OSS Index credentials in the UI and assume a fat-fingered token was the culprit. That didn't help. The error came back on the next analysis run, right after we restarted the container for an unrelated...

Slowly Changing Dimensions in Microsoft Fabric - The no-code way

Dimension tables don't stay still. A customer moves city, a product gets reclassified, a salesperson switches regions. The question is never whether this happens, it's what you do with the old value once it changes. That question has a name: Slowly Changing Dimensions (SCD). If you haven't already, it's worth reading my first post about what SCDs actually are and the full set of types first — this post assumes you already know the difference between Type 1 and Type 2 and want to get straight to implementing Type 2 in Fabric. Until recently, implementing Type 2 in Fabric meant either building a Dataflow Gen2 with a chain of merge steps, or writing a PySpark notebook against Delta tables. Both work. Both also mean you're maintaining custom logic per table, forever. Fabric's Copy job now has SCD Type 2 built in as a write method. No merge statements, no derived columns for surrogate keys, no alter-row logic. You pick a write method from a dropdown. This pos...