diff --git a/src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs b/src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs
index 36ab75c70ad2..62e29c93d032 100644
--- a/src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs
+++ b/src/Middleware/Diagnostics/src/DiagnosticsTelemetry.cs
@@ -1,8 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Diagnostics;
diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandledType.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandledType.cs
new file mode 100644
index 000000000000..c15ef9061aba
--- /dev/null
+++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandledType.cs
@@ -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;
+
+///
+/// The result of handling an exception with the .
+///
+public enum ExceptionHandledType
+{
+ ///
+ /// Exception was unhandled.
+ ///
+ Unhandled,
+ ///
+ /// Exception was handled by an service instance registered in the DI container.
+ ///
+ ExceptionHandlerService,
+ ///
+ /// Exception was handled by an instance registered in the DI container.
+ ///
+ ProblemDetailsService,
+ ///
+ /// Exception was handled by by .
+ ///
+ ExceptionHandlerDelegate,
+ ///
+ /// Exception was handled by by .
+ ///
+ ExceptionHandlingPath
+}
diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs
index c8361522d1ef..f6f5bed77f60 100644
--- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs
+++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddlewareImpl.cs
@@ -127,13 +127,12 @@ 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.ResponseStartedErrorHandler();
+ DiagnosticsTelemetry.ReportUnhandledException(_logger, context, edi.SourceException);
_metrics.RequestException(exceptionName, ExceptionResult.Skipped, handler: null);
edi.Throw();
}
@@ -168,52 +167,97 @@ 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 = ExceptionHandledType.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 = ExceptionHandledType.ExceptionHandlerService;
+ handlerTag = exceptionHandler.GetType().FullName;
break;
}
}
- if (!handled)
+ if (result == ExceptionHandledType.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 = ExceptionHandledType.ExceptionHandlingPath;
+ handlerTag = _options.ExceptionHandlingPath.Value;
+ }
+ else
+ {
+ result = ExceptionHandledType.ExceptionHandlerDelegate;
+ }
+ }
}
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 = ExceptionHandledType.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 != ExceptionHandledType.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 suppressDiagnostics = false;
+
+ // Customers may prefer to handle the exception and to do their own diagnostics.
+ // 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 exception diagnostics in the middleware should be suppressed.
+ if (_options.SuppressDiagnosticsCallback is { } suppressCallback)
+ {
+ var suppressDiagnosticsContext = new ExceptionHandlerSuppressDiagnosticsContext
+ {
+ HttpContext = context,
+ Exception = edi.SourceException,
+ ExceptionHandledBy = result
+ };
+ suppressDiagnostics = suppressCallback(suppressDiagnosticsContext);
+ }
+ else
+ {
+ // Default behavior is to suppress diagnostics if the exception was handled by an IExceptionHandler service instance.
+ suppressDiagnostics = result == ExceptionHandledType.ExceptionHandlerService;
+ }
+
+ if (!suppressDiagnostics)
{
- WriteDiagnosticEvent(_diagnosticListener, eventName, new { httpContext = context, exception = edi.SourceException });
+ // 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 });
+ }
+
+ DiagnosticsTelemetry.ReportUnhandledException(_logger, context, edi.SourceException);
}
- _metrics.RequestException(exceptionName, ExceptionResult.Handled, handler);
+ _metrics.RequestException(exceptionName, ExceptionResult.Handled, handlerTag);
return;
}
+ // Exception is unhandled. Record diagnostics for the unhandled exception before it is wrapped.
+ DiagnosticsTelemetry.ReportUnhandledException(_logger, context, 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));
@@ -222,6 +266,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.
+ DiagnosticsTelemetry.ReportUnhandledException(_logger, context, edi.SourceException);
}
finally
{
diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs
index f345e19e8514..a38352d93715 100644
--- a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs
+++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerOptions.cs
@@ -1,8 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics.Tracing;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Builder;
@@ -40,10 +42,38 @@ public class ExceptionHandlerOptions
public bool AllowStatusCode404Response { get; set; }
///
- /// 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.
///
///
/// If is null, the default exception status code 500 is used.
///
public Func? StatusCodeSelector { get; set; }
+
+ ///
+ /// Gets or sets a callback that can return to suppress diagnostics in .
+ ///
+ /// If is null, the default behavior is to suppress diagnostics if the exception was handled by
+ /// an service instance registered in the DI container.
+ /// To always record diagnostics for handled exceptions, set a callback that returns .
+ ///
+ ///
+ /// 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.
+ ///
+ ///
+ /// Suppressed diagnostics include:
+ ///
+ ///
+ /// -
+ /// Logging UnhandledException to .
+ ///
+ /// -
+ /// Writing the Microsoft.AspNetCore.Diagnostics.HandledException event to .
+ ///
+ /// -
+ /// Adding the error.type tag to the http.server.request.duration metric.
+ ///
+ ///
+ ///
+ public Func? SuppressDiagnosticsCallback { get; set; }
}
diff --git a/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerSuppressDiagnosticsContext.cs b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerSuppressDiagnosticsContext.cs
new file mode 100644
index 000000000000..2a02bc5f4986
--- /dev/null
+++ b/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerSuppressDiagnosticsContext.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Diagnostics;
+
+///
+/// The context used to determine whether should record diagnostics for an exception.
+///
+public sealed class ExceptionHandlerSuppressDiagnosticsContext
+{
+ ///
+ /// Gets the of the current request.
+ ///
+ public required HttpContext HttpContext { get; init; }
+
+ ///
+ /// Gets the that the exception handler middleware is processing.
+ ///
+ public required Exception Exception { get; init; }
+
+ ///
+ /// Gets the result of exception handling by .
+ ///
+ public required ExceptionHandledType ExceptionHandledBy { get; init; }
+}
diff --git a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt
index 13c61eb5eab2..158b0cf33ead 100644
--- a/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt
+++ b/src/Middleware/Diagnostics/src/PublicAPI.Unshipped.txt
@@ -1,4 +1,20 @@
#nullable enable
+Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.SuppressDiagnosticsCallback.get -> System.Func?
+Microsoft.AspNetCore.Builder.ExceptionHandlerOptions.SuppressDiagnosticsCallback.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!
\ No newline at end of file
+Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
+Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.ExceptionHandlerDelegate = 3 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
+Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.ExceptionHandlerService = 1 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
+Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.ExceptionHandlingPath = 4 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
+Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.ProblemDetailsService = 2 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
+Microsoft.AspNetCore.Diagnostics.ExceptionHandledType.Unhandled = 0 -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
+Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext
+Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.Exception.get -> System.Exception!
+Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.Exception.init -> void
+Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.ExceptionHandledBy.get -> Microsoft.AspNetCore.Diagnostics.ExceptionHandledType
+Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.ExceptionHandledBy.init -> void
+Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.ExceptionHandlerSuppressDiagnosticsContext() -> void
+Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
+Microsoft.AspNetCore.Diagnostics.ExceptionHandlerSuppressDiagnosticsContext.HttpContext.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!
diff --git a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs
index b5513d9cf0e1..eaf419889b72 100644
--- a/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs
+++ b/src/Middleware/Diagnostics/test/UnitTests/ExceptionHandlerMiddlewareTest.cs
@@ -22,6 +22,7 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Moq;
@@ -33,6 +34,7 @@ public class ExceptionHandlerMiddlewareTest : LoggedTest
public async Task ExceptionIsSetOnProblemDetailsContext()
{
// Arrange
+ ExceptionHandlerSuppressDiagnosticsContext suppressContext = null;
using var host = new HostBuilder()
.ConfigureServices(services =>
{
@@ -53,7 +55,14 @@ public async Task ExceptionIsSetOnProblemDetailsContext()
.UseTestServer()
.Configure(app =>
{
- app.UseExceptionHandler();
+ app.UseExceptionHandler(new ExceptionHandlerOptions
+ {
+ SuppressDiagnosticsCallback = context =>
+ {
+ suppressContext = context;
+ return true;
+ }
+ });
app.Run(context =>
{
throw new Exception("Test exception");
@@ -75,12 +84,16 @@ public async Task ExceptionIsSetOnProblemDetailsContext()
var body = await response.Content.ReadFromJsonAsync();
var originalExceptionMessage = ((JsonElement)body.Extensions["OriginalExceptionMessage"]).GetString();
Assert.Equal("Test exception", originalExceptionMessage);
+
+ Assert.IsType(suppressContext.Exception);
+ Assert.Equal(ExceptionHandledType.ProblemDetailsService, suppressContext.ExceptionHandledBy);
}
[Fact]
public async Task Invoke_ExceptionThrownResultsInClearedRouteValuesAndEndpoint()
{
// Arrange
+ var sink = new TestSink();
var httpContext = CreateHttpContext();
httpContext.SetEndpoint(new Endpoint((_) => Task.CompletedTask, new EndpointMetadataCollection(), "Test"));
httpContext.Request.RouteValues["John"] = "Doe";
@@ -92,10 +105,48 @@ public async Task Invoke_ExceptionThrownResultsInClearedRouteValuesAndEndpoint()
Assert.Null(context.GetEndpoint());
return Task.CompletedTask;
});
- var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor);
+ var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, loggerFactory: new TestLoggerFactory(sink, true));
// Act & Assert
await middleware.Invoke(httpContext);
+
+ Assert.Collection(sink.Writes, w => Assert.Equal("UnhandledException", w.EventId.Name));
+ }
+
+ [Theory]
+ [InlineData(ExceptionHandledType.ExceptionHandlerDelegate, false)]
+ [InlineData(ExceptionHandledType.ProblemDetailsService, true)]
+ public async Task Invoke_HasExceptionHandler_SuppressDiagnostics_CallbackRun(ExceptionHandledType suppressResult, bool logged)
+ {
+ // Arrange
+ var sink = new TestSink();
+ var httpContext = CreateHttpContext();
+
+ var optionsAccessor = CreateOptionsAccessor(
+ exceptionHandler: context =>
+ {
+ context.Features.Set(new TestHttpResponseFeature());
+ return Task.CompletedTask;
+ },
+ suppressDiagnosticsCallback: c => c.ExceptionHandledBy == suppressResult);
+ var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, loggerFactory: new TestLoggerFactory(sink, true));
+
+ // Act & Assert
+ await middleware.Invoke(httpContext);
+
+ if (logged)
+ {
+ Assert.Collection(sink.Writes, w => Assert.Equal("UnhandledException", w.EventId.Name));
+ }
+ else
+ {
+ Assert.Empty(sink.Writes);
+ }
+ }
+
+ private sealed class TestHttpResponseFeature : HttpResponseFeature
+ {
+ public override bool HasStarted => true;
}
[Fact]
@@ -126,6 +177,7 @@ public async Task Invoke_ExceptionHandlerCaptureRouteValuesAndEndpoint()
public async Task IExceptionHandlers_CallNextIfNotHandled()
{
// Arrange
+ var sink = new TestSink();
var httpContext = CreateHttpContext();
var optionsAccessor = CreateOptionsAccessor();
@@ -137,7 +189,7 @@ public async Task IExceptionHandlers_CallNextIfNotHandled()
new TestExceptionHandler(true, "3"),
};
- var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers);
+ var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers, loggerFactory: new TestLoggerFactory(sink, true));
// Act & Assert
await middleware.Invoke(httpContext);
@@ -145,6 +197,56 @@ public async Task IExceptionHandlers_CallNextIfNotHandled()
Assert.True(httpContext.Items.ContainsKey("1"));
Assert.True(httpContext.Items.ContainsKey("2"));
Assert.True(httpContext.Items.ContainsKey("3"));
+
+ // IExceptionHandlers handling an exception suppress diagnostics by default.
+ Assert.Empty(sink.Writes);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task IExceptionHandlers_SuppressDiagnostics_TestLogs(bool? suppressDiagnostics)
+ {
+ // Arrange
+ var sink = new TestSink();
+ var httpContext = CreateHttpContext();
+
+ var metricsTagsFeature = new TestHttpMetricsTagsFeature();
+ httpContext.Features.Set(metricsTagsFeature);
+
+ Func suppressDiagnosticsCallback = null;
+ if (suppressDiagnostics != null)
+ {
+ suppressDiagnosticsCallback = c => suppressDiagnostics.Value;
+ }
+
+ var optionsAccessor = CreateOptionsAccessor(suppressDiagnosticsCallback: suppressDiagnosticsCallback);
+
+ var exceptionHandlers = new List
+ {
+ new TestExceptionHandler(true, "1")
+ };
+
+ var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, exceptionHandlers, loggerFactory: new TestLoggerFactory(sink, true));
+
+ // Act & Assert
+ await middleware.Invoke(httpContext);
+
+ Assert.True(httpContext.Items.ContainsKey("1"));
+
+ if (suppressDiagnostics == null || suppressDiagnostics == true)
+ {
+ Assert.Empty(sink.Writes);
+ Assert.Empty(metricsTagsFeature.Tags);
+ }
+ else
+ {
+ Assert.Collection(sink.Writes, w => Assert.Equal("UnhandledException", w.EventId.Name));
+ var errorTag = Assert.Single(metricsTagsFeature.Tags);
+ Assert.Equal("error.type", errorTag.Key);
+ Assert.Equal("System.InvalidOperationException", errorTag.Value);
+ }
}
[Fact]
@@ -445,6 +547,32 @@ public async Task Metrics_ExceptionThrown_Unhandled_Reported()
m => AssertRequestException(m, "System.InvalidOperationException", "unhandled"));
}
+ [Fact]
+ public async Task Metrics_ExceptionThrown_ErrorPathHandled_Reported()
+ {
+ // Arrange
+ var httpContext = CreateHttpContext();
+ var optionsAccessor = CreateOptionsAccessor(
+ exceptionHandler: context =>
+ {
+ context.Features.Set(new TestHttpResponseFeature());
+ return Task.CompletedTask;
+ },
+ exceptionHandlingPath: "/error");
+ var meterFactory = new TestMeterFactory();
+ var middleware = CreateMiddleware(_ => throw new InvalidOperationException(), optionsAccessor, meterFactory: meterFactory);
+ var meter = meterFactory.Meters.Single();
+
+ using var diagnosticsRequestExceptionCollector = new MetricCollector(meterFactory, DiagnosticsMetrics.MeterName, "aspnetcore.diagnostics.exceptions");
+
+ // Act
+ await middleware.Invoke(httpContext);
+
+ // Assert
+ Assert.Collection(diagnosticsRequestExceptionCollector.GetMeasurementSnapshot(),
+ m => AssertRequestException(m, "System.InvalidOperationException", "handled", "/error"));
+ }
+
private static void AssertRequestException(CollectedMeasurement measurement, string exceptionName, string result, string handler = null)
{
Assert.Equal(1, measurement.Value);
@@ -490,7 +618,8 @@ private HttpContext CreateHttpContext()
private IOptions CreateOptionsAccessor(
RequestDelegate exceptionHandler = null,
- string exceptionHandlingPath = null)
+ string exceptionHandlingPath = null,
+ Func suppressDiagnosticsCallback = null)
{
exceptionHandler ??= c => Task.CompletedTask;
var options = new ExceptionHandlerOptions()
@@ -498,6 +627,10 @@ private IOptions CreateOptionsAccessor(
ExceptionHandler = exceptionHandler,
ExceptionHandlingPath = exceptionHandlingPath,
};
+ if (suppressDiagnosticsCallback != null)
+ {
+ options.SuppressDiagnosticsCallback = suppressDiagnosticsCallback;
+ }
var optionsAccessor = Mock.Of>(o => o.Value == options);
return optionsAccessor;
}
@@ -506,14 +639,15 @@ private ExceptionHandlerMiddlewareImpl CreateMiddleware(
RequestDelegate next,
IOptions options,
IEnumerable exceptionHandlers = null,
- IMeterFactory meterFactory = null)
+ IMeterFactory meterFactory = null,
+ ILoggerFactory loggerFactory = null)
{
next ??= c => Task.CompletedTask;
var listener = new DiagnosticListener("Microsoft.AspNetCore");
var middleware = new ExceptionHandlerMiddlewareImpl(
next,
- NullLoggerFactory.Instance,
+ loggerFactory ?? NullLoggerFactory.Instance,
options,
listener,
exceptionHandlers ?? Enumerable.Empty(),
@@ -529,4 +663,12 @@ public object GetService(Type serviceType)
throw new NotImplementedException();
}
}
+
+ private sealed class TestHttpMetricsTagsFeature : IHttpMetricsTagsFeature
+ {
+ public List> TagsList { get; } = new List>();
+
+ public ICollection> Tags => TagsList;
+ public bool MetricsDisabled { get; set; }
+ }
}