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;
}
}
}
Related
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()
I'm trying to gracefully terminate a ASP.Net Core 3.1 service (which will run in Kubernetes). When Kubernetes stops a service, it will send a SIGTERM event to the application, at which point I want in-flight requests to complete (which may take several seconds) before terminating... I think I can catch this in a hostedservice, as below, and hence not stop immediately.
The following works, but with a timeout of 5 seconds or longer, I receive an OperationCanceledException. Could anyone shed any light on why I get an OperationCanceledException or how shed any light on an alternative way to delay a SIGTERM event, to allow a graceful shutdown?
public static int Main(string[] args)
{
var logger = NLogBuilder
.ConfigureNLog("nlog.config")
.GetCurrentClassLogger();
try
{
CreateHostBuilder(args)
.ConfigureServices((hostBuilderContext, services) => { services.AddHostedService<LifetimeEventsHostedService>(); })
.Build()
.Run();
return 0;
}
catch (Exception e)
{
logger.Fatal(e, "Stopping due to exception");
return -1;
}
finally
{
LogManager.Shutdown();
}
}
This is the hosted service...
internal class LifetimeEventsHostedService : IHostedService
{
private readonly Microsoft.Extensions.Logging.ILogger _logger;
private readonly IHostApplicationLifetime _appLifetime;
public LifetimeEventsHostedService(
ILogger<LifetimeEventsHostedService> logger,
IHostApplicationLifetime appLifetime)
{
_logger = logger;
_appLifetime = appLifetime;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_appLifetime.ApplicationStarted.Register(OnStarted);
_appLifetime.ApplicationStopping.Register(OnStopping);
_appLifetime.ApplicationStopped.Register(OnStopped);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void OnStarted()
{
_logger.LogInformation("OnStarted has been called.");
// Perform post-startup activities here
}
private void OnStopping()
{
_logger.LogInformation("OnStopping has been called.");
// Perform on-stopping activities here
// This works, but a timeout of 5 seconds or more subsequently causes an OperationCanceledException
Thread.Sleep(5000);
}
private void OnStopped()
{
_logger.LogInformation("OnStopped has been called.");
// Perform post-stopped activities here
}
}
I'm open to alternative approaches to graceful shutdown with ASP.Net Core 3.1, as it stands, I'm using a hosted service.
Within the .Net Core app, I was setting ShutdownTimeout on the webhost, however, setting the ShutdownTimeout on the generic host, does allow me to gracefully wait a number of seconds (more than the default sigterm, which is 5 seconds) prior to shutdown. The hint from #PmanAce helped me work that out.
As such, the following codes allow me to gracefully terminate. One caveat, the Thread.Sleep in LifetimeEventsHostedService must be less than option.ShutdownTimeout.
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostBuilderContext, services) =>
{
services.AddHostedService<LifetimeEventsHostedService>();
services.Configure<HostOptions>(option =>
{
option.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseKestrel();
webBuilder.UseStartup<Startup>();
});
}
The following LifetimeEventsHostedService
public class LifetimeEventsHostedService : IHostedService
{
private readonly IHostApplicationLifetime _hostApplicationLifetime;
public LifetimeEventsHostedService(IHostApplicationLifetime hostApplicationLifetime)
{
_hostApplicationLifetime = hostApplicationLifetime;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
_hostApplicationLifetime.ApplicationStopping.Register(OnStopping);
_hostApplicationLifetime.ApplicationStopped.Register(OnStopped);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private void OnStopped()
{
Console.WriteLine("OnStopped");
}
private void OnStopping()
{
Console.WriteLine("OnStopping");
Console.WriteLine(DateTime.Now.ToLongTimeString());
Thread.Sleep(15000);
Console.WriteLine("Sleep finished");
Console.WriteLine(DateTime.Now.ToLongTimeString());
}
private void OnStarted()
{
Console.WriteLine("OnStarted");
}
}
Previously I could use LoggerFactory but now it is marked as obsolete.
How can I access ILoggingBuilder and create logger instance at the beginning of Mainmethod? I want to use builtin logger not any 3rd party logging library.
To get what you want, you have to edit CreateWebHostBuilder method:
public static IWebHost CreateWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole(); // Or any other provider
}).Build();
Now this method returns the host with configured logging. Now you can get you ILogger-Service from the services of your host:
public static void Main(string[] args)
{
var host = CreateWebHost(args);
try
{
var logger = host.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Called from Main");
host.Run();
}
catch (Exception)
{
// do something
}
}
To use Logging in Startup you can still use ILoggerFactory:
private ILoggerFactory _loggerFactory;
public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
{
Configuration = configuration;
_loggerFactory = loggerFactory;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
try
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
catch (Exception ex)
{
var logger = _loggerFactory.CreateLogger("Startup");
// log
throw;
}
}
Here are additional ressources for logging and app startup in asp.net core 2.2.
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.
}
}
}
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.