Here is an improved version of the Exception enrichment functionality described in the article Enriching your exceptions with information from Episerver. Updates has been made in order to make it more robust in regards to usage of abstract classes that may in some cases throw NotImplementedException, causing the enrichment code to terminate.
public static class ExceptionExtensions
{
private static readonly ILogger _logger = LogManager.GetLogger(typeof(ExceptionExtensions));
private static readonly Injected<HttpContextBase> _context;
private const int MaxValueLength = 150;
private const string Enriched = "OS_Extra";
private const string DoubleFaultMessage = "A double-fault exception occured when attempting to enrich an exception.";
/// <summary>
/// Used to add extra information to exceptions before they are being logged.
/// </summary>
/// <remarks>
/// Be really careful here so your code does not result in exceptions.
/// That would be _very_ bad for the website.
/// </remarks>
public static void Enrich(this Exception e)
{
if (e == null || e.Data.Contains(Enriched))
{
return;
}
try
{
EnrichUnsafe(e);
}
catch (Exception ex)
{
_logger.Log(Level.Error, DoubleFaultMessage, ex);
}
}
private static void EnrichUnsafe(Exception e)
{
e.Data[Enriched] = string.Empty;
HttpContextBase context = _context.Service;
if (context != null)
{
var request = GetValueOrDefault(() => context.Request);
UnsafeEnrichFrom(request, e);
var user = GetValueOrDefault(() => context.User);
UnsafeEnrichFrom(user, e);
var session = GetValueOrDefault(() => context.Session);
UnsafeEnrichFrom(session, e);
var browser = GetValueOrDefault(() => request?.Browser);
UnsafeEnrichFrom(browser, e);
}
else
{
e.Data["HttpContext"] = "null (possibly Optimizely scheduled job?)";
}
UnsafeEnrichWithOptimizelyData(e);
}
private static string TruncateValue(string value, int maxLength)
{
return !string.IsNullOrEmpty(value) && value.Length > maxLength
? value.Substring(0, maxLength) + "..."
: value;
}
private static void UnsafeEnrichFrom(HttpRequestBase request, Exception e)
{
if (request == null)
{
return;
}
e.Data["Request_Url"] = GetValueOrDefault(() => request.Url)?.ToString();
e.Data["Request_HttpMethod"] = GetValueOrDefault(() => request.HttpMethod);
e.Data["Request_UserHostAddress"] = GetValueOrDefault(() => request.UserHostAddress);
e.Data["Request_UserAgent"] = GetValueOrDefault(() => request.UserAgent);
e.Data["Request_UrlReferrer"] = GetValueOrDefault(() => request.UrlReferrer)?.ToString();
e.Data["Request_ContentType"] = GetValueOrDefault(() => request.ContentType);
e.Data["Request_ContentLength"] = GetValueOrDefault(() => request.ContentLength);
e.Data["Request_IsSecureConnection"] = GetValueOrDefault(() => request.IsSecureConnection);
// Headers
var headersBuilder = new StringBuilder();
var headers = GetValueOrDefault(() => request.Headers);
if (headers != null)
{
foreach (string key in headers)
{
var value = headers[key];
value = TruncateValue(value, MaxValueLength);
headersBuilder.AppendLine($" {key}: {value}");
}
}
if (headersBuilder.Length > 0)
{
headersBuilder.Insert(0, Environment.NewLine);
e.Data["Request_Headers"] = headersBuilder.ToString();
}
// Form (not logging actual values due to possible GDPR violation).
var formBuilder = new StringBuilder();
var form = GetValueOrDefault(() => request.Form);
if (form != null)
{
foreach (string key in form)
{
var value = form[key];
value = $"Lenght={value.Length}";
formBuilder.AppendLine($" {key}: {value}");
}
}
if (formBuilder.Length > 0)
{
formBuilder.Insert(0, Environment.NewLine);
e.Data["Request_Form"] = formBuilder.ToString();
}
// Cookies
var cookiesBuilder = new StringBuilder();
var cookies = GetValueOrDefault(() => request.Cookies);
if (cookies != null)
{
foreach (string key in cookies)
{
var value = cookies[key]?.Value;
value = TruncateValue(value, MaxValueLength);
cookiesBuilder.AppendLine($" {key}: {value}");
}
}
if (cookiesBuilder.Length > 0)
{
cookiesBuilder.Insert(0, Environment.NewLine);
e.Data["Request_Cookies"] = cookiesBuilder.ToString();
}
// Files
var filesBuilder = new StringBuilder();
var files = GetValueOrDefault(() => request.Files);
if (files != null)
{
foreach (string key in files)
{
var file = files[key];
if (file != null)
{
filesBuilder.AppendLine($" {key}: {file.FileName} ({file.ContentLength} bytes)");
}
}
}
if (filesBuilder.Length > 0)
{
filesBuilder.Insert(0, Environment.NewLine);
e.Data["Request_Files"] = filesBuilder.ToString();
}
}
private static void UnsafeEnrichFrom(IPrincipal principal, Exception e)
{
var identity = GetValueOrDefault(() => principal?.Identity);
if (identity == null)
{
return;
}
if (identity.IsAuthenticated)
{
e.Data["AuthenticatedUser"] = $"{identity.Name} (AuthenticationType:{identity.AuthenticationType})";
}
else
{
e.Data["AuthenticatedUser"] = $"User not authenticated.";
}
}
private static void UnsafeEnrichFrom(HttpSessionStateBase session, Exception e)
{
if (session == null)
{
return;
}
e.Data["Session_SessionID"] = GetValueOrDefault(() => session.SessionID);
var sessionVars = new StringBuilder();
foreach (string key in session)
{
var value = session[key]?.ToString();
value = TruncateValue(value, MaxValueLength);
sessionVars.AppendLine($" {key}: {value}");
}
if (sessionVars.Length > 0)
{
sessionVars.Insert(0, Environment.NewLine);
e.Data["Session_Variables"] = sessionVars.ToString();
}
}
private static void UnsafeEnrichFrom(HttpBrowserCapabilitiesBase browser, Exception e)
{
if (browser == null)
{
return;
}
e.Data["Browser_Type"] = GetValueOrDefault(() => browser.Type);
e.Data["Browser_Browser"] = GetValueOrDefault(() => browser.Browser);
e.Data["Browser_Version"] = GetValueOrDefault(() => browser.Version);
e.Data["Browser_Platform"] = GetValueOrDefault(() => browser.Platform);
e.Data["Browser_MobileDeviceModel"] = GetValueOrDefault(() => browser.MobileDeviceModel);
}
private static void UnsafeEnrichWithOptimizelyData(Exception e)
{
e.Data["StartPage_ID"] = ContentReference.StartPage?.ID.ToString(CultureInfo.InvariantCulture) ?? string.Empty;
PageData page = null;
try
{
page = ServiceLocator.Current.GetInstance<IPageRouteHelper>()?.Page;
}
catch (Exception ex)
{
e.Data["CurrentPage"] = $"Exception: {ex.Message}.";
}
if (page != null)
{
e.Data["CurrentPage"] = $"{page.PageName} (ID:{page.PageLink}).";
}
IContent content = null;
try
{
content = ServiceLocator.Current.GetInstance<IContentRouteHelper>()?.Content;
}
catch (Exception ex)
{
e.Data["CurrentContent"] = $"Exception: {ex.Message}.";
}
if (content != null)
{
e.Data["CurrentContent"] = $"{content.Name} (ID:{content.ContentLink}).";
}
}
/// <summary>
/// Used to safetly get values from properties.
/// </summary>
/// <typeparam name="T">Type of value to get.</typeparam>
/// <param name="getterFunc">Func that calls the getter. Example: () => request.HttpMethod</param>
/// <returns>The value to get, or the type default if not able to get the value.</returns>
/// <remarks>
/// When using abstract classes such as HttpContextBase there are instances when property getters
/// are not implemented (such as when mocking requests for testing or running scheduled jobs).
/// We need a safe way of getting values that do not cause NotImplementedException to be thrown.
/// </remarks>
private static T GetValueOrDefault<T>(Func<T> getterFunc)
{
try
{
return getterFunc();
}
catch
{
return default;
}
}
}