How to add HttpContext to enrich un-handled exception logs? - asp.net-core

I have setup Serilog to log to MSSql using:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Async(x => x.MSSqlServer(logConntectionString, tableName, LogEventLevel.Warning, autoCreateSqlTable: false, columnOptions: columnOptions))
.CreateLogger();
Additionally I have added added a SerilogMiddleware in the pipeline that successfully adds LogContext from the HttpContext.
In a test controller, I have these 2 test methods:
public class TestController : ControllerBase
{
[HttpGet, Route("test")]
public IActionResult Get() {
try
{
string[] sar = new string[0];
var errorgenerator = sar[2]; // Trigger exception
}
catch (Exception ex)
{
Log.Error(ex, "Caught Exception");
return StatusCode(500, "Custom 500 Error");
}
return Ok();
}
[HttpGet, Route("test2")]
public IActionResult Get2() {
string[] sar = new string[0];
var errorgenerator = sar[2];// Trigger exception
return Ok();
}
}
The first method is not DRY, and so I would like to handle global/uncaught exceptions such as method 2.
What I have from here is:
public class GloablExceptionFilter : ActionFilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var httpContext = context.HttpContext; // This does not appear to have the actual HttpContext
Log.Error(context.Exception, "Unhandled Exception");
}
}
Problem is, my middleware that otherwise worked no longer does.. It does not edit the response body, etc... Further, when I access ExceptionContext's context.HttpContext, it does not contain the actual HttpContext when triggered from inside a controller method such as above.
How do I inject or share HttpContext and or LogContext with this Filter?
If thats not possible, how do I accomplish logging exceptions, while being DRY, and having context when its available?
Update 1: Current Middleware
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddSerilog();
app.UseAuthentication();
// Logging Middleware is just after Authentication, to have access to
// user IsAuthorized, claims, etc..
app.UseMiddleware<SerilogMiddleware>();
app.UseCors("CORSPolicy");
app.UseMvc();
}
In the middleware itself:
public class SerilogMiddleware
{
readonly RequestDelegate _next;
public SerilogMiddleware(RequestDelegate next)
{
if (next == null) throw new ArgumentNullException(nameof(next));
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
// Do logging stuff with Request..
await _next(httpContext);
// Do logging stuff with Response but..
// This point is never reached, when exception is unhandled.
}
}

Based on code snippet you are not catching the exception when you pass the context down the pipeline.
If you do not catch/handle the exception within the middleware then it wont reach your code after calling down stream.
public class SerilogMiddleware {
readonly RequestDelegate _next;
public SerilogMiddleware(RequestDelegate next) {
if (next == null) throw new ArgumentNullException(nameof(next));
_next = next;
}
public async Task Invoke(HttpContext httpContext) {
// Do logging stuff with Request..
try {
await _next(httpContext);
} catch(Exception ex) {
try {
//Do exception specific logging
// if you don't want to rethrow the original exception
// then call return:
// return;
} catch (Exception loggingException) {
//custom
}
// Otherwise re -throw the original exception
throw;
}
// Do logging stuff with Response
}
}
The above will re-throw the original error after logging it so that the other handler in the pipeline will catch it and do the out of the box handling.

Related

How to access ModelState in UseExceptionHandler

