Skip to content

Commit 9757966

Browse files
authored
Add option to exception handler middleware to suppress logging (#59074)
1 parent d86c5e8 commit 9757966

File tree

7 files changed

+320
-27
lines changed

7 files changed

+320
-27
lines changed

src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.AspNetCore.Http.Features;
54
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Http.Features;
66
using Microsoft.Extensions.Logging;
77

88
namespace Microsoft.AspNetCore.Diagnostics;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Diagnostics;
5+
6+
/// <summary>
7+
/// The result of handling an exception with the <see cref="ExceptionHandlerMiddleware"/>.
8+
/// </summary>
9+
public enum ExceptionHandledType
10+
{
11+
/// <summary>
12+
/// Exception was unhandled.
13+
/// </summary>
14+
Unhandled,
15+
/// <summary>
16+
/// Exception was handled by an <see cref="Diagnostics.IExceptionHandler"/> service instance registered in the DI container.
17+
/// </summary>
18+
ExceptionHandlerService,
19+
/// <summary>
20+
/// Exception was handled by an <see cref="Http.IProblemDetailsService"/> instance registered in the DI container.
21+
/// </summary>
22+
ProblemDetailsService,
23+
/// <summary>
24+
/// Exception was handled by by <see cref="Builder.ExceptionHandlerOptions.ExceptionHandler"/>.
25+
/// </summary>
26+
ExceptionHandlerDelegate,
27+
/// <summary>
28+
/// Exception was handled by by <see cref="Builder.ExceptionHandlerOptions.ExceptionHandlingPath"/>.
29+
/// </summary>
30+
ExceptionHandlingPath
31+
}

src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,12 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
127127
return;
128128
}
129129

130-
DiagnosticsTelemetry.ReportUnhandledException(_logger, context, edi.SourceException);
131-
132130
// We can't do anything if the response has already started, just abort.
133131
if (context.Response.HasStarted)
134132
{
135133
_logger.ResponseStartedErrorHandler();
136134

135+
DiagnosticsTelemetry.ReportUnhandledException(_logger, context, edi.SourceException);
137136
_metrics.RequestException(exceptionName, ExceptionResult.Skipped, handler: null);
138137
edi.Throw();
139138
}
@@ -168,52 +167,97 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
168167
context.Response.StatusCode = _options.StatusCodeSelector?.Invoke(edi.SourceException) ?? DefaultStatusCode;
169168
context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response);
170169

171-
string? handler = null;
172-
var handled = false;
170+
string? handlerTag = null;
171+
var result = ExceptionHandledType.Unhandled;
173172
foreach (var exceptionHandler in _exceptionHandlers)
174173
{
175-
handled = await exceptionHandler.TryHandleAsync(context, edi.SourceException, context.RequestAborted);
176-
if (handled)
174+
if (await exceptionHandler.TryHandleAsync(context, edi.SourceException, context.RequestAborted))
177175
{
178-
handler = exceptionHandler.GetType().FullName;
176+
result = ExceptionHandledType.ExceptionHandlerService;
177+
handlerTag = exceptionHandler.GetType().FullName;
179178
break;
180179
}
181180
}
182181

