This is the third part of the Logging series. For your convenience you can find other parts in the table of contents in Part 1 – Correlations
So we need to handle errors and exceptions. Web jobs are already covered in the previous part so we need to take care of Sitefinity and WebAPI. Let’s begin with the former.
Table of Contents
Sitefinity
There are two paths: WebForms and MVC.
WebForms
This is easy, we only need to handle error:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
using System; using System.Web; using DomainCommons.Loggers; namespace AzureCommons.Filters { public class WebFormsErrorHandler { public static void ApplicationError(object sender, EventArgs e) { var logger = (ILogger)HttpContext.Current.Items[Constants.LoggerItem]; if (logger == null) { return; } logger.Log(LogLevel.Error, $"Application error from {sender}: {e}"); if (HttpContext.Current?.Response?.Headers == null) { return; } if (HttpContext.Current.Response.HeadersWritten) { return; } HttpContext.Current.Response.AddHeader(Constants.CorrelationIdHeader, logger.Correlator.GetCorrelationId()); HttpContext.Current.Response.AddHeader(Constants.CorrelationCounterHeader, logger.Correlator.GetLogicalTime().ToString()); } } } |
We handle the case of headers already sent to the client and add them if possible. Now you need to call this method from Global.asax.cs and we are done.
MVC
This time we need to create another action filter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using System.Web.Mvc; using DomainCommons.Correlations; using DomainCommons.Loggers; namespace AzureCommons.Filters { public class MvcExceptionFilter : IExceptionFilter { private readonly ILogger _logger; private readonly ICorrelator _correlator; public MvcExceptionFilter(ILogger logger, ICorrelator correlator) { _logger = logger; _correlator = correlator; } public void OnException(ExceptionContext filterContext) { _logger.Log(LogLevel.Error, $"Unhandled exception: {filterContext.Exception}"); if (filterContext.HttpContext?.Response?.Headers == null) { return; } if (filterContext.HttpContext.Response.HeadersWritten) { return; } filterContext.HttpContext.Response.AddHeader(Constants.CorrelationIdHeader, _correlator.GetCorrelationId()); filterContext.HttpContext.Response.AddHeader(Constants.CorrelationCounterHeader, _correlator.GetLogicalTime().ToString()); } } } |
The same stuff. You need to register this filter in your DI container and that is all. You could also consider adding HandleErrorAttribute
globally.
WebAPI2
Now comes the tricky part. WebAPI2 actually has three different ways of handling exceptions: attribute, logger and handler. We can implement all of them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System.Net; using System.Net.Http; using System.Web.Http.Filters; using DomainCommons.Correlations; using DomainCommons.Loggers; namespace AzureCommons.Filters { public class WebApiExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext context) { var logger = (ILogger) context.Request.GetDependencyScope().GetService(typeof (ILogger)); logger.Log(LogLevel.Error, $"Unhandled exception: {context.Exception}"); var correlator = (ICorrelator)context.Request.GetDependencyScope().GetService(typeof(ICorrelator)); context.Response = context.Response ?? new HttpResponseMessage(HttpStatusCode.InternalServerError); context.Response.Headers?.Add(Constants.CorrelationIdHeader, correlator.GetCorrelationId()); context.Response.Headers?.Add(Constants.CorrelationCounterHeader, correlator.GetLogicalTime().ToString()); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web.Http.ExceptionHandling; using ConfigurationCommons.Configurations; using DomainCommons.Loggers; namespace AzureCommons.Filters { [RegisterManually] public class WebApiExceptionLogger : IExceptionLogger { public Task LogAsync(ExceptionLoggerContext context, CancellationToken cancellationToken) { var logger = (ILogger)context.Request.GetDependencyScope().GetService(typeof(ILogger)); logger.Log(LogLevel.Error, $"Unhandled exception: {context.Exception}"); return Task.CompletedTask; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Web.Http.ExceptionHandling; using ConfigurationCommons.Configurations; using DomainCommons.Correlations; using DomainCommons.Loggers; namespace AzureCommons.Filters { [RegisterManually] public class WebApiExceptionHandler : IExceptionHandler { public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken) { var logger = (ILogger)context.Request.GetDependencyScope().GetService(typeof(ILogger)); logger.Log(LogLevel.Error, $"Unhandled exception: {context.Exception}"); var correlator = (ICorrelator)context.Request.GetDependencyScope().GetService(typeof(ICorrelator)); context.ExceptionContext.Response = context.ExceptionContext.Response ?? new HttpResponseMessage(HttpStatusCode.InternalServerError); context.ExceptionContext.Response?.Headers?.Add(Constants.CorrelationIdHeader, correlator.GetCorrelationId()); context.ExceptionContext.Response?.Headers?.Add(Constants.CorrelationCounterHeader, correlator.GetLogicalTime().ToString()); return Task.CompletedTask; } } } |
We log everything we can, set headers and creates response if necessary.
You need to register your filters:
1 2 3 |
configuration.Filters.Add(new WebApiExceptionFilterAttribute()); configuration.Services.Replace(typeof(IExceptionHandler), new WebApiExceptionHandler()); configuration.Services.Insert(typeof(IExceptionLogger), 0, new WebApiExceptionLogger()); |
Summary
Now we handle exceptions and errors in our applications. In the next part we will see how to implement logic for passing correlation IDs and logical times to other systems using RestSharp.