I have domain service that throws a custom DomainServiceValidationException for a business validation. I want to globally capture the exception and return in ModelState. My current working solution is using ExceptionFilterAttribute
public class ExceptionHandlerAttribute : ExceptionFilterAttribute
{
private readonly ILogger<ExceptionHandlerAttribute> _logger;
public ExceptionHandlerAttribute(ILogger<ExceptionHandlerAttribute> logger)
{
_logger = logger;
}
public override void OnException(ExceptionContext context)
{
if (context == null || context.ExceptionHandled)
{
return;
}
if(context.Exception is DomainServiceValidationException)
{
context.ModelState.AddModelError("Errors", context.Exception.Message);
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Result = new BadRequestObjectResult(context.ModelState);
}
else
{
handle exception
}
}
}
I want to know if there is any way to access ModelState in UseExceptionHandler middleware
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLifetime)
{
app.UseExceptionHandler(new ExceptionHandlerOptions()
{
ExceptionHandler = async (context) =>
{
var ex = context.Features.Get<IExceptionHandlerFeature>().Error;
// how to access to ModelState here
}
});
}
}
Shorten answer:
No, you cannot access ModelState in UseExceptionHandler middleware.
Explanation:
Firstly you need to know ModelState is only available after Model Binding.
Then Model Binding invokes before Action Filters and after Resource Filters(See Figure 1). But Middleware invokes before Filters(See Figure 2).
Figure 1:
Figure 2:
Reference:
How Filters work
Conclusion:
That is to say, you can not get the ModelState in UseExceptionHandler middleware.
Workaround:
You can only store the ModelState automatically in Filters(Action Filters or Exception Filters or Result Filters), then you can use it within middleware.
app.UseExceptionHandler(new ExceptionHandlerOptions()
{
ExceptionHandler = async (context) =>
{
var ex = context.Features.Get<IExceptionHandlerFeature>().Error;
// how to access to ModelState here
var data = context.Features.Get<ModelStateFeature>()?.ModelState;
}
});
Reference:
How to store ModelState automatically

Generic async try catch on base API - ASP NET CORE

I have created a generic try/catch method on base API on a net core 2.2 project, and I am not sure about perfomance of this generic method. Is this a good way to do it?
This is on base api:
protected async Task<IActionResult> TryReturnOk<TReturn>(Func<Task<TReturn>> function)
{
try
{
var result = await function();
return Ok(result);
}
catch (Exception ex)
{
_fileLogger.LogError(ex.Message);
_fileLogger.LogError(ex.StackTrace);
return BadRequest(ex);
}
}
And it is used on post method in the api-s like:
public async Task<IActionResult> Post([FromBody] LogViewModel log)
{
return await TryReturnOk(() => _writeLogService.WriteLog(log));
}
Instead of cluttering up all your controllers, I would centralized logging to middleware like below.
400 Bad Request should be used when e.g. request model is not valid. When an exception is thrown, 500 is more appropriate.
public class LoggerMiddleware
{
private readonly ILogger _fileLogger;
private readonly RequestDelegate _next;
public LoggerMiddleware(RequestDelegate next, ILogger fileLogger)
{
_next = next;
_fileLogger = fileLogger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next.Invoke(context);
}
catch (Exception ex)
{
_fileLogger.LogError(ex.Message);
_fileLogger.LogError(ex.StackTrace);
context.Response.Clear();
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
}
}
}
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseLoggerMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoggerMiddleware>();
}
}
In Startup#Configure
app.UseLoggerMiddleware()

Exceptions handled in UseExceptionHandler are logged by Application Insights, but they shouldnt

I initialize logging in program.cs:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseApplicationInsights()
.UseStartup<Startup>();
And later I have some global exception handling in Startup.cs:
public static void ConfigureExceptionHandler(this IApplicationBuilder app)
{
app.UseExceptionHandler(appError =>
{
appError.Run(async context =>
{
context.Response.StatusCode = 200;
});
});
}
What I've noticed, code from my ITelemetryProcessor is executed before the one from app.UseExceptionHandler.
As the result, handled exceptions are being logged to Application Insights. How can I prevent it?
Ok so I found a decent workaround. I used custom middleware for changing response codes, so the exception doesn't pop up to insights, as long as it's handled in the middleware. Example:
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
public ExceptionHandlingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (Exception ex)
{
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
}
}
}

Exception handling and redirecting from view component