183-
if (!handled)
182+
if (result == ExceptionHandledType.Unhandled)
184183
{
185184
if (_options.ExceptionHandler is not null)
186185
{
187186
await _options.ExceptionHandler!(context);
187+
188+
// If the response has started, assume exception handler was successful.
189+
if (context.Response.HasStarted)
190+
{
191+
if (_options.ExceptionHandlingPath.HasValue)
192+
{
193+
result = ExceptionHandledType.ExceptionHandlingPath;
194+
handlerTag = _options.ExceptionHandlingPath.Value;
195+
}
196+
else
197+
{
198+
result = ExceptionHandledType.ExceptionHandlerDelegate;
199+
}
200+
}
188201
}
189202
else
190203
{
191-
handled = await _problemDetailsService!.TryWriteAsync(new()
204+
if (await _problemDetailsService!.TryWriteAsync(new()
192205
{
193206
HttpContext = context,
194207
AdditionalMetadata = exceptionHandlerFeature.Endpoint?.Metadata,
195208
ProblemDetails = { Status = context.Response.StatusCode },
196209
Exception = edi.SourceException,
197-
});
198-
if (handled)
210+
}))
199211
{
200-
handler = _problemDetailsService.GetType().FullName;
212+
result = ExceptionHandledType.ProblemDetailsService;
213+
handlerTag = _problemDetailsService.GetType().FullName;
201214
}
202215
}
203216
}
204-
// If the response has already started, assume exception handler was successful.
205-
if (context.Response.HasStarted || handled || _options.StatusCodeSelector != null || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)
217+
218+
if (result != ExceptionHandledType.Unhandled || _options.StatusCodeSelector != null || context.Response.StatusCode != StatusCodes.Status404NotFound || _options.AllowStatusCode404Response)
206219
{
207-
const string eventName = "Microsoft.AspNetCore.Diagnostics.HandledException";
208-
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(eventName))
220+
var suppressDiagnostics = false;
221+
222+
// Customers may prefer to handle the exception and to do their own diagnostics.
223+
// In that case, it can be undesirable for the middleware to log the exception at an error level.
224+
// Run the configured callback to determine if exception diagnostics in the middleware should be suppressed.
225+
if (_options.SuppressDiagnosticsCallback is { } suppressCallback)
226+
{
227+
var suppressDiagnosticsContext = new ExceptionHandlerSuppressDiagnosticsContext
228+
{
229+
HttpContext = context,
230+
Exception = edi.SourceException,
231+
ExceptionHandledBy = result
232+
};
233+
suppressDiagnostics = suppressCallback(suppressDiagnosticsContext);
234+
}
235+
else
236+
{
237+
// Default behavior is to suppress diagnostics if the exception was handled by an IExceptionHandler service instance.
238+
suppressDiagnostics = result == ExceptionHandledType.ExceptionHandlerService;
239+
}
240+
241+
if (!suppressDiagnostics)
209242
{
210-
WriteDiagnosticEvent(_diagnosticListener, eventName, new { httpContext = context, exception = edi.SourceException });
243+
// Note: Microsoft.AspNetCore.Diagnostics.HandledException is used by AppInsights to log errors.
244+
// The diagnostics event is run together with standard exception logging.
245+
const string eventName = "Microsoft.AspNetCore.Diagnostics.HandledException";
246+
if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(eventName))
247+
{
248+
WriteDiagnosticEvent(_diagnosticListener, eventName, new { httpContext = context, exception = edi.SourceException });
249+
}
250+
251+
DiagnosticsTelemetry.ReportUnhandledException(_logger, context, edi.SourceException);
211252
}
212253

213-
_metrics.RequestException(exceptionName, ExceptionResult.Handled, handler);
254+
_metrics.RequestException(exceptionName, ExceptionResult.Handled, handlerTag);
214255
return;
215256
}
216257

