Asp.net core healthchecks randomly fails with TaskCanceledException or OperationCanceledException - asp.net-core

I've implemented healthchecks in my asp.net core application.
One healthcheck does 2 checks - DbContext connection and custom one that checks NpgsqlConnection.
Everything works fine in over 99% of cases. Occasionally healthcheck fails throwing TaskCanceledException or OperationCanceledException. From my logs I can see that this exceptions are thrown after around 2ms-25ms (so there is no chance any timeout happened).
Important hint:
When I hit healtchecks many times (simple F5 in browser) it throws the exception. Looks like you can't hit /health endpoint before previous healthcheck is completed. If this is the case - why? Even if I put Thread.Sleep(5000); in custom healthcheck (an no DB connection check at all) it will fail if I hit /health endpoint before 5 seconds passes.
QUESTION: Is healtheck somehow 'magically' single-threaded (when you hit that endpoint again, it cancels previous healthcheck invocation)?
Startup.cs ConfigureServices
services
.AddHealthChecks()
.AddCheck<StorageHealthCheck>("ReadOnly Persistance")
.AddDbContextCheck<MyDbContext>("EFCore persistance");
Startup.cs Configure
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors(options => options.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseMiddleware<RequestLogMiddleware>();
app.UseMiddleware<ErrorLoggingMiddleware>();
if (!env.IsProduction())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "V1");
c.SwaggerEndpoint($"/swagger/v2/swagger.json", $"V2");
});
}
app.UseHealthChecks("/health", new HealthCheckOptions()
{
ResponseWriter = WriteResponse
});
app.UseMvc();
StorageHealthCheck.cs
public class StorageHealthCheck : IHealthCheck
{
private readonly IMediator _mediator;
public StorageHealthCheck(IMediator mediator)
{
_mediator = mediator;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
{
var isReadOnlyHealthy = await _mediator.Send(new CheckReadOnlyPersistanceHealthQuery());
return new HealthCheckResult(isReadOnlyHealthy ? HealthStatus.Healthy : HealthStatus.Unhealthy, null);
}
}
CheckReadOnlyPersistanceHealthQueryHandler:
NpgsqlConnectionStringBuilder csb = new NpgsqlConnectionStringBuilder(_connectionString.Value);
string sql = $#"
SELECT * FROM pg_database WHERE datname = '{csb.Database}'";
try
{
using (IDbConnection connection = new NpgsqlConnection(_connectionString.Value))
{
connection.Open();
var stateAfterOpening = connection.State;
if (stateAfterOpening != ConnectionState.Open)
{
return false;
}
connection.Close();
return true;
}
}
catch
{
return false;
}
TaskCanceledException:
System.Threading.Tasks.TaskCanceledException: A task was canceled.
at Npgsql.TaskExtensions.WithCancellation[T](Task`1 task, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnector.ConnectAsync(NpgsqlTimeout timeout, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnector.RawOpen(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnector.Open(NpgsqlTimeout timeout, Boolean async, CancellationToken cancellationToken)
at Npgsql.NpgsqlConnection.<>c__DisplayClass32_0.<<Open>g__OpenLong|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlDatabaseCreator.ExistsAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Diagnostics.HealthChecks.DbContextHealthCheck`1.CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken)
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.CheckHealthAsync(Func`2 predicate, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckMiddleware.InvokeAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Builder.Extensions.MapWhenMiddleware.Invoke(HttpContext context)
OperationCanceledException:
System.OperationCanceledException: The operation was canceled.
at System.Threading.CancellationToken.ThrowOperationCanceledException()
at Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService.CheckHealthAsync(Func`2 predicate, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckMiddleware.InvokeAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Builder.Extensions.MapWhenMiddleware.Invoke(HttpContext context)

I've finally found the answer.
The initial reason is that when the HTTP request is aborted, then httpContext.RequestAborted CancellationToken is triggered, and it throws an exception (OperationCanceledException).
I have a global exception handler in my application, and I have been converting every unhandled exception to a 500 error. Even though the client aborted the request, and never got the 500 response, my logs kept logging this.
The solution I implemented is like that:
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
if (context.RequestAborted.IsCancellationRequested)
{
_logger.LogWarning(ex, "RequestAborted. " + ex.Message);
return;
}
_logger.LogCritical(ex, ex.Message);
await HandleExceptionAsync(context, ex);
throw;
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
var code = HttpStatusCode.InternalServerError; // 500 if unexpected
//if (ex is MyNotFoundException) code = HttpStatusCode.NotFound;
//else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
//else if (ex is MyException) code = HttpStatusCode.BadRequest;
var result = JsonConvert.SerializeObject(new { error = ex.Message });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
hope it helps to somebody.

My best theory, after testing in a large production environment, is that you need to await any writers to the http context output stream in the health check. I was getting this error in a method where I returned a task that was not awaited. Awaiting the task appears to have solved the problem. The nice thing about await is that you could also catch a TaskCancelledException and just eat it.
Example:
// map health checks
endpoints.MapHealthChecks("/health-check", new HealthCheckOptions
{
ResponseWriter = HealthCheckExtensions.WriteJsonResponseAsync,
Predicate = check => check.Name == "default"
});
/// <summary>
/// Write a json health check response
/// </summary>
/// <param name="context">Http context</param>
/// <param name="report">Report</param>
/// <returns>Task</returns>
public static async Task WriteJsonResponseAsync(HttpContext context, HealthReport report)
{
try
{
HealthReportEntry entry = report.Entries.Values.FirstOrDefault();
context.Response.ContentType = "application/json; charset=utf-8";
await JsonSerializer.SerializeAsync(context.Response.Body, entry.Data,entry.Data.GetType());
}
catch (TaskCancelledException)
{
}
}

Related

Hangfire exception forces application to crash

I am getting below exception
Unhandled exception. System.AggregateException: One or more hosted services failed to stop. (The operation was canceled.)
---> System.OperationCanceledException: The operation was canceled.
at System.Threading.CancellationToken.ThrowOperationCanceledException()
at Hangfire.Processing.TaskExtensions.WaitOneAsync(WaitHandle waitHandle, TimeSpan timeout, CancellationToken token)
at Hangfire.Processing.BackgroundDispatcher.WaitAsync(TimeSpan timeout, CancellationToken cancellationToken)
at Hangfire.Server.BackgroundProcessingServer.WaitForShutdownAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.Internal.Host.StopAsync(CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at Microsoft.Extensions.Hosting.Internal.Host.StopAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.WaitForShutdownAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
at Microsoft.AspNetCore.Builder.WebApplication.Run(String url
Asp.net core version 6.0.3
Hangfire.core Version 1.7.28 Hangfire.Aspnetcore version 1.7.28
I have asp.net core application with Hangfire integrated with it
sudo code:
startup.cs
ConfigureServices method
services.AddHangfire();
services.AddHangfireServer();
Configure method
app.UseHangfireDashboard("/hangfire");
Research and analysis till now,
As exception is thrown Microsoft.Extensions.Hosting.Internal.Host.StopAsync I understand that hangfire a hosted service internally and that causes an issue.
possible solution could be if the above service is my custom hosted service i could have wrapped it
under a IHostedService service and could have handled the exception as below eg
public class MyHostedServiceWrapper : IHostedService
{
private readonly ILogger<MyHostedServiceWrapper> _logger;
private readonly IHostedService _innerService;
public MyHostedServiceWrapper(ILogger<MyHostedServiceWrapper> logger, IHostedService innerService)
{
_logger = logger;
_innerService = innerService;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
await _innerService.StartAsync(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while starting the hosted service");
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
try
{
await _innerService.StopAsync(cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while stopping the hosted service");
}
// Return a completed task to indicate that the service has stopped
await Task.CompletedTask;
}
}
But that is not the case.
I just want to skip the above exception without crashing the application

How to use MS Graph in a Action Filter

I am trying to run an action filter any time one of my controllers is called from my front end to check if a token issued by Azure AD is still active. If I run this code in one of my controllers methods directly the code is fine and does as follows:
If token is expired or cache has been cleared, MS Graph SDK returns a redirect to MS Login
If token is valid it runs the MS Graph API and returns the results as normal.
In this ActionFilter below if the scenario is 1 the code just stops and errors out. In visual studio it actually generates the following service error
{Code: generalException
Message: An error occurred sending the request.
}
{Microsoft.Identity.Web.MicrosoftIdentityWebChallengeUserException: IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent.
---> MSAL.NetCore.4.46.0.0.MsalUiRequiredException:
ErrorCode: user_null
Microsoft.Identity.Client.MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.
at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.Silent.SilentRequest.ExecuteAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.Internal.Requests.RequestBase.RunAsync(CancellationToken cancellationToken)
at Microsoft.Identity.Client.ApiConfig.Executors.ClientApplicationBaseExecutor.ExecuteAsync(AcquireTokenCommonParameters commonParameters, AcquireTokenSilentParameters silentParameters, CancellationToken cancellationToken)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForWebAppWithAccountFromCacheAsync(IConfidentialClientApplication application, ClaimsPrincipal claimsPrincipal, IEnumerable`1 scopes, String tenantId, MergedOptions mergedOptions, String userFlow, TokenAcquisitionOptions tokenAcquisitionOptions)
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
StatusCode: 0
ResponseBody:
Headers:
--- End of inner exception stack trace ---
at Microsoft.Identity.Web.TokenAcquisition.GetAuthenticationResultForUserAsync(IEnumerable`1 scopes, String authenticationScheme, String tenantId, String userFlow, ClaimsPrincipal user, TokenAcquisitionOptions tokenAcquisitionOptions)
at Microsoft.Identity.Web.TokenAcquisitionAuthenticationProvider.AuthenticateRequestAsync(HttpRequestMessage request)
at Microsoft.Graph.AuthenticationHandler.SendAsync(HttpRequestMessage httpRequestMessage, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
at Microsoft.Graph.HttpProvider.SendRequestAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)}
So basically the ActionFilter does not automatically redirect the user to the MS Login screen or generate any response to send back to the front end for that matter. It just errors out even with a try and catch statement. So is there any way to do this? My action filter is set as follows:
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Configuration;
using Microsoft.Graph;
using Microsoft.Identity.Client;
using Microsoft.Identity.Web;
using Microsoft.Extensions.DependencyInjection;
using System;
public class CheckTokenFilter : IActionFilter
{
private readonly GraphServiceClient _graphServiceClient;
private readonly ITokenAcquisition _tokenAcquisition;
private readonly string[] initialScopes;
private readonly MicrosoftIdentityConsentAndConditionalAccessHandler _consentHandler;
public CheckTokenFilter(GraphServiceClient graphServiceClient, ITokenAcquisition tokenAcquisition, IConfiguration configuration, MicrosoftIdentityConsentAndConditionalAccessHandler consentHandler)
{
_graphServiceClient = graphServiceClient;
_tokenAcquisition = tokenAcquisition;
initialScopes = configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
this._consentHandler = consentHandler;
}
public void OnActionExecuted(ActionExecutedContext context)
{
//noop
}
public async void OnActionExecuting(ActionExecutingContext context)
{
User currentUser = null;
try
{
currentUser = await _graphServiceClient.Me.Request().GetAsync();
}
// Catch CAE exception from Graph SDK
catch (ServiceException svcex) when (svcex.Message.Contains("Continuous access evaluation resulted in claims challenge"))
{
try
{
Console.WriteLine($"{svcex}");
string claimChallenge = WwwAuthenticateParameters.GetClaimChallengeFromResponseHeaders(svcex.ResponseHeaders);
_consentHandler.ChallengeUser(initialScopes, claimChallenge);
}
catch (Exception ex2)
{
_consentHandler.HandleException(ex2);
}
}
}
}
Current method that is working based on answer below:
public class CheckTokenFilter : IActionFilter
{
private readonly GraphServiceClient _graphServiceClient;
private string SignedInRedirectUri;
public CheckTokenFilter(GraphServiceClient graphServiceClient, IConfiguration configuration)
{
_graphServiceClient = graphServiceClient;
SignedInRedirectUri = configuration["AzureAd:SignedInRedirectUri"];
}
public void OnActionExecuted(ActionExecutedContext context)
{
//noop
}
public async void OnActionExecuting(ActionExecutingContext context)
{
//First we check if token is valid and if not it will generate an error and challenge user causing a redirect which is caught in our program.cs. It sends a 403 error to the front end to load the login page.
User currentUser = null;
try
{
currentUser = await _graphServiceClient.Me.Request().GetAsync();
}
// Catch CAE exception from Graph SDK
catch
{
context.Result = new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme,
new AuthenticationProperties
{
IsPersistent = true,
RedirectUri = SignedInRedirectUri
});
}
}
}
What about using something like:
context.Result = new ChallengeResult(Your Scheme)
I use something similar in my solution, but I use:
[Authorize]
[ServiceFilter(typeof(*******AuthenticationFilter))]
This way if the token is not valid, it will go off and challenge the user before it goes to the filter. The filter can then do additional work once the user is authenticated at a high level. The filter just throws an exception in my case, as it shouldn't get into a bad state.

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.
}
}
}

How to add HttpContext to enrich un-handled exception logs?

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.

WebAPI 2.0 call monitoring

Using ASP.NET WebAPI 2.0 and have a conceptual issue.
Would like to keep a global record of any API that is called by any user/ client and it would be awesome if this was stored in the database.
What would be the best mechanism to accomplish this?
I' using a DelegatingHandler for a long time in several projects which is doing just fine.
public class ApiCallLogHandler : DelegatingHandler {
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
var started = DateTime.UtcNow;
HttpResponseMessage response = null;
Exception baseException = null;
try {
response = await base.SendAsync(request, cancellationToken);
} catch(Exception exception) {
CommonLogger.Logger.LogError(exception);
baseException = exception;
}
try {
var callModel = await GetCallModelAsync(request, response);
if(baseException != null)
callModel.Exception = baseException
callModel.ExecutionTime = (DateTime.UtcNow - started).ToString();
await CommonLogger.Logger.LogApiCallAsync(callModel);
} catch (Exception exception) {
CommonLogger.Logger.LogError(exception);
}
return response;
}
private async Task<ApiCallModel> GetCallModelAsync(HttpRequestMessage request, HttpResponseMessage response) {
// parse request and response and create a model to store in database...
}
}
By this approach you are able to track all requests, exceptions during execution, and even full-response of each API call.
ApiCallModel is just a simple POCO class which you should fill it with your required data from request and response.
CommonLogger.Logger.* is your logging mechanism.
And, you have to register the handler with this snippet:
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
config.MessageHandlers.Add(new ApiCallLogHandler());
}
}