Application Insight for C# WPF
Introduction
Some years ago I wrote a post about using Application Insight in a WPF application to capture telemetry data. Today there is still no official support for WPF (or WinForms or CommandLine) in application Insights and my post remains relevant, however over time the Application Insight libraries (and my understanding of code) has changed and my post does not reflect these changes. As such I have decided to write an updated post to show how to implement an Application Insight client for simple inclusion and use in a project.
The code that I am going to be walking through is part of the VaraniumSharp.Initiator library, you can either check out the code on GitHub or just follow along here. If you only want to use the client and you're not interested in the implementation details grab a copy of the VaraniumSharp.Initiator nuget and jump to the AppInsightClient Usage section.
AppInsightClient
Design Overview
For my previous attempt I developed the Application Insight Helper class to be inject-able so that it would play well with Dependency Injection, however actual use of this code has led me to believe that the client should rather be a static class. This is similar to how logging frameworks work and allow for greater usage flexibility. The AppInsightClient
class guards against attempts to track telemetry data when it has not been initialized, as such it can be left in place during testing without requiring any configuration and without the risk of posting test data to the telemetry server.
The Code
The actual implementation of the client can be found in the AppInsightClient.cs file.
This file contains all of the details that is required to set up and use the Application Insight Telemetry client to post data to Application Insight.
Application Insight Nuget Package
VaraniumSharp.Initiator
makes use of Paket to manage its Nuget dependencies. A reference to the Microsoft.ApplicationInsights
nuget package has been added in the paket.dependencies
and paket.references
files
The package that we use for this implementation is the Microsoft.ApplicationInsights package which contains all the Core functionality for interacting with Application Insights without any of the specialization (like automatic data capture) that is available to other application type like web apps.
Constructor
/// <summary>
/// Static Constructor
/// </summary>
static AppInsightClient()
{
LogInstance = Log.Logger.ForContext("Module", nameof(AppInsightClient));
TrackTelemetry = true;
}
The constructor is straightforward. It captures a contextual instance of the Serilog logger (which is the logger used by VaraniumSharp.Initiator) with the AppInsightClient
set as the property. In this way all logs captured by the AppInsightClient
is easy to trace in our logs.
The TrackTelemetry
property is also set to true so that tracking will be enable by default once the class has been initialized
Initialization Method
/// <summary>
/// Initialize the Telemetry client with appropriate settings
/// </summary>
/// <param name="instrumentationKey">Application Insight instrumentation key</param>
/// <param name="userKey">Key that uniquely identify the user</param>
public static async Task InitializeAsync(string instrumentationKey, string userKey)
{
try
{
await StartupLock.WaitAsync();
if (IsInitialized)
{
LogInstance.Warning("Client can only be initialized once");
return;
}
_telemetryClient = new TelemetryClient
{
InstrumentationKey = instrumentationKey
};
_telemetryClient.Context.User.AuthenticatedUserId = userKey;
_telemetryClient.Context.Session.Id = Guid.NewGuid().ToString();
_telemetryClient.Context.Device.OperatingSystem = GetWindowsFriendlyName();
_telemetryClient.Context.Device.Model = GetDeviceModel();
_telemetryClient.Context.Device.OemName = GetDeviceManufacturer();
_telemetryClient.Context.Component.Version = GetComponentVersion();
IsInitialized = true;
}
finally
{
StartupLock.Release();
}
}
To pass data to the Application Insight servers the TelemetryClient
that is provided by the Application Insight library will be used. The initialization method will prevent multiple initialization attempts by checking and setting the IsInitialized
variable. It also ensures that multiple threads calling the methods does not cause multiple instances of the TelemetryClient
to be instantiated by making use of a SemaphoreSlim
called StartupLock
to lock the initialization method to a single thread.
The InitializeAsync
method takes the InstrumentationKey, which is acquired from the Azure portal as well as a userKey
which is a unique value used to identify the user in Application Insights. How this key is defined or stored is outside the scope of this article.
The Session Key uniquely identifies the current application session and as such it is simply a Guid
that is generated the first time that the AppInsightClient
is initialized, this value will change each time the application is restarted and correlates telemetry data to that single run.
The method calls several sub-methods that are used to set certain properties of the TelemetryClient
that will be posted to the Application Insight server and provide some additional details about the user's environment. The details that will be gathered are:
- Operating system friendly name
- Device model
- Device manufacturer
- Application version
The implementation of these sub-methods is trivial and will not be explored in further detail this is left as an exercise for the reader.
Properties
/// <summary>
/// The OS that the TelemetryClient will report
/// </summary>
public static string OperatingSystem => _telemetryClient?.Context.Device.OperatingSystem;
There are multiple Properties
that expose some of the details that the TelemetryClient
has been configured with. All of these Properties
guard against the TelemetryClient
not having been initialized. All the properties are well documented and will not be discussed in further detail as their implementation is trivial.
Tracking Methods
/// <summary>
/// User actions and other events. Used to track user behavior or to monitor performance.
/// </summary>
/// <param name="name">Name of the event</param>
/// <param name="properties">Dictionary of event properties</param>
/// <param name="metrics">Dictionary of event metrics</param>
public static void TrackEvent(string name, IDictionary<string, string> properties = null,
IDictionary<string, double> metrics = null)
{
if (TelemetryCanBePosted())
{
_telemetryClient.TrackEvent(name, properties, metrics);
}
}
The AppInsightClient
provides pass-through methods for all tracking methods that are exposed by the TelemetryClient
. The only thing that is special about these methods is that all calls to the TelemetryClient
is wrapped with a statement to check if data can be posted to the Application Insights servers.
/// <summary>
/// Check if we can post Telemetry data.
/// This method checks if the client has been initialized and if posting of telemetry data is allowed
/// </summary>
/// <returns>True - Telemetry data can be posted</returns>
private static bool TelemetryCanBePosted()
{
var canPost = true;
if (!IsInitialized)
{
LogInstance.Warning("Cannot track telemetry - Client has not been initialized");
canPost = false;
}
if (!TrackTelemetry)
{
LogInstance.Verbose("Telemetry data posting is disabled");
canPost = false;
}
return canPost;
}
The TelemetryCanBePosted
methods ensures that telemetry data can be posted by checking that the TelemetryClient
instance has been initialized and that the TrackTelemetry
property is true
. In this way it prevents posting data if the user has turned tracking off or if the client has not been initialized yet. In both cases an entry is logged so that it is clear why data is not being posted to the Application Insights server.
Unit Testing
All of the AppInsightClient methods have been unit tested. Most of the tests are fairly straightforward serving only to ensure that a method calls the correct underlying methods.
The test are written with the NUnit test framework with Fluent Assertions for asserting the correctness of statements. It also leverages HttpMock to verify that the TelemetryClient actually makes the calls to the Application Insight servers (The data isn't checked, just the fact that the call is made) as well as Moq for instances where mocking is required.
Test Configuration
To adjust the endpoint to which the TelemetryClient
posts we need to add an ApplicationInsights.config
file to the test project. The content of the file is as follows
<?xml version="1.0" encoding="utf-8" ?>
<ApplicationInsights xmlns="http://schemas.microsoft.com/ApplicationInsights/2013/Settings">
<TelemetryChannel>
<EndpointAddress>http://localhost:8888/v2/track</EndpointAddress>
</TelemetryChannel>
</ApplicationInsights>
Testing
Setup
Because the AppInsightClient
is a static class we need to go to some extra trouble during testing. While we are not overly interested in the exact data that is posted we would like to ensure that setup only occurs once. To do that we use the OneTimeSetup
attribute from NUnit.
[OneTimeSetUp]
public async Task SetupWithCheckThatPostCannotOccurPriorToInitialization()
{
// arrange
_logMock = LoggerFixture.SetupLogCatcher();
// act
AppInsightClient.TrackEvent("BeforeStart");
// assert
_logMock.Verify(t => t.Warning("Cannot track telemetry - Client has not been initialized"), Times.Once);
await AppInsightClient.InitializeAsync(TestKey, TestUserKey);
}
This method includes a test (which is weird) but there is no other practical way to work around it as we do not want to force our tests to execute in a specific order. Because of this we will verify that tracking does not occur before the AppInsightClient
has been set up before setting it up. Should this "test" fail the first test to call this method will fail.
TelemetryClient setup tests
There are multiple tests that ensure that the TelemetryClient
data has been correctly configured. Only a single example will be shown as the rest follow the same pattern.
[Test]
public void TelemetryClientCorrectlyRetrievesOperatingSystem()
{
// arrange
var expectedOs = RetrieveValueFromManagementInformation("Caption", "Win32_OperatingSystem", "Unknown");
// act
// assert
AppInsightClient.OperatingSystem.Should().Be(expectedOs);
}
To test that the OS name has been correctly configured the test pulls the OS name for itself and then compares it to the name that has been configured for the TelemetryClient
. This is required as the OS friendly name will change depending on which machine the test is executed.
Testing TelemetryClient posts
Before testing if the TelemetryClient correctly post data the HttpMock
server has to be configured to capture the call. As all calls go to the same URL a simple helper method has been created to ease setup
private static IHttpServer SetupServer()
{
const string url = "http://localhost:8888";
var httpMock = HttpMockRepository.At(url);
httpMock.Start();
httpMock.Stub(t => t.Post(UrlPath))
.OK();
return httpMock;
}
This method ensure that the HttpMock
libary is listening on the endpoint that we have configured the TelemetryClient
to post to
[Test]
public void TrackingRequestWorksCorrectly()
{
// arrange
var httpMock = SetupServer();
// act
AppInsightClient.TrackRequest("Test", DateTimeOffset.Now, TimeSpan.Zero, "202", true);
AppInsightClient.Flush();
// assert
httpMock.AssertWasCalled(t => t.Post(UrlPath));
}
The test itself is fairly trivial, simply request that the AppInsightClient should post some telemetry data, provide it with dummy data and then ensure that the endpoint was called on the HttpMock
.
Testing Double Initialization
This test ensures that the TelemetryClient
will not be initialized twice. To verify this test we will capture the log entry that states that a second initialization has not occur.
public static Mock<ILogger> SetupLogCatcher()
{
var loggerFixture = new Mock<ILogger>();
loggerFixture.Setup(t => t.ForContext("Module", It.IsAny<string>(), false)).Returns(loggerFixture.Object);
Log.Logger = loggerFixture.Object;
return loggerFixture;
}
The method above is part of the test fixtures and is used to simplify the setup of the ILogger
mock that is used to verify that the log entry has been posted.
[Test]
public async Task InitializationCanOccurOnlyOnce()
{
// arrange
// act
await AppInsightClient.InitializeAsync(TestKey, TestUserKey);
// assert
AppInsightClient.IsInitialized.Should().BeTrue();
_logMock.Verify(t => t.Warning("Client can only be initialized once"), Times.Once);
}
Because the setup method has already initialized the AppInsightClient
a second call to it will log the warning that it has already been initialized.
AppInsightClient Usage
Azure Setup
If you do not already have an Azure account head here to sign up for a free trial otherwise head to the Azure Portal and log in to your account. Next add a new Application Insight instance by heading to Application Insight
in the side panel then selecting Add
Next, enter all relevant information in the creation tab then click Create
and wait for Azure to set up your instance.
Once set up, click on your instance to get its properties
To get your Instrumentation Key scroll down and select Properties
then copy your instrumentation key
Consuming Project Setup
The example project can be viewed in this repository
VaraniumSharp.Initiator Nuget Package
Right-click your project and select the Manage NuGet Packages item
Next enter "VaraniumSharp.Initiator" in the search box, then select the VaraniumSharp.Initiator package and click the Install arrow
This will add the VaraniumSharp.Initiator
package to your project and allow the use of the AppInsightClient
class, if required accept the changes that will be made to your project and accept any license agreements then wait for Visual Studio to install the packages.
Initialize AppInsightClient
Navigate to the App.xaml
backing code
Override the OnStartup
method and set up the AppInsightClient
with the following code
protected override async void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
const string instrumentationKey = "[Redacted]";
const string userKey = "[Temporary Key]";
await AppInsightClient.InitializeAsync(instrumentationKey, userKey);
var currentDomain = AppDomain.CurrentDomain;
currentDomain.UnhandledException += CurrentDomainOnUnhandledException;
currentDomain.ProcessExit += CurrentDomainOnProcessExit;
}
Because the initialize method is asynchronous we need to make the overridden method async.
Do not be tempted to make use of the Wait
method on async Tasks
, this can cause your application to deadlock!
We need to attach handlers for the current AppDomain
so that we can flush the AppInsightClient
data before the application is closed. Failure to do this will result in the data that is still in the TelemetryClient
's memory buffer to be lost
The implementation for the CurrentDomainOnUnhandledException
code is as follows
private static void CurrentDomainOnUnhandledException(object sender, UnhandledExceptionEventArgs unhandledExceptionEventArgs)
{
AppInsightClient.TrackException((Exception)unhandledExceptionEventArgs.ExceptionObject);
AppInsightClient.Flush();
}
with the code to handle the CurrentDomainOnProcessExit
below
private static void CurrentDomainOnProcessExit(object sender, EventArgs eventArgs)
{
AppInsightClient.Flush();
}
Posting Telemetry Data with AppInsightClient
To track telemetry data we can simply invoke the appropriate method on the AppInsightClient
from anywhere in our code. In the example below we add a page view when our main window is displayed
public MainWindow()
{
InitializeComponent();
AppInsightClient.TrackPageView(nameof(MainWindow));
}
Viewing Telemetry Data on Azure
Once you have captured some telemetry data you can view that data in the Azure Application Insight dashboard.
Scroll down to Metrics Explorer then click on one of the graphs to configure it
This will show the metrics that have been posted with the ability to view the individual values that have been posted
There are plenty of ways to visualize your metrics but I won't dig into the details here, instead it is left as an exercise.