258+
// Exception is unhandled. Record diagnostics for the unhandled exception before it is wrapped.
259+
DiagnosticsTelemetry.ReportUnhandledException(_logger, context, edi.SourceException);
260+
217261
edi = ExceptionDispatchInfo.Capture(new InvalidOperationException($"The exception handler configured on {nameof(ExceptionHandlerOptions)} produced a 404 status response. " +
218262
$"This {nameof(InvalidOperationException)} containing the original exception was thrown since this is often due to a misconfigured {nameof(ExceptionHandlerOptions.ExceptionHandlingPath)}. " +
219263
$"If the exception handler is expected to return 404 status responses then set {nameof(ExceptionHandlerOptions.AllowStatusCode404Response)} to true.", edi.SourceException));
@@ -222,6 +266,9 @@ private async Task HandleException(HttpContext context, ExceptionDispatchInfo ed
222266
{
223267
// Suppress secondary exceptions, re-throw the original.
224268
_logger.ErrorHandlerException(ex2);
269+
270+
// There was an error handling the exception. Log original unhandled exception.
271+
DiagnosticsTelemetry.ReportUnhandledException(_logger, context, edi.SourceException);
225272
}
226273
finally
227274
{

src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.Tracing;
45
using Microsoft.AspNetCore.Diagnostics;
56
using Microsoft.AspNetCore.Http;
7+
using Microsoft.Extensions.Logging;
68

79
namespace Microsoft.AspNetCore.Builder;
810

@@ -40,10 +42,38 @@ public class ExceptionHandlerOptions
4042
public bool AllowStatusCode404Response { get; set; }
4143

4244
/// <summary>
43-
/// Gets or sets a delegate used to map an exception to a http status code.
45+
/// Gets or sets a delegate used to map an exception to an HTTP status code.
4446
/// </summary>
4547
/// <remarks>
4648
/// If <see cref="StatusCodeSelector"/> is <c>null</c>, the default exception status code 500 is used.
4749
/// </remarks>
4850
public Func<Exception, int>? StatusCodeSelector { get; set; }
51+
52+
/// <summary>
53+
/// Gets or sets a callback that can return <see langword="true" /> to suppress diagnostics in <see cref="ExceptionHandlerMiddleware" />.
54+
/// <para>
55+
/// If <see cref="SuppressDiagnosticsCallback"/> is <c>null</c>, the default behavior is to suppress diagnostics if the exception was handled by
56+
/// an <see cref="IExceptionHandler"/> service instance registered in the DI container.
57+
/// To always record diagnostics for handled exceptions, set a callback that returns <see langword="false" />.
58+
/// </para>
59+
/// <para>
60+
/// This callback is only run if the exception was handled by the middleware.
61+
/// Unhandled exceptions and exceptions thrown after the response has started are always logged.
62+
/// </para>
63+
/// <para>
64+
/// Suppressed diagnostics include:
65+
/// </para>
66+
/// <list type="bullet">
67+
/// <item>
68+
/// <description>Logging <c>UnhandledException</c> to <see cref="ILogger"/>.</description>
69+
/// </item>
70+
/// <item>
71+
/// <description>Writing the <c>Microsoft.AspNetCore.Diagnostics.HandledException</c> event to <see cref="EventSource" />.</description>
72+
/// </item>
73+
/// <item>
74+
/// <description>Adding the <c>error.type</c> tag to the <c>http.server.request.duration</c> metric.</description>
75+
/// </item>
76+
/// </list>
77+
/// </summary>
78+
public Func<ExceptionHandlerSuppressDiagnosticsContext, bool>? SuppressDiagnosticsCallback { get; set; }
4979
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Diagnostics;
7+
8+
/// <summary>
9+
/// The context used to determine whether <see cref="ExceptionHandlerMiddleware"/> should record diagnostics for an exception.
10+
/// </summary>
11+
public sealed class ExceptionHandlerSuppressDiagnosticsContext
12+
{
13+
/// <summary>
14+
/// Gets the <see cref="Http.HttpContext"/> of the current request.
15+
/// </summary>
16+
public required HttpContext HttpContext { get; init; }
17+
18+
/// <summary>
19+
/// Gets the <see cref="System.Exception"/> that the exception handler middleware is processing.
20+
/// </summary>
21+
public required Exception Exception { get; init; }
22+
23+
/// <summary>
24+
/// Gets the result of exception handling by <see cref="ExceptionHandlerMiddleware"/>.
25+
/// </summary>
26+
public required ExceptionHandledType ExceptionHandledBy { get; init; }
27+
}
Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.SuppressDiagnosticsCallback.get -> System.Func<Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext!, bool>?
3+
Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.SuppressDiagnosticsCallback.set -> void
24
Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.get -> bool
35
Microsoft.AspNetCore.Builder.StatusCodePagesOptions.CreateScopeForErrors.set -> void
4-
static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! pathFormat, bool createScopeForErrors, string? queryFormat = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
6+
Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
7+
Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.ExceptionHandlerDelegate = 3 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
8+
Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.ExceptionHandlerService = 1 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
9+
Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.ExceptionHandlingPath = 4 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
10+
Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.ProblemDetailsService = 2 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
11+
Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.Unhandled = 0 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
12+
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext
13+
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.Exception.get -> System.Exception!
14+
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.Exception.init -> void
15+
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.ExceptionHandledBy.get -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
16+
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.ExceptionHandledBy.init -> void
17+
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.ExceptionHandlerSuppressDiagnosticsContext() -> void
18+
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
19+
Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.HttpContext.init -> void
20+
static Microsoft.AspNetCore.Builder.StatusCodePagesExtensions.UseStatusCodePagesWithReExecute(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, string! pathFormat, bool createScopeForErrors, string? queryFormat = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!

0 commit comments

Comments
 (0)