Scott Hanselman

Setting up Application Insights took 10 minutes. It created two days of work for me.

March 13, 2018 Comment on this post [10] Posted in ASP.NET | Azure | DotNetCore
Sponsored By

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:

Browser Time is slow in Bangladesh

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.

18.4 secs to load the page in Bangladesh

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.

That's some long load times

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.

Lots of requests from Cloudflare

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.

1 Alert!

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.

scarygraph

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?

Null Reference Exception

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.

An application map that shows all the components, both client and server

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.

38097 successful calls

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.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
March 13, 2018 11:01
This is not well documented/promoted, however, you can use an environment variable
APPINSIGHTS_INSTRUMENTATIONKEY
to configure App Insights. No need to change ApplicationInsights.config per environment. You can check
Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.Active.DisableTelemetry
to see if telemetry is disabled and
Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.Active.InstrumentationKey
to get the current key and render it out in your javascript. The SDK will first check ApplicationInsights.config for the key. If not found, it will check the environment variable and use it.
March 13, 2018 14:39
@Phil

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;
}
}
March 13, 2018 14:54
The HEAD/GET thing came up when we got our site pen tested. We had HEAD enabled, and the security consultant recommended that it should be disabled, because it is not often used (so it is easy to overlook problems) and it is not necessarily handled in the same way as a GET.

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.
March 13, 2018 18:24
This is fantastic. One question I have - do you have one instance of AppInsights for everything? Dev/Production environments? Client/Server? I thought it was good practice to have one for each environment and client/service. Curious to see what you've done here.
March 13, 2018 19:59
@Paul

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.
March 13, 2018 22:52
I use something similar but Insights comes at a cost and I'm not interested in tracking it in non-production environments. So I have an app setting key that contains the instrumentation key which I load up in a small class that wraps ConfigurtionManager.AppSettings so at Application_Start I just call TelemetryConfiguration.Active.InstrumentationKey = AppSettings["InsightsKey"].

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.
March 14, 2018 0:17
Hi Scott,

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.
March 14, 2018 3:15
Rail - Was this years ago or recently? I'm told it's using sampling now and it's scaling to very large sites.
March 15, 2018 19:04
Hi Scott,

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?
March 16, 2018 5:46
I set this up today after reading this article. One issue I seem to be having though is when I look at the client information it shows that all the users are using IE 11. Im pretty sure that is not the case, especially since I am using Chrome. Is anyone else seeing this? I did a quick search but didnt find anything but it may just be something silly that I did, wont be the first time.

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.