Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save RyanThomas73/534027e37679a96ffe5688340c62c56a to your computer and use it in GitHub Desktop.
Save RyanThomas73/534027e37679a96ffe5688340c62c56a to your computer and use it in GitHub Desktop.
AppInsights telemetry initializer to include exception.Data information
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using System;
using System.COllections.Generic;
namespace YourDomain.ApplicationInsights.Extensibility;
/// <summary>
/// Usage Example:
/// <c>builder.Services.AddSingleton<ITelemetryInitializer, ExceptionDataDictionaryTelemetryInitializer>();</c>
/// </summary>
public class ExceptionDataDictionaryTelemetryInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
if (telemetry is not ExceptionTelemetry exceptionTelemetry
|| exceptionTelemetry.Exception is null)
{
return;
}
var exceptionIndex = -1;
Queue<Exception>? exceptionsToCheck = null;
var initialException = exceptionTelemetry.Exception
while (initialException is not null || exceptionsToCheck?.Count > 0)
{
++exceptionIndex;
var nextException = exceptionsToCheck?.Dequeue() ?? exceptionTelemetry.Exception;
initialException = null;
// NOTES: The app insights sdk currently trims the exceptions beyond a fixed count
// using an internal constant (currently set to 10).
//
// In an edge case where the exception tree is particularly large the exception index
// here would not correlate with an exception detail in the telemetry above that constant value.
// See https://github.com/microsoft/ApplicationInsights-dotnet/blob/a99b7d646b903df17cb6c8f6814a8bef5bc934d0/BASE/src/Microsoft.ApplicationInsights/DataContracts/ExceptionTelemetry.cs#L430
//
// If their logic for traversing the exception tree changes, the exception indexes used here could
// become mismatched from the actual exception details as well.
SerializeExceptionData(exceptionIndex, exceptionTelemetry, nextException);
if (nextException is AggregateException aggregateException)
{
exceptionsToCheck ??= new Queue<Exception>();
foreach (var innerException in aggregateException.InnerExceptions)
exceptionsToCheck.Enqueue(innerException);
}
else if (nextException.InnerException is not null)
{
exceptionsToCheck ??= new Queue<Exception>();
exceptionsToCheck.Enqueue(nextException.InnerException);
}
}
}
public static void SerializeExceptionData(
int exceptionIndex,
ExceptionTelemetry exceptionTelemetry,
Exception nextException)
{
if (nextException.Data.Count < 1)
return;
try
{
// NOTE: Replace/customize this logic with your desired behavior for serializing the .Data dictionary
// and adding it to the telemetry properties
// e.g. JsonConvert.SerializeObject(nextException.Data);
IDictionaryEnumerator dataDictionaryEnumerator = nextException.Data.GetEnumerator();
while (dataDictionaryEnumerator.MoveNext())
{
var dataKey = Convert.ToString(dataDictionaryEnumerator.Key, CultureInfo.InvariantCulture);
var dataValue = dataDictionaryEnumerator.Value is null
? "null"
: Convert.ToString(dataDictionaryEnumerator.Value, CultureInfo.InvariantCulture);
exceptionTelemetry.Properties.Add(
FormattableString.Invariant($"Exception[{exceptionIndex}].{dataKey}"),
dataValue
);
}
}
catch {}
}
}
@pharring
Copy link

Optimizations:

  1. Avoid creating new Queue<Exception> in the common case where there is no InnerException and it's not an AggregateException.
  2. You can get Key and Value in one shot (avoid the Data[key] lookup) by manually walking the Data dictionary's enumerator. It's not as convenient as the generic IDictionary<K,V>, but just as fast:
IDictionaryEnumerator dataDictionaryEnumerator = ex.Data.GetEnumerator();
while (dataDictionaryEnumerator.MoveNext())
{
    Console.WriteLine("[{0}] = {1}", dataDictionaryEnumerator.Key, dataDictionaryEnumerator.Value);
}

Consider using System.Convert.ToString(..., CultureInfo.InvariantCulture) on both the key and the value. As well as letting you specify the culture, it also handles null (returns the string "null"). Be aware that some keys/values aren't always "printable".

@RyanThomas73
Copy link
Author

@pharring
Thanks much for the suggested improvements.

Note:
I tested Convert.ToString(null, CultureInfo.InvariantCulture); (targetting .NET 6) and it returns an empty string result not a "null" string.

var test1 = $"A{null}"; // "A"
var test2 = FormattableString.Invariant($"B{null}"); // "B"
var test3 = Convert.ToString(null, CultureInfo.InvariantCulture); // null
object nullObject = null;
var test4 = Convert.ToString(nullObject, CultureInfo.InvariantCulture); // ""

@pharring
Copy link

Oh, you are right: empty string, not "null" (for the object overload of ToString). My mistake. Sorry about that.
However, you can't just add CultureInfo.InvariantCulture on regular object.ToString. It has to be IFormattable (That's the other thing that Convert.ToString does for you -- checks for IFormattable). See for example https://github.com/microsoft/ApplicationInsights-dotnet/blob/main/LOGGING/src/ILogger/ApplicationInsightsLogger.cs#L181

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment