Monday 22 May 2017

Storing the AppInsights key in web.config

When working with Application Insights your configuration has a unique key value called the instrumentation key, or more commonly the iKey, which is the unique value that AppInsights uses to associate any telemetry output from your code with the appropriate bucket of data within Azure. It’s just a GUID and is available in the Azure portal. typically this is defined inside the ApplicationInsights.Config file as follows…

<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
  ...
  <InstrumentationKey>00000000-0000-0000-0000-000000000000</InstrumentationKey>
</ApplicationInsights>

This GUID needs to contain the value from within your azure subscription. So far so good. The problem (such as it is) is that for a whole host of reasons I’d rather define this key within the web.config rather than a separate configuration file, as this makes managing keys much easier. If I’m deploying straight to Azure, I’d like to define app settings in Azure so as to override this value for my live environment. Or, if I were using Octopus deploy, I’d like to be able to define a variable and have a different value per environment (Test/Staging/Live and so on). Using the ApplicationInsights.Config file for this makes life a lot more difficult than it needs to be.

Now, I’ve looked at loads of suggestions as to how to overcome this issue – but none (other than the one I’m about to present here) worked for me. Your mileage might vary however. What I wanted was a simple way to write some code in order to read the iKey from my web.config, such that I could roll out my website and have the appropriate key updated for me. In order to do this I had to pull out my trusty copy of .NET Reflector and look at the code. I found a number of posts in the web that suggesting updating the value of TelemetryConfiguration.Active.InstrumentationKey, but regardless of where I set this it didn’t work.

During my spelunking through the code I did find that it’s possible to override the value from the ApplicationInsights.Config file with an environment variable – that being APPINSIGHTS_INSTRUMENTATIONKEY. If this value is defined on the machine you release on then fine, you’re in business. I can see that this is a decent option if you own the hardware that this is running on as someone from IT can login, set the value, and be done with it. But who owns servers these days? I wanted something that was effectively “zero-touch”, and environment variables are not exactly that – nor did I want to write a .ps script that would set this, so I headed out to find another option.

Initialization

In my web project I’d used the defaults for App Insights, which updated my web.config to add the following HTTP module into the processing pipeline…

<system.web>
  <compilation debug="true" targetFramework="4.5.2" />
  <httpRuntime targetFramework="4.5.2" />
  <httpModules>
    <add name="ApplicationInsightsWebTracking"
type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" />
  </httpModules>
</system.web>

Looking at this module, it called TelemetryConfiguration.Active (a static method) that constructs the telemetry configuration and, by default, reads it from the ApplicationInsights.Config file. From there it then reads the .config data and constructs all of the “plug-ins”, initializes the lot, and that then starts emitting trace data. The initialization of the the plug-ins is done with this section of code from the TelemetryConfigurationFactory class…

private static void InitializeComponents(TelemetryConfiguration configuration, TelemetryModules modules)
{
    InitializeComponent(configuration.TelemetryChannel, configuration);
    InitializeComponents(configuration.TelemetryInitializers, configuration);
    InitializeComponents(configuration.TelemetryProcessorChain.TelemetryProcessors, configuration);
    if (modules != null)
    {
        InitializeComponents(modules.Modules, configuration);
    }
}

So, it configures the telemetry channel (where the telemetry goes), then the initializers and so on. I had a thought – what if I were to add a new class into this initialization phase which would read the iKey from web.config, then I’d be able to plug this into the pipeline and I’d be sorted.

The Code

I came up with this trivially simple class…

public class InstrumentationKeyTelemetryInitializer : ITelemetryModule
{
    public void Initialize(TelemetryConfiguration configuration)
    {
        var iKey = ConfigurationManager.AppSettings["ApplicationInsights:iKey"];

        if (!string.IsNullOrEmpty(iKey))
        {
            configuration.InstrumentationKey = iKey;
        }
    }
}

All it does is read from the .config file and, if the ApplicationInsights:iKey value has been defined, that’s used as the instrumentation key. I then updated my web.config to add on this new app setting…

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    ...
  </configSections>
  <appSettings>
    ...
    <add key="ApplicationInsights:iKey" value="4246A9AC-B3E8-473B-B75D-65DE6B2EEED6"/>
  </appSettings>
</configuration>

That’s not a real key by the way, I just made up a GUID. To plug this in I then altered the ApplicationInsights.Config file to add this new class to the TelemetryModules element…

<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
  ...
  <TelemetryModules>
    <Add Type="MSADC.AppInsights.InstrumentationKeyTelemetryInitializer, MSADC.AppInsights"/>
    ... other modules here
  </TelemetryModules>
  ...
  <InstrumentationKey>00000000-0000-0000-0000-000000000000</InstrumentationKey>
</ApplicationInsights>

Note that I have specifically set the instrumentation key to zeros, as I can easily spot that in the debug output if it’s wrong. I then ran the app and everything worked fine. Well, it nearly did. My initializer was called, but after all the others – so there were a few telemetry calls that included the “zero” key which was of no use at all. What I realised was that my telemetry initializer had to be the first in the .config file. With that done, everything did indeed work just as expected. The first call I got from the telemetry logging was this…

Application Insights Telemetry:
{
  "name": "Microsoft.ApplicationInsights.Dev.4246a9acb3e8473bb75d65de6b2eeed6.Message",
  "time": "2017-05-22T14:53:56.3869277Z",
  "iKey": "4246a9ac-b3e8-473b-b75d-65de6b2eeed6",
  "tags": {
    "ai.operation.parentId": "Oezb1HoAPP8=",
    "ai.location.ip": "127.0.0.1",
    "ai.cloud.roleInstance": "DESKTOP-6NDCRK5",
    "ai.operation.name": "GET /",
    "ai.operation.syntheticSource": "SDKTelemetry",
    "ai.application.ver": "1.0.0.0",
    "ai.operation.id": "Oezb1HoAPP8=",
    "ai.internal.sdkVersion": "dotnet:2.2.0-54037"
  },
  "data": {
    "baseType": "MessageData",
    "baseData": {
      "ver": 2,
      "message": "AI: Diagnostic message: Performance counters are unavailable {snip!}",
      "properties": {
        "Tags": "API",
        "DeveloperMode": "true"
      }
    }
  }
}

Voila! I have my key used by AppInsights, and I can now setup replacement keys for staging/live in the appropriate tool (in my case Octopus).

Conclusion

This was a bit harder than I’d expected but I got a solution in the end. I can now use this initializer to allow me to define the AppInsights key in a more convenient spot.

I don’t have a code download for this – please feel free to cut & paste the code above into your project, and update the web.config file as required.