How can I implement exception handling in my view component?
Wrapping the logic from my action method into try/catch blocks doesn't catch any exceptions thrown within a view component itself, and I don't want the app to stop functioning regardless of any errors. This is what I'm doing so far and trying to accomplish:
Action Method
public IActionResult LoadComments(int id)
{
try
{
return ViewComponent("CardComments", new { id });
}
catch (SqlException e)
{
return RedirectToAction("Error", "Home");
}
}
To reiterate, this does not catch a SqlException that occurs inside the view component itself, and thus it fails to redirect.
View Component
public class CardCommentsViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(int id)
{
try
{
IEnumerable<CardCommentData> comments = await DbHelper.GetCardCommentData(id);
return View(comments);
}
catch (SqlException e)
{
//Redirect from here if possible?
}
}
}
Can I accomplish this from the controller's action method? If not, how can I redirect from the view component itself? I've tried researching this problem and came up empty. Any information would be helpful.
You can try to redirect to another page using HttpContextAccessor.HttpContext.Response.Redirect:
public class CardCommentsViewComponent : ViewComponent
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CardCommentsViewComponent( IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<IViewComponentResult> InvokeAsync(int id)
{
try
{
IEnumerable<CardCommentData> comments = await DbHelper.GetCardCommentData(id);
return View(comments);
}
catch (SqlException e)
{
_httpContextAccessor.HttpContext.Response.Redirect("/About");
return View(new List<CardCommentData>());
}
}
}
Register in DI :
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
But the preferred way is using global exception handler /filter to trace the exception and redirect to related error page :
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-2.2

How do I enrich a Serilog log entry during ASP.NET Core middleware exception unwinding?

