From 7b9cfac65aed73e20b54c1feca8e98cf121c2aa5 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 2 Oct 2015 10:43:48 -0700 Subject: [PATCH] Adding telemetry publish for unhandled exceptions to developer exception page and exception handler #180 --- .../DeveloperExceptionPageMiddleware.cs | 10 ++- .../ExceptionHandlerMiddleware.cs | 12 ++- src/Microsoft.AspNet.Diagnostics/project.json | 9 +- .../DeveloperExceptionPageMiddlewareTest.cs | 37 ++++++++- .../ExceptionHandlerTest.cs | 42 ++++++++++ .../TestTelemetryListener.cs | 83 +++++++++++++++++++ .../project.json | 1 + 7 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 test/Microsoft.AspNet.Diagnostics.Tests/TestTelemetryListener.cs diff --git a/src/Microsoft.AspNet.Diagnostics/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Microsoft.AspNet.Diagnostics/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index a73c2ed707..55e28f7dbb 100644 --- a/src/Microsoft.AspNet.Diagnostics/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Microsoft.AspNet.Diagnostics/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; using System.Globalization; using System.IO; using System.Linq; @@ -29,6 +30,7 @@ namespace Microsoft.AspNet.Diagnostics private static readonly bool IsMono = Type.GetType("Mono.Runtime") != null; private readonly ILogger _logger; private readonly IFileProvider _fileProvider; + private readonly TelemetrySource _telemetrySource; /// /// Initializes a new instance of the class @@ -39,7 +41,8 @@ namespace Microsoft.AspNet.Diagnostics RequestDelegate next, ErrorPageOptions options, ILoggerFactory loggerFactory, - IApplicationEnvironment appEnvironment) + IApplicationEnvironment appEnvironment, + TelemetrySource telemetrySource) { if (next == null) { @@ -55,6 +58,7 @@ namespace Microsoft.AspNet.Diagnostics _options = options; _logger = loggerFactory.CreateLogger(); _fileProvider = options.FileProvider ?? new PhysicalFileProvider(appEnvironment.ApplicationBasePath); + _telemetrySource = telemetrySource; } /// @@ -72,6 +76,7 @@ namespace Microsoft.AspNet.Diagnostics catch (Exception ex) { _logger.LogError("An unhandled exception has occurred while executing the request", ex); + if (context.Response.HasStarted) { _logger.LogWarning("The response has already started, the error page middleware will not be executed."); @@ -84,6 +89,9 @@ namespace Microsoft.AspNet.Diagnostics context.Response.StatusCode = 500; await DisplayException(context, ex); + + _telemetrySource.WriteTelemetry("Microsoft.AspNet.Diagnostics.UnhandledException", new { httpContext = context, exception = ex }); + return; } catch (Exception ex2) diff --git a/src/Microsoft.AspNet.Diagnostics/ExceptionHandler/ExceptionHandlerMiddleware.cs b/src/Microsoft.AspNet.Diagnostics/ExceptionHandler/ExceptionHandlerMiddleware.cs index 4508bc6fe3..3fd9c82f51 100644 --- a/src/Microsoft.AspNet.Diagnostics/ExceptionHandler/ExceptionHandlerMiddleware.cs +++ b/src/Microsoft.AspNet.Diagnostics/ExceptionHandler/ExceptionHandlerMiddleware.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics.Tracing; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; @@ -17,8 +18,13 @@ namespace Microsoft.AspNet.Diagnostics private readonly ExceptionHandlerOptions _options; private readonly ILogger _logger; private readonly Func _clearCacheHeadersDelegate; + private readonly TelemetrySource _telemetrySource; - public ExceptionHandlerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, ExceptionHandlerOptions options) + public ExceptionHandlerMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + ExceptionHandlerOptions options, + TelemetrySource telemetrySource) { _next = next; _options = options; @@ -28,6 +34,7 @@ namespace Microsoft.AspNet.Diagnostics _options.ExceptionHandler = _next; } _clearCacheHeadersDelegate = ClearCacheHeaders; + _telemetrySource = telemetrySource; } public async Task Invoke(HttpContext context) @@ -63,6 +70,9 @@ namespace Microsoft.AspNet.Diagnostics context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response); await _options.ExceptionHandler(context); + + _telemetrySource.WriteTelemetry("Microsoft.AspNet.Diagnostics.HandledException", new { httpContext = context, exception = ex }); + // TODO: Optional re-throw? We'll re-throw the original exception by default if the error handler throws. return; } diff --git a/src/Microsoft.AspNet.Diagnostics/project.json b/src/Microsoft.AspNet.Diagnostics/project.json index 944cdc473e..e4ef39f1d0 100644 --- a/src/Microsoft.AspNet.Diagnostics/project.json +++ b/src/Microsoft.AspNet.Diagnostics/project.json @@ -13,10 +13,15 @@ "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", "Microsoft.Extensions.OptionsModel": "1.0.0-*", "Microsoft.Dnx.Compilation.Abstractions": "1.0.0-*", - "Microsoft.Extensions.WebEncoders.Core": "1.0.0-*" + "Microsoft.Extensions.WebEncoders.Core": "1.0.0-*", + "System.Diagnostics.Tracing.Telemetry": "4.0.0-beta-*" }, "frameworks": { - "dnx451": {}, + "dnx451": { + "frameworkAssemblies": { + "System.Runtime": "" + } + }, "dnxcore50": { "dependencies": { "System.Reflection.Extensions": "4.0.1-beta-*" diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/DeveloperExceptionPageMiddlewareTest.cs b/test/Microsoft.AspNet.Diagnostics.Tests/DeveloperExceptionPageMiddlewareTest.cs index 46a35730c7..b685c3c126 100644 --- a/test/Microsoft.AspNet.Diagnostics.Tests/DeveloperExceptionPageMiddlewareTest.cs +++ b/test/Microsoft.AspNet.Diagnostics.Tests/DeveloperExceptionPageMiddlewareTest.cs @@ -3,16 +3,20 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Tracing; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Versioning; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNet.Builder; using Microsoft.AspNet.Diagnostics.Views; using Microsoft.AspNet.FileProviders; +using Microsoft.AspNet.TestHost; using Microsoft.AspNet.Testing; using Microsoft.Dnx.Runtime; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Xunit; @@ -298,7 +302,8 @@ namespace Microsoft.AspNet.Diagnostics (httpContext) => { return Task.FromResult(0); }, errorPageOptions, new LoggerFactory(), - new TestApplicationEnvironment()); + new TestApplicationEnvironment(), + new TelemetryListener("Microsoft.Aspnet")); return middleware; } @@ -471,5 +476,35 @@ namespace Microsoft.AspNet.Diagnostics public IEnumerable ExpectedErrorCode { get; set; } public IEnumerable ExpectedPostErrorCode { get; set; } } + + [Fact] + public async Task UnhandledErrorsWriteToDiagnosticTelemetryWhenUsingExceptionPage() + { + // Arrange + TelemetryListener telemetryListener = null; + var server = TestServer.Create(app => + { + telemetryListener = app.ApplicationServices.GetRequiredService(); + app.UseDeveloperExceptionPage(); + app.Run(context => + { + throw new Exception("Test exception"); + }); + }); + var listener = new TestTelemetryListener(); + telemetryListener.SubscribeWithAdapter(listener); + + // Act + await server.CreateClient().GetAsync("/path"); + + // Assert + Assert.NotNull(listener.EndRequest?.HttpContext); + Assert.Null(listener.HostingUnhandledException?.HttpContext); + Assert.Null(listener.HostingUnhandledException?.Exception); + Assert.NotNull(listener.DiagnosticUnhandledException?.HttpContext); + Assert.NotNull(listener.DiagnosticUnhandledException?.Exception); + Assert.Null(listener.DiagnosticHandledException?.HttpContext); + Assert.Null(listener.DiagnosticHandledException?.Exception); + } } } diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/ExceptionHandlerTest.cs b/test/Microsoft.AspNet.Diagnostics.Tests/ExceptionHandlerTest.cs index 5fb589c95c..71efb76baa 100644 --- a/test/Microsoft.AspNet.Diagnostics.Tests/ExceptionHandlerTest.cs +++ b/test/Microsoft.AspNet.Diagnostics.Tests/ExceptionHandlerTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.Tracing; using System.IO; using System.Linq; using System.Net; @@ -10,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; using Microsoft.AspNet.TestHost; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.AspNet.Diagnostics @@ -321,5 +323,45 @@ namespace Microsoft.AspNet.Diagnostics Assert.Equal("abcdef", values.First()); } } + + [Fact] + public async Task HandledErrorsWriteToDiagnosticTelemetryWhenUsingExceptionHandler() + { + // Arrange + TelemetryListener telemetryListener = null; + + var server = TestServer.Create(app => + { + telemetryListener = app.ApplicationServices.GetRequiredService(); + + app.UseExceptionHandler("/handle-errors"); + app.Map("/handle-errors", (innerAppBuilder) => + { + innerAppBuilder.Run(async (httpContext) => + { + await httpContext.Response.WriteAsync("Handled error in a custom way."); + }); + }); + app.Run(context => + { + throw new Exception("Test exception"); + }); + }); + + var listener = new TestTelemetryListener(); + telemetryListener.SubscribeWithAdapter(listener); + + // Act + await server.CreateClient().GetAsync(string.Empty); + + // Assert + Assert.NotNull(listener.EndRequest?.HttpContext); + Assert.Null(listener.HostingUnhandledException?.HttpContext); + Assert.Null(listener.HostingUnhandledException?.Exception); + Assert.Null(listener.DiagnosticUnhandledException?.HttpContext); + Assert.Null(listener.DiagnosticUnhandledException?.Exception); + Assert.NotNull(listener.DiagnosticHandledException?.HttpContext); + Assert.NotNull(listener.DiagnosticHandledException?.Exception); + } } } diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/TestTelemetryListener.cs b/test/Microsoft.AspNet.Diagnostics.Tests/TestTelemetryListener.cs new file mode 100644 index 0000000000..e92f928d22 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Tests/TestTelemetryListener.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.TelemetryAdapter; + +namespace Microsoft.AspNet.Diagnostics +{ + public class TestTelemetryListener + { + public class OnRequestEventData + { + public IProxyHttpContext HttpContext { get; set; } + } + + public class OnExceptionEventData + { + public IProxyHttpContext HttpContext { get; set; } + public IProxyException Exception { get; set; } + } + + public OnRequestEventData BeginRequest { get; set; } + public OnRequestEventData EndRequest { get; set; } + public OnExceptionEventData HostingUnhandledException { get; set; } + public OnExceptionEventData DiagnosticUnhandledException { get; set; } + public OnExceptionEventData DiagnosticHandledException { get; set; } + + [TelemetryName("Microsoft.AspNet.Hosting.BeginRequest")] + public virtual void OnBeginRequest(IProxyHttpContext httpContext) + { + BeginRequest = new OnRequestEventData() + { + HttpContext = httpContext + }; + } + + [TelemetryName("Microsoft.AspNet.Hosting.EndRequest")] + public virtual void OnEndRequest(IProxyHttpContext httpContext) + { + EndRequest = new OnRequestEventData() + { + HttpContext = httpContext + }; + } + + [TelemetryName("Microsoft.AspNet.Hosting.UnhandledException")] + public virtual void OnHostingUnhandledException(IProxyHttpContext httpContext, IProxyException exception) + { + HostingUnhandledException = new OnExceptionEventData() + { + HttpContext = httpContext, + Exception = exception + }; + } + + [TelemetryName("Microsoft.AspNet.Diagnostics.UnhandledException")] + public virtual void OnDiagnosticUnhandledException(IProxyHttpContext httpContext, IProxyException exception) + { + DiagnosticUnhandledException = new OnExceptionEventData() + { + HttpContext = httpContext, + Exception = exception + }; + } + + [TelemetryName("Microsoft.AspNet.Diagnostics.HandledException")] + public virtual void OnDiagnosticHandledException(IProxyHttpContext httpContext, IProxyException exception) + { + DiagnosticHandledException = new OnExceptionEventData() + { + HttpContext = httpContext, + Exception = exception + }; + } + + public interface IProxyHttpContext + { + } + + public interface IProxyException + { + } + } +} diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/project.json b/test/Microsoft.AspNet.Diagnostics.Tests/project.json index 8ffde45094..d0edefb7a7 100644 --- a/test/Microsoft.AspNet.Diagnostics.Tests/project.json +++ b/test/Microsoft.AspNet.Diagnostics.Tests/project.json @@ -8,6 +8,7 @@ "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.AspNet.Testing": "1.0.0-*", "Microsoft.Extensions.DependencyInjection": "1.0.0-*", + "Microsoft.Extensions.TelemetryAdapter": "1.0.0-*", "xunit.runner.aspnet": "2.0.0-aspnet-*" },