Setting up Application Insights took 10 minutes. It created two days of work for me.
I've been upgrading my podcast site from a 10 year old WebMatrix site to modern open-source ASP.NET Core with Razor Pages. The site is now off the IIS web server and running cross-platform in Azure.
I added Application Insights to the site in about 10 min just a few days ago. It was super easy to setup and basically automatic in Visual Studio 2017 Community. I left the defaults, installed a bit of script on the client, and enabled the server-side profiler, and AppInsights already found a few interesting things.
It took 10 minutes to set up App Insights. It took two days (and work continues) to fix what it found. I love it. This tool has already given me a deeper insight into how my code runs and how it's behaving - and I'm just scratching the service. I'll need to do some videos and/or more blog posts to dig deeper. Truly, you need to try it.
Slow performance in other countries
I could fill this blog post with dozens of awesome screenshots of the useful charts, graphs, and filters that I got by just turning on AppInsights. But the most interesting part is that I turned it on really expecting nothing. I figured I'd get some "Google Analytics"-type behavior.
Then I got this email:
Huh. I had set up the Azure CDN at images.hanselminutes.com to handle all the faces for each episode. I then added lazy loading so that the webite only loads the images that enter the browser's viewport. I figured I was pretty much done.
However I didn't really think about the page itself as it loads for folks from around the world - given that it's hosted on Azure in the West US.
Ideally I'd want the site to load in less than a second, but this is my archives page with 600 shows so it's pretty heavy.
Yuck. I have a few options. I could pay and load up another copy of the site in South Asia and then do some global load balancing. However, I'm hosting this on a single small (along with a dozen other sites) so I don't want to really pay much to fix this.
I ended up signing up for a free account at CloudFlare and set up caching for my HTML. The images stay the same. served by the Azure CDN.
Fixing Random and regular Server 500 errors
I left the site up for a while and came back later to a warning. You can see my site availability is just 93%. Note that there's "2 Servers?" That's because one is my local machine! Very cool that AppInsights also (optionally) tracks your local development server as well.
When I dig in I see a VERY interesting sawtooth pattern.
Pro Tip - Recognizing that a Sawtooth Pattern is a Bad Thing (tm) is an important DevOps thing. Why is this happening regularly? Is it exactly regularly (like every 4 hours on a schedule?) or somewhat regularly (like a garbage collection issue?)
What do these operations have in common? Look closely.
It's not a GET it's a HEAD. Remember that HTTP Verbs are more than GET, POST, PUT, DELETE. There's also HEAD. It literally is a HEADer call. Like a GET, but no body.
HTTP HEAD - The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response.
I installed HTTPie - which is like curl or wget for humans - and issue a HEAD command from my local machine while under the debugger.
C:>http --verify=no HEAD https://localhost:5001
HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Date: Tue, 13 Mar 2018 03:41:51 GMT
Server: Kestrel
Ok that is bad. See the 500? I check out AppInsights and see it has the full call stack. See it's getting a NullReferenceException as it tries to Render() the Razor page?
It turns out since I'm using Razor Pages, I have implemented "OnGet" where I do my data base work then pass a model to the pages to generate HTML. However, if someone issues a HEAD, then the pages still run but the local data work never happened (I have no OnHead() call). I have a few options here. I could handle HEAD myself. I could no-op it, but that'd be a lie.
THOUGHT: I think this behavior is sub-optimal. While GET and POST are distinct and it makes sense to require an OnGet() and OnPost(), I think that HEAD is special. It's basically a GET with a "don't return the body" flag set. So why not have Razor Pages automatically delegate OnHead to OnGet, unless there's an explicit OnHead() declared? I'll file an issue on GitHub because I don't like this behavior and I find it counter-intuitive. I could also register a global IPageFilter to make this work for all my site's pages.
The simplest thing to do is just to delegate the OnHead to to the OnGet handler.
public Task OnHeadAsync(int? id, string path) => OnGetAsync(id, path);
Then double check and test it with HTTPie:
C:\>http --verify=no HEAD https://localhost:5001
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Date: Tue, 13 Mar 2018 03:53:55 GMT
Request-Context: appId=cid-v1:e310025f-88e9-4133-bc15-e775513c67ac
Server: Kestrel
Bonus - Application Map
Since I have AppInsights enabled on both the client and the server, I can see this cool live Application Map. I'll check again in a few days to see if I have fewer errors. You can see where my Podcast Site calls into the backend data service at Simplecast.
I saw a few failures in my call to SimpleCast's API as I was failing to consistently set my API key. Everything in this map can be drilled down into.
Bonus - Web Performance Testing
I figured while I was in the Azure Portal I would also take advantage of the free performance testing. I did a simulated aggressive 250 users beating on the site. Average response time is 1.22 seconds and I was doing over 600 req/second.
I am learning a ton of stuff. I have more things to fix, more improvements to make, and more insights to dig into. I LOVE that it's creating all this work for me because it's giving me a better application/website!
You can get a free Azure account at http://azure.com/free or check out Azure for Startups https://azure.microsoft.com/overview/startups/ and get a bunch of free Azure time. AppInsights works with Node, Docker, Java, ASP.NET, ASP.NET Core, and other platforms. It even supports telemetry in Electron or Windows Apps.
Sponsor: Get the latest JetBrains Rider for debugging third-party .NET code, Smart Step Into, more debugger improvements, C# Interactive, new project wizard, and formatting code in columns.
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
The nice trick is then you can set this on the AppSettings page in Azure so you can point the dev/test and production versions of the web site at different ApplicationInsights instances.
I set this up in the code with a couple of utility classes...
public class ApplicationInsightsConfig
{
public static void Initialize(string name = null, string component = null)
{
var key = ConfigurationManager.AppSettings["APPINSIGHTS_INSTRUMENTATIONKEY"];
if (string.IsNullOrEmpty(key) && !System.Diagnostics.Debugger.IsAttached)
{
TelemetryConfiguration.Active.DisableTelemetry = true;
return;
}
TelemetryConfiguration.Active.TelemetryInitializers.Add(new ApplicationTelemetryInitializer(name, component));
if (!string.IsNullOrEmpty(key))
{
TelemetryConfiguration.Active.InstrumentationKey = key;
}
}
}
and
/// <summary>
/// Creates a <see cref="ITelemetryInitializer" /> using the <see cref="ApplicationContext" /> details
/// </summary>
public class ApplicationTelemetryInitializer : ITelemetryInitializer
{
/// <summary>
/// Creates a new instance of the <see cref="ApplicationTelemetryInitializer" /> class.
/// </summary>
/// <param name="name"></param>
/// <param name="component"></param>
public ApplicationTelemetryInitializer(string name = null, string component = null)
{
Name = name;
Component = component;
}
/// <summary>
/// Get the name for the telemetry (default: ApplicationContext.AppName)
/// </summary>
public string Name { get; }
/// <summary>
/// Get the component for the telemetry
/// </summary>
public string Component { get; }
public void Initialize(ITelemetry telemetry)
{
// NB Might want to take this from the appsettings as AppName is public display
telemetry.Context.Properties["App"] = Name ?? ApplicationContext.AppName;
telemetry.Context.Component.Version = ApplicationContext.Version;
telemetry.Context.Properties["Environment"] = ApplicationContext.Environment;
telemetry.Context.Properties["Component"] = Component;
}
}
For example, if you had any authentication steps in your OnGet() these would not be applied. That shouldn't really be an issue because you're not returning a body (because it's a HEAD request) and you're not making any changes (because it's treated like a GET, not a POST/PUT/DELETE), but it does mean you're relying on other code being well written.
I used to initialize the Instrumentation Key like that. We use App Insights as a target for Serilog. I want to be able to see the logs in develop too. I leave it up to the TelemetryConfigurationFactory to initialize the instrumentation key. One less 'app setting' that I have to deal with. In Azure Web App, I can setting the setting in the settings blade. On premise, I use Octopus Deploy to inject the key into the ApplicationInsights.config file. I try to avoid per-developer configuration file differences. At some point, someone always commits their config by accident. If a dev wants to see stuff in App Insights, they create an instance, set the envrionment variable locally.
The ITelemetryInitializer is a good idea too. Thanks for sharing.
Then I just have a partial view that contains something like this:
@if (Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.Active.InstrumentationKey != "")
{
<script type="text/javascript">
var appInsights = window.appInsights || function (config) {
/* snippet from Insights documentation */
}), t
}({
instrumentationKey: "@Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.Active.InstrumentationKey"
});
window.appInsights = appInsights;
appInsights.trackPageView();
</script>
}
The _insights.cshtml file is then just added to my layout view and included on every page.
Yeah, Application Insights is really cool for small projects, it gives a lot of benefits.
We tried to use it in a complex project with a quite big load, but we found that it affects the performance of the application and extremely expensive.
I will continue using it for personal projects, but for commercial usage, I would choose other services.
Just a minor question. What is the advantage of doing this delegate
public Task OnHeadAsync(int? id, string path) => OnGetAsync(id, path);
as opposed to simply calling OnGetAsync in the body of OnHeadAsync?
Comments are closed.