I want to log details from HTTP requests with any unhandled exceptions using Serilog (such as the full request path, all HTTP headers, any form fields, etc). So I followed this tutorial to add information from the current HttpContext.Request to a logged Serilog log: https://blog.getseq.net/smart-logging-middleware-for-asp-net-core/
Here is my version of SerilogMiddleware;
/// <summary>This class logs Request Headers of any failed request.</summary>
public class SerilogMiddleware
{
private static readonly ILogger _log = global::Serilog.Log.ForContext<SerilogMiddleware>();
private readonly RequestDelegate next;
public SerilogMiddleware( RequestDelegate next )
{
this.next = next ?? throw new ArgumentNullException( nameof( next ) );
}
public async Task Invoke( HttpContext httpContext )
{
if( httpContext == null ) throw new ArgumentNullException( nameof( httpContext ) );
try
{
await this.next( httpContext );
// TODO: Log certian HTTP 4xx responses?
if( httpContext.Response?.StatusCode >= 500 )
{
GetLogForErrorContext( httpContext ).Warning( _MessageTemplateForHttp500 );
}
}
catch( Exception ex ) when( LogException( httpContext, ex ) )
{
// LogException returns false, so this catch block will never be entered.
}
}
const String _MessageTemplateForException = "Unhandled exception in {RequestResource}";
const String _MessageTemplateForHttp500 = "Handled HTTP 500 in {RequestResource}";
private static Boolean LogException( HttpContext httpContext, Exception ex )
{
GetLogForErrorContext( httpContext ).Error( ex, _MessageTemplateForException );
return false; // return false so the exception is not caught and continues to propagate upwards. (I understand this is cheaper than `throw;` inside catch).
}
private static ILogger GetLogForErrorContext( HttpContext httpContext )
{
HttpRequest req = httpContext.Request;
String resource = "{0} {1}{2} {3}".FormatInvariant( req.Method, req.Path, req.QueryString.ToString(), req.Protocol );
// re: `ForContext`: https://nblumhardt.com/2016/08/context-and-correlation-structured-logging-concepts-in-net-5/
ILogger result = _log
.ForContext( "RequestHeaders" , req.Headers.ToDictionary( h => h.Key, h => h.Value.ToString() /* Returns all values, comma-separated */ ), destructureObjects: true )
.ForContext( "RequestResource", resource )
.ForContext( "ResponseStatus", httpContext.Response?.StatusCode )
;
if( req.HasFormContentType )
result = result.ForContext( "RequestForm", req.Form.ToDictionary( v => v.Key, v => v.Value.ToString() ) );
return result;
}
}
However, I also have Serilog in my IWebHostBuilder code:
IWebHostBuilder webHostBuilder = WebHost
.CreateDefaultBuilder( args )
.ConfigureLogging( (ctx, cfg ) =>
{
cfg.ClearProviders();
cfg.AddSerilog(); // it's unclear if this is required or not
} )
.UseStartup<Startup>()
.UseSerilog();
webHostBuilder.Build().Run();
In short:
This is an ASP.NET Core middleware class that wraps await next( context ) in a try/catch that get an ILogger using Log.ForContext( ... ) to add new properties to the logger (e.g. request path, response code, etc).
Because this code actually invokes ILogger.Error it causes the event to be logged immediately.
But the try/catch lets the exception continue to propagate up the call-stack (by using catch( Exception ex ) when ( LogExceptionThenReturnFalse( httpContext, ex ) ).
...which means Serilog logs the exception and HTTP request again using default enrichment.
I want Serilog to only log the exception once, with my added enrichment. The quick-fix would be to fully catch the exception in my SerilogMiddleware to prevent further propagation, but that means it doesn't hit the Serilog ILogger configured in my IWebHostBuilder. And if I let the exception propagate and don't log it in my middleware then I can't log data from HttpContext.
How do I "attach" information to the current Serilog "context" so that when the exception is eventually caught and logged by the IWebHostBuilder Serilog logger it includes additional HttpContext data?
Best solution i have found so far, and is clearly a hack,
Idea stolen from here - https://blog.datalust.co/smart-logging-middleware-for-asp-net-core/
Add a middleware class to capture the exception and add manually.
// Idea from https://blog.datalust.co/smart-logging-middleware-for-asp-net-core/
public class LogDetailsMiddleware
{
private readonly RequestDelegate _next;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IUserManager _userManager;
private readonly ILogger _logger = Serilog.Log.ForContext<LogDetailsMiddleware>();
public LogDetailsMiddleware(RequestDelegate next, IHttpContextAccessor httpContextAccessor, IUserManager userManager)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
_next = next;
_httpContextAccessor = httpContextAccessor;
_userManager = userManager;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
LogContext.PushProperty("Email", _userManager.CurrentUser.Email);
LogContext.PushProperty("Url", _httpContextAccessor.HttpContext.Request.GetDisplayUrl());
Stopwatch sw = Stopwatch.StartNew();
try
{
await _next(httpContext);
sw.Stop();
}
// Never caught, because `LogException()` returns false.
catch (Exception ex) when (LogException( sw, ex)) { }
}
bool LogException(Stopwatch sw, Exception ex)
{
sw.Stop();
_logger.Error(ex, "An unhandled exception has occurred while executing the request.");
return false;
}
}
We are logging our requests with HttpClientFactory
services.AddHttpClient("clientWithLogger")
.AddHttpMessageHandler<HttpClientLoggingHandler>();
And our HttpClientLoggingHandler
public class HttpClientLoggingHandler : DelegatingHandler
{
private readonly ILogger<HttpClientLoggingHandler> _logger;
public HttpClientLoggingHandler(ILogger<HttpClientLoggingHandler> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var sw = Stopwatch.StartNew();
_logger.LogInformation("Starting request to '{requestUri}'", request.RequestUri);
var response = await base.SendAsync(request, cancellationToken);
sw.Stop();
_logger.LogInformation("Finished request to '{requestUri}' in {elapsedMilliseconds}ms, response: {response}",
request.RequestUri, sw.ElapsedMilliseconds, await response.Content.ReadAsStringAsync());
return response;
}
}
Then we can simply use HttpClientFactory
public class DeviceDetector
{
private readonly IHttpClientFactory _httpClientFactory;
private const string LicenceKey = "XXX";
private const string Domain = "https://xxx/api/v1/";
public DeviceDetector(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<Device> DetectDevice(string userAgent)
{
var url = $"{Domain}{LicenceKey}";
var result = await _httpClientFactory.CreateClient("clientWithLogger").GetStringAsync(url);
return Newtonsoft.Json.JsonConvert.DeserializeObject<Device>(result);
}
}
This way we use the normal ILogger, which is Serilog behind the scenes, and have total control over what is logging and when.
Edit
If you just want to log the errors then the logic can be easily added
public class HttpClientLoggingHandler : DelegatingHandler
{
private readonly ILogger<HttpClientLoggingHandler> _logger;
public HttpClientLoggingHandler(ILogger<HttpClientLoggingHandler> logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Log error requests
try
{
var response = await base.SendAsync(request, cancellationToken);
if(response.IsSuccessStatusCode)
{
// Success status code
}
else if( (int)response.StatusCode >= 500 )
{
// error 500
}
}
catch( Exception ex ) when( LogException( httpContext, ex ) )
{
// LogException returns false, so this catch block will never be entered.
}
}
}