Skip to content

Add option to exception handler middleware to suppress logging #59074

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ public async Task Invoke(HttpContext context)
}
catch (Exception ex)
{
DiagnosticsTelemetry.AddMetricsTags(context, ex);

var exceptionName = ex.GetType().FullName!;

if ((ex is OperationCanceledException || ex is IOException) && context.RequestAborted.IsCancellationRequested)
Expand All @@ -135,7 +137,7 @@ public async Task Invoke(HttpContext context)
return;
}

DiagnosticsTelemetry.ReportUnhandledException(_logger, context, ex);
_logger.UnhandledException(ex);

if (context.Response.HasStarted)
{
Expand Down
5 changes: 1 addition & 4 deletions src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@

using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Diagnostics;

internal static class DiagnosticsTelemetry
{
public static void ReportUnhandledException(ILogger logger, HttpContext context, Exception ex)
public static void AddMetricsTags(HttpContext context, Exception ex)
{
logger.UnhandledException(ex);

if (context.Features.Get<IHttpMetricsTagsFeature>() is { } tagsFeature)
{
// Multiple exception middleware could be registered that have already added the tag.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,13 @@ static async Task Awaited(ExceptionHandlerMiddlewareImpl middleware, HttpContext

private async Task HandleException(HttpContext context, ExceptionDispatchInfo edi)
{
DiagnosticsTelemetry.AddMetricsTags(context, edi.SourceException);

var exceptionName = edi.SourceException.GetType().FullName!;

if ((edi.SourceException is OperationCanceledException || edi.SourceException is IOException) && context.RequestAborted.IsCancellationRequested)
{
// Don't log unhandled exception for aborted request.
_logger.RequestAbortedException();

if (!context.Response.HasStarted)
Expand All @@ -127,11 +130,10 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
return;
}

DiagnosticsTelemetry.ReportUnhandledException(_logger, context, edi.SourceException);

// We can't do anything if the response has already started, just abort.
if (context.Response.HasStarted)
{
_logger.UnhandledException(edi.SourceException);
_logger.ResponseStartedErrorHandler();

_metrics.RequestException(exceptionName, ExceptionResult.Skipped, handler: null);
Expand Down Expand Up @@ -168,52 +170,87 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
context.Response.StatusCode = _options.StatusCodeSelector?.Invoke(edi.SourceException) ?? DefaultStatusCode;
context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response);

string? handler = null;
var handled = false;
string? handlerTag = null;
var result = ExceptionHandlerResult.Unhandled;
foreach (var exceptionHandler in _exceptionHandlers)
{
handled = await exceptionHandler.TryHandleAsync(context, edi.SourceException, context.RequestAborted);
if (handled)
if (await exceptionHandler.TryHandleAsync(context, edi.SourceException, context.RequestAborted))
{
handler = exceptionHandler.GetType().FullName;
result = ExceptionHandlerResult.IExceptionHandler;
handlerTag = exceptionHandler.GetType().FullName;
break;
}
}

if (!handled)
if (result == ExceptionHandlerResult.Unhandled)
{
if (_options.ExceptionHandler is not null)
{
await _options.ExceptionHandler!(context);

// If the response has started, assume exception handler was successful.
if (context.Response.HasStarted)
{
if (_options.ExceptionHandlingPath.HasValue)
{
result = ExceptionHandlerResult.ExceptionHandlingPath;
handlerTag = _options.ExceptionHandlingPath.Value;
}
else
{
result = ExceptionHandlerResult.ExceptionHandler;
}
}
}
else
{
handled = await _problemDetailsService!.TryWriteAsync(new()
if (await _problemDetailsService!.TryWriteAsync(new()
{
HttpContext = context,
AdditionalMetadata = exceptionHandlerFeature.Endpoint?.Metadata,
ProblemDetails = { Status = context.Response.StatusCode },
Exception = edi.SourceException,
});
if (handled)
}))
{
handler = _problemDetailsService.GetType().FullName;
result = ExceptionHandlerResult.ProblemDetailsService;
handlerTag = _problemDetailsService.GetType().FullName;
}
}
}
// If the response has already started, assume exception handler was successful.
if (context.Response.HasStarted || handled || _options.StatusCodeSelector != null || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)

if (result != ExceptionHandlerResult.Unhandled || _options.StatusCodeSelector != null || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)
{
const string eventName = "Microsoft.AspNetCore.Diagnostics.HandledException";
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(eventName))
var suppressLogging = false;

// Customers may prefer to handle the exception and perfer to do their own logging.
// In that case, it can be undesirable for the middleware to log the exception at an error level.
// Run the configured callback to determine if the exception logging in middleware should be suppressed.
if (_options.SuppressLoggingCallback is { } suppressLoggingCallback)
{
WriteDiagnosticEvent(_diagnosticListener, eventName, new { httpContext = context, exception = edi.SourceException });
var logContext = new ExceptionHandlerSuppressLoggingContext { Exception = edi.SourceException, HandlerResult = result };
suppressLogging = suppressLoggingCallback(logContext);
}

_metrics.RequestException(exceptionName, ExceptionResult.Handled, handler);
if (!suppressLogging)
{
// Note: Microsoft.AspNetCore.Diagnostics.HandledException is used by AppInsights to log errors.
// The diagnostics event is run together with standard exception logging.
const string eventName = "Microsoft.AspNetCore.Diagnostics.HandledException";
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(eventName))
{
WriteDiagnosticEvent(_diagnosticListener, eventName, new { httpContext = context, exception = edi.SourceException });
}

_logger.UnhandledException(edi.SourceException);
}

_metrics.RequestException(exceptionName, ExceptionResult.Handled, handlerTag);
return;
}

// Log original unhandled exception before it is wrapped.
_logger.UnhandledException(edi.SourceException);

edi = ExceptionDispatchInfo.Capture(new InvalidOperationException($"The exception handler configured on {nameof(ExceptionHandlerOptions)} produced a 404 status response. " +
$"This {nameof(InvalidOperationException)} containing the original exception was thrown since this is often due to a misconfigured {nameof(ExceptionHandlerOptions.ExceptionHandlingPath)}. " +
$"If the exception handler is expected to return 404 status responses then set {nameof(ExceptionHandlerOptions.AllowStatusCode404Response)} to true.", edi.SourceException));
Expand All @@ -222,6 +259,9 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
{
// Suppress secondary exceptions, re-throw the original.
_logger.ErrorHandlerException(ex2);

// There was an error handling the exception. Log original unhandled exception.
_logger.UnhandledException(edi.SourceException);
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,17 @@ public class ExceptionHandlerOptions
public bool AllowStatusCode404Response { get; set; }

/// <summary>
/// Gets or sets a delegate used to map an exception to a http status code.
/// Gets or sets a delegate used to map an exception to an HTTP status code.
/// </summary>
/// <remarks>
/// If <see cref="StatusCodeSelector"/> is <c>null</c>, the default exception status code 500 is used.
/// </remarks>
public Func<Exception, int>? StatusCodeSelector { get; set; }

/// <summary>
/// Gets or sets a callback that can be used to suppress logging by <see cref="ExceptionHandlerMiddleware" />.
/// This callback is only run if the exception was handled by the middleware.
/// Unhandled exceptions and exceptions thrown after the response has started are always logged.
/// </summary>
public Func<ExceptionHandlerSuppressLoggingContext, bool>? SuppressLoggingCallback { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Diagnostics;

/// <summary>
/// The result of executing <see cref="ExceptionHandlerMiddleware"/>.
/// </summary>
public enum ExceptionHandlerResult
{
/// <summary>
/// Exception was unhandled.
/// </summary>
Unhandled,
/// <summary>
/// Exception was handled by an <see cref="Diagnostics.IExceptionHandler"/> instance registered in the DI container.
/// </summary>
IExceptionHandler,
/// <summary>
/// Exception was handled by an <see cref="Http.IProblemDetailsService"/> instance registered in the DI container.
/// </summary>
ProblemDetailsService,
/// <summary>
/// Exception was handled by by <see cref="Builder.ExceptionHandlerOptions.ExceptionHandler"/>.
/// </summary>
ExceptionHandler,
/// <summary>
/// Exception was handled by by <see cref="Builder.ExceptionHandlerOptions.ExceptionHandlingPath"/>.
/// </summary>
ExceptionHandlingPath
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Diagnostics;

/// <summary>
/// The context used to determine whether exception handler middleware should log an exception.
/// </summary>
public sealed class ExceptionHandlerSuppressLoggingContext
{
/// <summary>
/// Gets the <see cref="System.Exception"/> that the exception handler middleware is processing.
/// </summary>
public required Exception Exception { get; init; }

/// <summary>
/// Gets the result of the exception handler middleware.
/// </summary>
public required ExceptionHandlerResult HandlerResult { get; init; }
}
16 changes: 15 additions & 1 deletion src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
#nullable enable
Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.SuppressLoggingCallback.get -> System.Func<Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressLoggingContext!, bool>?
Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.SuppressLoggingCallback.set -> void
Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.get -> bool
Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.set -> void
static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! pathFormat, bool createScopeForErrors, string? queryFormat = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult.ExceptionHandler = 3 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult.ExceptionHandlingPath = 4 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult.IExceptionHandler = 1 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult.ProblemDetailsService = 2 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult.Unhandled = 0 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressLoggingContext
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressLoggingContext.Exception.get -> System.Exception!
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressLoggingContext.Exception.init -> void
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressLoggingContext.ExceptionHandlerSuppressLoggingContext() -> void
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressLoggingContext.HandlerResult.get -> Microsoft.AspNetCore.Diagnostics.ExceptionHandlerResult
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressLoggingContext.HandlerResult.init -> void
static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! pathFormat, bool createScopeForErrors, string? queryFormat = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
Loading
Loading