diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c252ea1..b3c055155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## Version 2.6.0-beta3 +- [Implement unhandled exception auto-tracking (500 requests) for MVC 5 and WebAPI 2 applications.](https://github.com/Microsoft/ApplicationInsights-dotnet-server/pull/847) ## Version 2.6.0-beta2 - [Added a max length restriction to values passed in through requests.](https://github.com/Microsoft/ApplicationInsights-dotnet-server/pull/810) diff --git a/Src/Common/StringUtilities.cs b/Src/Common/StringUtilities.cs index e7f685b0b..bda5755d7 100644 --- a/Src/Common/StringUtilities.cs +++ b/Src/Common/StringUtilities.cs @@ -12,7 +12,6 @@ public static class StringUtilities /// public static string EnforceMaxLength(string input, int maxLength) { - Debug.Assert(input != null, $"{nameof(input)} must not be null"); Debug.Assert(maxLength > 0, $"{nameof(maxLength)} must be greater than 0"); if (input != null && input.Length > maxLength) diff --git a/Src/Web/Web.Net45.Tests/MvcExceptionHandlerTests.cs b/Src/Web/Web.Net45.Tests/MvcExceptionHandlerTests.cs new file mode 100644 index 000000000..eb3c24226 --- /dev/null +++ b/Src/Web/Web.Net45.Tests/MvcExceptionHandlerTests.cs @@ -0,0 +1,155 @@ +namespace Microsoft.ApplicationInsights.Web +{ + using System; + using System.Collections.Concurrent; + using System.Linq; + using System.Web.Mvc; + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights.Web.Helpers; + using Microsoft.ApplicationInsights.Web.TestFramework; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MvcExceptionHandlerTests + { + private ConcurrentQueue sentTelemetry; + private TelemetryConfiguration configuration; + + [TestInitialize] + public void TestInit() + { + GlobalFilters.Filters.Clear(); + this.sentTelemetry = new ConcurrentQueue(); + + var stubTelemetryChannel = new StubTelemetryChannel + { + OnSend = t => + { + if (t is ExceptionTelemetry telemetry) + { + this.sentTelemetry.Enqueue(telemetry); + } + } + }; + + this.configuration = new TelemetryConfiguration + { + InstrumentationKey = Guid.NewGuid().ToString(), + TelemetryChannel = stubTelemetryChannel + }; + } + + [TestCleanup] + public void Cleanup() + { + while (this.sentTelemetry.TryDequeue(out var _)) + { + } + + GlobalFilters.Filters.Clear(); + } + + [TestMethod] + public void MvcExceptionFilterIsInjectedAndTracksException() + { + using (var exceptionModule = new ExceptionTrackingTelemetryModule()) + { + exceptionModule.Initialize(this.configuration); + + var mvcExceptionFilters = GlobalFilters.Filters; + Assert.AreEqual(1, mvcExceptionFilters.Count); + + var handleExceptionFilter = (HandleErrorAttribute)mvcExceptionFilters.Single().Instance; + Assert.IsNotNull(handleExceptionFilter); + + var exception = new Exception("test"); + var controllerCtx = HttpModuleHelper.GetFakeControllerContext(isCustomErrorEnabled: true); + handleExceptionFilter.OnException(new ExceptionContext(controllerCtx, exception)); + + Assert.AreEqual(1, this.sentTelemetry.Count); + + var trackedException = this.sentTelemetry.Single(); + Assert.IsNotNull(trackedException); + Assert.AreEqual(exception, trackedException.Exception); + } + } + + [TestMethod] + public void MvcExceptionFilterIsNotInjectedIsInjectionIsDisabled() + { + using (var exceptionModule = new ExceptionTrackingTelemetryModule()) + { + exceptionModule.EnableMvcAndWebApiExceptionAutoTracking = false; + exceptionModule.Initialize(this.configuration); + + Assert.IsFalse(GlobalFilters.Filters.Any()); + } + } + + [TestMethod] + public void MvcExceptionLoggerIsNotInjectedIfAnotherInjectionDetected() + { + GlobalFilters.Filters.Add(new MvcAutoInjectedFilter()); + Assert.AreEqual(1, GlobalFilters.Filters.Count); + + using (var exceptionModule = new ExceptionTrackingTelemetryModule()) + { + exceptionModule.Initialize(this.configuration); + + var filters = GlobalFilters.Filters; + Assert.AreEqual(1, filters.Count); + Assert.IsInstanceOfType(filters.Single().Instance, typeof(MvcAutoInjectedFilter)); + } + } + + [TestMethod] + public void MvcExceptionFilterNoopIfCustomErrorsIsFalse() + { + using (var exceptionModule = new ExceptionTrackingTelemetryModule()) + { + exceptionModule.Initialize(this.configuration); + + var mvcExceptionFilters = GlobalFilters.Filters; + Assert.AreEqual(1, mvcExceptionFilters.Count); + + var handleExceptionFilter = (HandleErrorAttribute)mvcExceptionFilters.Single().Instance; + Assert.IsNotNull(handleExceptionFilter); + + var exception = new Exception("test"); + var controllerCtx = HttpModuleHelper.GetFakeControllerContext(isCustomErrorEnabled: false); + handleExceptionFilter.OnException(new ExceptionContext(controllerCtx, exception)); + + Assert.IsFalse(this.sentTelemetry.Any()); + } + } + + [TestMethod] + public void MvcExceptionFilterNoopIfExceptionIsNull() + { + using (var exceptionModule = new ExceptionTrackingTelemetryModule()) + { + exceptionModule.Initialize(this.configuration); + + var mvcExceptionFilters = GlobalFilters.Filters; + Assert.AreEqual(1, mvcExceptionFilters.Count); + + var handleExceptionFilter = (HandleErrorAttribute)mvcExceptionFilters.Single().Instance; + Assert.IsNotNull(handleExceptionFilter); + + var controllerCtx = HttpModuleHelper.GetFakeControllerContext(isCustomErrorEnabled: true); + var exceptionContext = new ExceptionContext(controllerCtx, new Exception()); + exceptionContext.Exception = null; + handleExceptionFilter.OnException(exceptionContext); + + Assert.IsFalse(this.sentTelemetry.Any()); + } + } + + private class MvcAutoInjectedFilter : HandleErrorAttribute + { + public const bool IsAutoInjected = true; + } + } +} diff --git a/Src/Web/Web.Net45.Tests/Web.Net45.Tests.csproj b/Src/Web/Web.Net45.Tests/Web.Net45.Tests.csproj index fbb287940..66800b5ff 100644 --- a/Src/Web/Web.Net45.Tests/Web.Net45.Tests.csproj +++ b/Src/Web/Web.Net45.Tests/Web.Net45.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -25,15 +25,30 @@ + + ..\..\..\..\packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll + ..\..\..\..\packages\Microsoft.ApplicationInsights.2.6.0-beta2\lib\net45\Microsoft.ApplicationInsights.dll + + ..\..\..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + True + False ..\Web.Shared.Net.Tests\Azure\Emulation\Microsoft.WindowsAzure.ServiceRuntime.dll + + ..\..\..\..\packages\Moq.4.8.2\lib\net45\Moq.dll + + + ..\..\..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + True + + ..\..\..\..\packages\System.Diagnostics.DiagnosticSource.4.4.0\lib\net45\System.Diagnostics.DiagnosticSource.dll @@ -41,9 +56,42 @@ + + ..\..\..\..\packages\Microsoft.AspNet.WebApi.Client.5.2.4\lib\net45\System.Net.Http.Formatting.dll + + + ..\..\..\..\packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + + ..\..\..\..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll + + + ..\..\..\..\packages\Microsoft.AspNet.WebPages.3.2.4\lib\net45\System.Web.Helpers.dll + + + ..\..\..\..\packages\Microsoft.AspNet.WebApi.Core.5.2.4\lib\net45\System.Web.Http.dll + + + ..\..\..\..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.4\lib\net45\System.Web.Http.WebHost.dll + + + ..\..\..\..\packages\Microsoft.AspNet.Mvc.5.2.4\lib\net45\System.Web.Mvc.dll + + + ..\..\..\..\packages\Microsoft.AspNet.Razor.3.2.4\lib\net45\System.Web.Razor.dll + + + ..\..\..\..\packages\Microsoft.AspNet.WebPages.3.2.4\lib\net45\System.Web.WebPages.dll + + + ..\..\..\..\packages\Microsoft.AspNet.WebPages.3.2.4\lib\net45\System.Web.WebPages.Deployment.dll + + + ..\..\..\..\packages\Microsoft.AspNet.WebPages.3.2.4\lib\net45\System.Web.WebPages.Razor.dll + @@ -76,7 +124,9 @@ + + @@ -90,4 +140,4 @@ - + \ No newline at end of file diff --git a/Src/Web/Web.Net45.Tests/WebApiExceptionLoggerTests.cs b/Src/Web/Web.Net45.Tests/WebApiExceptionLoggerTests.cs new file mode 100644 index 000000000..363cf9bdf --- /dev/null +++ b/Src/Web/Web.Net45.Tests/WebApiExceptionLoggerTests.cs @@ -0,0 +1,103 @@ +namespace Microsoft.ApplicationInsights +{ + using System; + using System.Collections.Concurrent; + using System.Linq; + using System.Web.Http; + using System.Web.Http.ExceptionHandling; + using Microsoft.ApplicationInsights.Channel; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility; + using Microsoft.ApplicationInsights.Web; + using Microsoft.ApplicationInsights.Web.TestFramework; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class WebApiExceptionLoggerTests + { + private ConcurrentQueue sentTelemetry; + private TelemetryConfiguration configuration; + + [TestInitialize] + public void TestInit() + { + GlobalConfiguration.Configuration.Services.Clear(typeof(IExceptionLogger)); + this.sentTelemetry = new ConcurrentQueue(); + + var stubTelemetryChannel = new StubTelemetryChannel + { + OnSend = t => + { + if (t is ExceptionTelemetry telemetry) + { + this.sentTelemetry.Enqueue(telemetry); + } + } + }; + + this.configuration = new TelemetryConfiguration + { + InstrumentationKey = Guid.NewGuid().ToString(), + TelemetryChannel = stubTelemetryChannel + }; + } + + [TestCleanup] + public void Cleanup() + { + while (this.sentTelemetry.TryDequeue(out var _)) + { + } + + GlobalConfiguration.Configuration.Services.Clear(typeof(IExceptionLogger)); + } + + [TestMethod] + public void WebApiExceptionLoggerIsInjectedAndTracksException() + { + Assert.IsFalse(GlobalConfiguration.Configuration.Services.GetServices(typeof(IExceptionLogger)).Any()); + + using (var exceptionModule = new ExceptionTrackingTelemetryModule()) + { + exceptionModule.Initialize(this.configuration); + + var webApiExceptionLoggers = GlobalConfiguration.Configuration.Services.GetServices(typeof(IExceptionLogger)).ToList(); + Assert.AreEqual(1, webApiExceptionLoggers.Count); + + var logger = (ExceptionLogger)webApiExceptionLoggers[0]; + Assert.IsNotNull(logger); + + var exception = new Exception("test"); + var exceptionContext = new ExceptionLoggerContext(new ExceptionContext(exception, new ExceptionContextCatchBlock("catch block name", true, false))); + logger.Log(exceptionContext); + + Assert.AreEqual(1, this.sentTelemetry.Count); + + var trackedException = (ExceptionTelemetry)this.sentTelemetry.Single(); + Assert.IsNotNull(trackedException); + Assert.AreEqual(exception, trackedException.Exception); + } + } + + [TestMethod] + public void WebApiExceptionLoggerIsNotInjectedIfAnotherInjectionDetected() + { + GlobalConfiguration.Configuration.Services.Add(typeof(IExceptionLogger), new WebApiAutoInjectedLogger()); + Assert.AreEqual(1, GlobalConfiguration.Configuration.Services.GetServices(typeof(IExceptionLogger)).Count()); + + using (var exceptionModule = new ExceptionTrackingTelemetryModule()) + { + exceptionModule.Initialize(this.configuration); + + var loggers = GlobalConfiguration.Configuration.Services.GetServices(typeof(IExceptionLogger)).ToList(); + Assert.AreEqual(1, loggers.Count); + Assert.IsInstanceOfType(loggers.Single(), typeof(WebApiAutoInjectedLogger)); + } + } + + private class WebApiAutoInjectedLogger : ExceptionLogger + { + public const bool IsAutoInjected = true; + } + } +} diff --git a/Src/Web/Web.Net45.Tests/packages.config b/Src/Web/Web.Net45.Tests/packages.config index deb2123d8..b7e38c687 100644 --- a/Src/Web/Web.Net45.Tests/packages.config +++ b/Src/Web/Web.Net45.Tests/packages.config @@ -1,13 +1,26 @@ - + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/Src/Web/Web.Net45/Web.Net45.csproj b/Src/Web/Web.Net45/Web.Net45.csproj index 0bc1bea65..7b2ad3c66 100644 --- a/Src/Web/Web.Net45/Web.Net45.csproj +++ b/Src/Web/Web.Net45/Web.Net45.csproj @@ -1,4 +1,4 @@ - + @@ -29,6 +29,7 @@ ..\..\..\..\packages\Microsoft.AspNet.TelemetryCorrelation.1.0.0\lib\net45\Microsoft.AspNet.TelemetryCorrelation.dll + ..\..\..\..\packages\System.Diagnostics.DiagnosticSource.4.4.0\lib\net45\System.Diagnostics.DiagnosticSource.dll @@ -70,4 +71,4 @@ - + \ No newline at end of file diff --git a/Src/Web/Web.Shared.Net.Tests/ExceptionTrackingTelemetryModuleTests.cs b/Src/Web/Web.Shared.Net.Tests/ExceptionTrackingTelemetryModuleTests.cs index 3d19d6a01..f7de7e38b 100644 --- a/Src/Web/Web.Shared.Net.Tests/ExceptionTrackingTelemetryModuleTests.cs +++ b/Src/Web/Web.Shared.Net.Tests/ExceptionTrackingTelemetryModuleTests.cs @@ -1,8 +1,9 @@ namespace Microsoft.ApplicationInsights.Web { using System; - using System.Collections.Generic; - + using System.Collections.Concurrent; + using System.Linq; + using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; @@ -17,13 +18,13 @@ public class ExceptionTrackingTelemetryModuleTests { private TelemetryConfiguration configuration; - private IList sendItems; + private ConcurrentQueue sendItems; [TestInitialize] public void TestInit() { - this.sendItems = new List(); - var stubTelemetryChannel = new StubTelemetryChannel { OnSend = item => this.sendItems.Add(item) }; + this.sendItems = new ConcurrentQueue(); + var stubTelemetryChannel = new StubTelemetryChannel { OnSend = item => this.sendItems.Enqueue(item) }; this.configuration = new TelemetryConfiguration { InstrumentationKey = Guid.NewGuid().ToString(), @@ -39,12 +40,14 @@ public void OnErrorTracksExceptionsThatArePresentInHttpContext() platformContext.AddError(exception1); platformContext.AddError(new Exception("2")); - var module = new ExceptionTrackingTelemetryModule(); - module.Initialize(this.configuration); - module.OnError(platformContext); - + using (var module = new ExceptionTrackingTelemetryModule()) + { + module.Initialize(this.configuration); + module.OnError(platformContext); + } + Assert.Equal(2, this.sendItems.Count); - Assert.Equal(exception1, ((ExceptionTelemetry)this.sendItems[0]).Exception); + Assert.Equal(exception1, ((ExceptionTelemetry)this.sendItems.First()).Exception); } [TestMethod] @@ -54,20 +57,23 @@ public void OnErrorSetsSeverityToCriticalForRequestWithStatusCode500() platformContext.Response.StatusCode = 500; platformContext.AddError(new Exception()); - var module = new ExceptionTrackingTelemetryModule(); - module.Initialize(this.configuration); - module.OnError(platformContext); - - Assert.Equal(SeverityLevel.Critical, ((ExceptionTelemetry)this.sendItems[0]).SeverityLevel); + using (var module = new ExceptionTrackingTelemetryModule()) + { + module.Initialize(this.configuration); + module.OnError(platformContext); + } + + Assert.Equal(SeverityLevel.Critical, ((ExceptionTelemetry)this.sendItems.First()).SeverityLevel); } [TestMethod] public void OnErrorDoesNotThrowOnNullContext() { - var module = new ExceptionTrackingTelemetryModule(); - - module.Initialize(this.configuration); - module.OnError(null); // is not supposed to throw + using (var module = new ExceptionTrackingTelemetryModule()) + { + module.Initialize(this.configuration); + module.OnError(null); // is not supposed to throw + } } } } diff --git a/Src/Web/Web.Shared.Net.Tests/Helpers/HttpModuleHelper.cs b/Src/Web/Web.Shared.Net.Tests/Helpers/HttpModuleHelper.cs index f7dbc8127..522e3b789 100644 --- a/Src/Web/Web.Shared.Net.Tests/Helpers/HttpModuleHelper.cs +++ b/Src/Web/Web.Shared.Net.Tests/Helpers/HttpModuleHelper.cs @@ -8,10 +8,13 @@ using System.Threading; using System.Web; using System.Web.Hosting; + using System.Web.Mvc; + using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.ApplicationInsights.Web.Implementation; + using Moq; using VisualStudio.TestTools.UnitTesting; internal static class HttpModuleHelper @@ -69,9 +72,33 @@ public static HttpContext GetFakeHttpContext(IDictionary headers var context = new HttpContext(workerRequest); HttpContext.Current = context; + return context; } + public static ControllerContext GetFakeControllerContext(bool isCustomErrorEnabled = false) + { + var mock = new Mock(GetFakeHttpContext()); + + using (var writerResponse = new StringWriter(CultureInfo.InvariantCulture)) + { + var response = new HttpResponseWrapper(new HttpResponse(writerResponse)); + mock.SetupGet(ctx => ctx.IsCustomErrorEnabled).Returns(isCustomErrorEnabled); + mock.SetupGet(ctx => ctx.Response).Returns(response); + } + + var controllerCtx = new ControllerContext + { + HttpContext = mock.Object + }; + + controllerCtx.RouteData.Values["controller"] = "controller"; + controllerCtx.RouteData.Values["action"] = "action"; + controllerCtx.Controller = new DefaultController(); + + return controllerCtx; + } + public static HttpContextBase GetFakeHttpContextBase(IDictionary headers = null) { return new HttpContextWrapper(GetFakeHttpContext(headers)); @@ -182,5 +209,9 @@ public override string GetRemoteAddress() return base.GetRemoteAddress(); } } + + private class DefaultController : Controller + { + } } } \ No newline at end of file diff --git a/Src/Web/Web.Shared.Net/ExceptionTrackingTelemetryModule.cs b/Src/Web/Web.Shared.Net/ExceptionTrackingTelemetryModule.cs index 7948289ac..1a58c7ad4 100644 --- a/Src/Web/Web.Shared.Net/ExceptionTrackingTelemetryModule.cs +++ b/Src/Web/Web.Shared.Net/ExceptionTrackingTelemetryModule.cs @@ -11,11 +11,19 @@ /// /// Telemetry module to collect unhandled exceptions caught by http module. /// - public class ExceptionTrackingTelemetryModule : ITelemetryModule + public class ExceptionTrackingTelemetryModule : ITelemetryModule, IDisposable { - private TelemetryClient telemetryClient; + private readonly object lockObject = new object(); + private TelemetryClient telemetryClient; private bool initializationErrorReported; + private bool isInitialized = false; + private bool disposed = false; + + /// + /// Gets or sets a value indicating whether automatic MVC 5 and WebAPI 2 exceptions tracking should be done. + /// + public bool EnableMvcAndWebApiExceptionAutoTracking { get; set; } = true; /// /// Implements on error callback of http module. @@ -67,8 +75,48 @@ public void OnError(HttpContext context) /// Telemetry configuration to use for initialization. public void Initialize(TelemetryConfiguration configuration) { - this.telemetryClient = new TelemetryClient(configuration); - this.telemetryClient.Context.GetInternalContext().SdkVersion = SdkVersionUtils.GetSdkVersion("web:"); + if (this.EnableMvcAndWebApiExceptionAutoTracking) + { + if (!this.isInitialized) + { + lock (this.lockObject) + { + if (!this.isInitialized) + { + this.telemetryClient = new TelemetryClient(configuration); + this.telemetryClient.Context.GetInternalContext().SdkVersion = SdkVersionUtils.GetSdkVersion("web:"); + ExceptionHandlersInjector.Inject(this.telemetryClient); + this.isInitialized = true; + } + } + } + } + } + + /// + /// IDisposable implementation. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// IDisposable implementation. + /// + /// The method has been called directly or indirectly by a user's code. + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.isInitialized = false; + } + + this.disposed = true; + } } } } diff --git a/Src/Web/Web.Shared.Net/Implementation/ExceptionHandlersInjector.cs b/Src/Web/Web.Shared.Net/Implementation/ExceptionHandlersInjector.cs new file mode 100644 index 000000000..0c5960bc4 --- /dev/null +++ b/Src/Web/Web.Shared.Net/Implementation/ExceptionHandlersInjector.cs @@ -0,0 +1,763 @@ +namespace Microsoft.ApplicationInsights.Web.Implementation +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Globalization; + using System.Reflection; + using System.Reflection.Emit; + using Microsoft.ApplicationInsights.DataContracts; + using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; + + internal static class ExceptionHandlersInjector + { + // we only support ASP.NET 5 for now + // and may extend list of supported versions if there will be a business need + private const int MinimumMvcVersion = 5; + private const int MinimumWebApiVersion = 5; + + private const string AssemblyName = "Microsoft.ApplicationInsights.ExceptionTracking"; + private const string MvcHandlerName = AssemblyName + ".MvcExceptionFilter"; + private const string WebApiHandlerName = AssemblyName + ".WebApiExceptionLogger"; + + private const string TelemetryClientFieldName = "telemetryClient"; + private const string IsAutoInjectedFieldName = "IsAutoInjected"; + private const string OnExceptionMethodName = "OnException"; + private const string OnLogMethodName = "Log"; + + /// + /// Forces injection of MVC5 exception filter and WebAPI2 exception logger into the global configurations. + /// + /// + /// Injection is attempted each time method is called. However if the filter/logger was injected already, injection is skipped. + /// Use this method only when you can guarantee it's called once per AppDomain. + /// + internal static void Inject(TelemetryClient telemetryClient) + { + try + { + WebEventSource.Log.InjectionStarted(); + + var trackExceptionMethod = GetMethodOrFail(typeof(TelemetryClient), "TrackException", new[] { typeof(ExceptionTelemetry) }); + var exceptionTelemetryCtor = GetConstructorOrFail(typeof(ExceptionTelemetry), new[] { typeof(Exception) }); + + var assemblyName = new AssemblyName(AssemblyName); + var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + + var moduleBuilder = assemblyBuilder.DefineDynamicModule(AssemblyName); + + AddMvcFilter( + telemetryClient, + moduleBuilder, + exceptionTelemetryCtor, + trackExceptionMethod); + + AddWebApiExceptionLogger( + telemetryClient, + moduleBuilder, + exceptionTelemetryCtor, + trackExceptionMethod); + } + catch (Exception e) + { + WebEventSource.Log.InjectionUnknownError(e.ToString()); + } + + WebEventSource.Log.InjectionCompleted(); + } + + #region Mvc + + /// + /// Generates new MVC5 filter class implementing HandleErrorAttribute and adds instance of it to the GlobalFilterCollection. + /// + /// instance. + /// to define type in. + /// of default constructor of . + /// of . + private static void AddMvcFilter( + TelemetryClient telemetryClient, + ModuleBuilder moduleBuilder, + ConstructorInfo exceptionTelemetryCtor, + MethodInfo trackExceptionMethod) + { + // This method emits following code: + // [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] + // public class MvcExceptionFilter : HandleErrorAttribute + // { + // public const bool IsAutoInjected = true; + // private readonly TelemetryClient telemetryClient = new TelemetryClient(); + // + // public MvcExceptionFilter(TelemetryClient tc) : base() + // { + // telemetryClient = tc; + // } + // + // public override void OnException(ExceptionContext filterContext) + // { + // if (filterContext != null && filterContext.HttpContext != null && filterContext.Exception != null && filterContext.HttpContext.IsCustomErrorEnabled) + // telemetryClient.TrackException(new ExceptionTelemetry(filterContext.Exception)); + // base.OnException(filterContext); + // } + // } + // + // GlobalFilters.Filters.Add(new MvcExceptionFilter(new TelemetryClient())); + try + { + // Get HandleErrorAttribute, make sure it's resolved and MVC version is supported + var handleErrorType = GetTypeOrFail("System.Web.Mvc.HandleErrorAttribute, System.Web.Mvc"); + if (handleErrorType.Assembly.GetName().Version.Major < MinimumMvcVersion) + { + WebEventSource.Log.InjectionVersionNotSupported(handleErrorType.Assembly.GetName().Version.ToString(), "MVC"); + return; + } + + // get global filter collection + GetMvcGlobalFiltersOrFail(out dynamic globalFilters, out Type globalFilterCollectionType); + + if (!NeedToInjectMvc(globalFilters)) + { + // there is another filter in the collection that has IsAutoInjected const field set to true - stop injection. + return; + } + + var exceptionContextType = GetTypeOrFail("System.Web.Mvc.ExceptionContext, System.Web.Mvc"); + var exceptionGetter = GetMethodOrFail(exceptionContextType, "get_Exception"); + + var controllerContextType = GetTypeOrFail("System.Web.Mvc.ControllerContext, System.Web.Mvc"); + var httpContextGetter = GetMethodOrFail(controllerContextType, "get_HttpContext"); + + var baseOnException = GetMethodOrFail(handleErrorType, OnExceptionMethodName, new[] { exceptionContextType }); + var addFilter = GetMethodOrFail(globalFilterCollectionType, "Add", new[] { typeof(object) }); + + // HttpContextBase requires full assembly name to be resolved + // even though version 4.0.0.0 (CLR version) is specified, it will be resolved to the latest .NET System.Web installed + var httpContextBaseType = GetTypeOrFail("System.Web.HttpContextBase, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + var isCustomErrorEnabled = GetMethodOrFail(httpContextBaseType, "get_IsCustomErrorEnabled"); + + // build a type for exception filter. + TypeBuilder typeBuilder = moduleBuilder.DefineType(MvcHandlerName, TypeAttributes.Public | TypeAttributes.Class, handleErrorType); + typeBuilder.SetCustomAttribute(GetUsageAttributeOrFail()); + var telemetryClientField = typeBuilder.DefineField(TelemetryClientFieldName, typeof(TelemetryClient), FieldAttributes.Private); + + DefineAutoInjectedField(typeBuilder); + + // emit constructor that assigns telemetry client field + var handleErrorBaseCtor = GetConstructorOrFail(handleErrorType, new Type[0]); + EmitConstructor(typeBuilder, typeof(TelemetryClient), telemetryClientField, handleErrorBaseCtor); + + // emit OnException method that handles exception + EmitMvcOnException( + typeBuilder, + exceptionContextType, + telemetryClientField, + exceptionGetter, + trackExceptionMethod, + baseOnException, + exceptionTelemetryCtor, + httpContextBaseType, + httpContextGetter, + isCustomErrorEnabled); + + // create error handler type + var handlerType = typeBuilder.CreateType(); + + // add handler to global filters + var mvcFilter = Activator.CreateInstance(handlerType, telemetryClient); + addFilter.Invoke(globalFilters, new[] { mvcFilter }); + } + catch (ResolutionException e) + { + // some of the required types/methods/properties/etc were not found. + // it may indicate we are dealing with a new version of MVC library + // handle it and log here, we may still succeed with WebApi injection + WebEventSource.Log.InjectionFailed("MVC", e.ToInvariantString()); + } + } + + /// + /// Emits OnException method. + /// + /// MVCExceptionFilter type builder. + /// Type of ExceptionContext. + /// FieldInfo of MVCExceptionFilter.telemetryClient. + /// MethodInfo to get ExceptionContext.Exception. + /// MethodInfo of TelemetryClient.TrackException(ExceptionTelemetry). + /// MethodInfo of base (HandleErrorAttribute) OnException method. + /// ConstructorInfo of ExceptionTelemetry. + /// Type of HttpContextBase. + /// MethodInfo to get ExceptionContext.HttpContext. + /// MethodInfo to get ExceptionContext.HttpContextBase.IsCustomErrorEnabled. + private static void EmitMvcOnException( + TypeBuilder typeBuilder, + Type exceptionContextType, + FieldInfo telemetryClientField, + MethodInfo exceptionGetter, + MethodInfo trackException, + MethodInfo baseOnException, + ConstructorInfo exceptionTelemetryCtor, + Type httpContextBaseType, + MethodInfo httpContextGetter, + MethodInfo isCustomErrorEnabled) + { + // This method emits following code: + // public override void OnException(ExceptionContext filterContext) + // { + // if (filterContext != null && filterContext.HttpContext != null && filterContext.Exception != null && filterContext.HttpContext.IsCustomErrorEnabled) + // telemetryClient.TrackException(new ExceptionTelemetry(filterContext.Exception)); + // base.OnException(filterContext); + // } + + // defines public override void OnException(ExceptionContext filterContext) + var onException = typeBuilder.DefineMethod( + OnExceptionMethodName, + MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig, + null, + new[] { exceptionContextType }); + var il = onException.GetILGenerator(); + + Label track = il.DefineLabel(); + Label end = il.DefineLabel(); + Label n1 = il.DefineLabel(); + Label n2 = il.DefineLabel(); + Label n3 = il.DefineLabel(); + + var httpContext = il.DeclareLocal(httpContextBaseType); + var exception = il.DeclareLocal(typeof(Exception)); + var v2 = il.DeclareLocal(typeof(bool)); + var v3 = il.DeclareLocal(typeof(bool)); + var v4 = il.DeclareLocal(typeof(bool)); + var v5 = il.DeclareLocal(typeof(bool)); + + // if filterContext is null, goto the end + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Stloc, v2); + il.Emit(OpCodes.Ldloc, v2); + il.Emit(OpCodes.Brfalse_S, n1); + il.Emit(OpCodes.Br_S, end); + + // if filterContext.HttpContext is null, goto the end + il.MarkLabel(n1); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, httpContextGetter); + il.Emit(OpCodes.Stloc, httpContext); + il.Emit(OpCodes.Ldloc, httpContext); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Stloc, v3); + il.Emit(OpCodes.Ldloc, v3); + il.Emit(OpCodes.Brfalse_S, n2); + il.Emit(OpCodes.Br_S, end); + + // if filterContext.Exception is null, goto the end + il.MarkLabel(n2); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, exceptionGetter); + il.Emit(OpCodes.Stloc, exception); + il.Emit(OpCodes.Ldloc, exception); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Stloc_S, v4); + il.Emit(OpCodes.Ldloc_S, v4); + il.Emit(OpCodes.Brfalse_S, n3); + il.Emit(OpCodes.Br_S, end); + + // if filterContext.HttpContext.IsCustomErrorEnabled is false, goto the end + il.MarkLabel(n3); + il.Emit(OpCodes.Ldloc, httpContext); + il.Emit(OpCodes.Callvirt, isCustomErrorEnabled); + il.Emit(OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Stloc_S, v5); + il.Emit(OpCodes.Ldloc_S, v5); + il.Emit(OpCodes.Brfalse_S, track); + il.Emit(OpCodes.Br_S, end); + + // telemetryClient.TrackException(new ExceptionTelemetry(filterContext.Exception)) + il.MarkLabel(track); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, telemetryClientField); + il.Emit(OpCodes.Ldloc, exception); + il.Emit(OpCodes.Newobj, exceptionTelemetryCtor); + il.Emit(OpCodes.Callvirt, trackException); + il.Emit(OpCodes.Br_S, end); + + // call base.OnException(filterContext); + il.MarkLabel(end); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, baseOnException); + + il.Emit(OpCodes.Ret); + } + + /// + /// Gets GlobalFilters.Filters property value. If not resolved, or is null, fails with . + /// + /// Resolved GlobalFilters.Filters instance. + /// Resolved GlobalFilterCollection type. + private static void GetMvcGlobalFiltersOrFail(out dynamic globalFilters, out Type globalFilterCollectionType) + { + globalFilterCollectionType = GetTypeOrFail("System.Web.Mvc.GlobalFilterCollection, System.Web.Mvc"); + + var globalFiltersType = GetTypeOrFail("System.Web.Mvc.GlobalFilters, System.Web.Mvc"); + globalFilters = GetStaticPropertyValueOrFail(globalFiltersType, "Filters"); + } + + /// + /// Checks if another auto injected filter was already added into the filter collection. + /// + /// GlobalFilters.Filters value of GlobalFilterCollection type. + /// True if injection needs to be done, false when collection already contains another auto injected filter. + private static bool NeedToInjectMvc(dynamic globalFilters) + { + var filters = (IEnumerable)globalFilters; + + // GlobalFilterCollection must implements IEnumerable, if not - fail. + if (filters == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Unexpected type of GlobalFilterCollection {0}", globalFilters.GetType())); + } + + var mvcFilterType = GetTypeOrFail("System.Web.Mvc.Filter, System.Web.Mvc"); + var mvcFilterInstanceProp = GetPropertyOrFail(mvcFilterType, "Instance"); + + // iterate over the filters + foreach (var filter in filters) + { + if (filter.GetType() != mvcFilterType) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Unexpected type of MVC Filter {0}", filter.GetType())); + } + + var instance = mvcFilterInstanceProp.GetValue(filter); + if (instance == null) + { + throw new ResolutionException($"MVC Filter does not have Instance property"); + } + + var isAutoInjectedField = instance.GetType().GetField(IsAutoInjectedFieldName, BindingFlags.Public | BindingFlags.Static); + if (isAutoInjectedField != null && (bool)isAutoInjectedField.GetValue(null)) + { + // isAutoInjected public const field (when true) indicates that our filter is already injected by other component. + // return false and stop MVC injection + WebEventSource.Log.InjectionSkipped(instance.GetType().AssemblyQualifiedName, "MVC"); + return false; + } + } + + return true; + } + + #endregion + + #region WebApi + + /// + /// Generates new WebAPI2 exception logger class implementing ExceptionLogger and adds instance of it to the GlobalConfiguration.Configuration.Services of IExceptionLogger type. + /// + /// instance. + /// to define type in. + /// of default constructor of . + /// of . + private static void AddWebApiExceptionLogger( + TelemetryClient telemetryClient, + ModuleBuilder moduleBuilder, + ConstructorInfo exceptionTelemetryCtor, + MethodInfo trackExceptionMethod) + { + // This method emits following code: + // public class WebApiExceptionLogger : ExceptionLogger + // { + // public const bool IsAutoInjected = true; + // private readonly TelemetryClient telemetryClient = new TelemetryClient(); + // + // public WebApiExceptionLogger(TelemetryClient tc) : base() + // { + // telemetryClient = tc; + // } + // + // public override void OnLog(ExceptionLoggerContext context) + // { + // if (context != null && context.Exception != null) + // telemetryClient.TrackException(new ExceptionTelemetry(context.Exception)); + // base.OnLog(context); + // } + // } + // + // GlobalConfiguration.Configuration.Services.Add(typeof(IExceptionLogger), new WebApiExceptionFilter(new TelemetryClient())); + try + { + // try to get all types/methods/properties/fields + // and if something is not available, fail fast + var baseExceptionLoggerType = GetTypeOrFail("System.Web.Http.ExceptionHandling.ExceptionLogger, System.Web.Http"); + if (baseExceptionLoggerType.Assembly.GetName().Version.Major < MinimumWebApiVersion) + { + WebEventSource.Log.InjectionVersionNotSupported(baseExceptionLoggerType.Assembly.GetName().Version.ToString(), "WebApi"); + return; + } + + var exceptionContextType = GetTypeOrFail("System.Web.Http.ExceptionHandling.ExceptionLoggerContext, System.Web.Http"); + var iexceptionLoggerType = GetTypeOrFail("System.Web.Http.ExceptionHandling.IExceptionLogger, System.Web.Http"); + + // get GlobalConfiguration.Configuration.Services + GetServicesContainerWebApiOrFail(out dynamic servicesContainer, out Type servicesContainerType); + if (!NeedToInjectWebApi(servicesContainer, servicesContainerType, iexceptionLoggerType)) + { + return; + } + + var baseOnLog = GetMethodOrFail(baseExceptionLoggerType, "Log", new[] { exceptionContextType }); + var addLogger = GetMethodOrFail(servicesContainerType, "Add", new[] { typeof(Type), typeof(object) }); + var exceptionGetter = GetMethodOrFail(exceptionContextType, "get_Exception"); + var exceptionLoggerBaseCtor = baseExceptionLoggerType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, CallingConventions.Standard, new Type[0], null); + if (exceptionLoggerBaseCtor == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Failed to get default constructor for type {0}", baseExceptionLoggerType.AssemblyQualifiedName)); + } + + // define 'public class WebApiExceptionLogger : ExceptionLogger' type + TypeBuilder typeBuilder = moduleBuilder.DefineType(WebApiHandlerName, TypeAttributes.Public | TypeAttributes.Class, baseExceptionLoggerType); + DefineAutoInjectedField(typeBuilder); + var telemetryClientField = typeBuilder.DefineField(TelemetryClientFieldName, typeof(TelemetryClient), FieldAttributes.Private | FieldAttributes.InitOnly); + + // emit constructor that assigns telemetry client field + EmitConstructor(typeBuilder, typeof(TelemetryClient), telemetryClientField, exceptionLoggerBaseCtor); + + // emit Log method + EmitWebApiLog(typeBuilder, exceptionContextType, exceptionGetter, telemetryClientField, exceptionTelemetryCtor, trackExceptionMethod, baseOnLog); + + // create error WebApiExceptionLogger type + var exceptionLoggerType = typeBuilder.CreateType(); + + // add WebApiExceptionLogger to list of services + var exceptionLogger = Activator.CreateInstance(exceptionLoggerType, telemetryClient); + addLogger.Invoke(servicesContainer, new[] { iexceptionLoggerType, exceptionLogger }); + } + catch (ResolutionException e) + { + // some of the required types/methods/properties/etc were not found. + // it may indicate we are dealing with a new version of WebApi library + // handle it and log here, we may still succeed with MVC injection + WebEventSource.Log.InjectionFailed("WebApi", e.ToInvariantString()); + } + } + + /// + /// Emits OnLog method. + /// + /// MVCExceptionFilter type builder. + /// Type of ExceptionContext. + /// MethodInfo to get ExceptionLoggerContext.Exception. + /// FieldInfo of WebAPIExceptionFilter.telemetryClient. + /// ConstructorInfo of ExceptionTelemetry. + /// MethodInfo of TelemetryClient.TrackException(ExceptionTelemetry). + /// MethodInfo of base (ExceptionLogger) OnLog method. + private static void EmitWebApiLog(TypeBuilder typeBuilder, Type exceptionContextType, MethodInfo exceptionGetter, FieldInfo telemetryClientField, ConstructorInfo exceptionTelemetryCtor, MethodInfo trackException, MethodInfo baseOnLog) + { + // This method emits following code: + // public override void OnLog(ExceptionLoggerContext context) + // { + // if (context != null && context.Exception != null) + // telemetryClient.TrackException(new ExceptionTelemetry(context.Exception)); + // base.OnLog(context); + // } + // public override void OnLog(ExceptionLoggerContext context) + var log = typeBuilder.DefineMethod(OnLogMethodName, MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig, null, new[] { exceptionContextType }); + var il = log.GetILGenerator(); + + Label track = il.DefineLabel(); + Label end = il.DefineLabel(); + Label n1 = il.DefineLabel(); + + var exception = il.DeclareLocal(typeof(Exception)); + var v1 = il.DeclareLocal(typeof(bool)); + var v2 = il.DeclareLocal(typeof(bool)); + + // is context is null, goto the end + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Stloc, v1); + il.Emit(OpCodes.Ldloc, v1); + il.Emit(OpCodes.Brfalse_S, n1); + il.Emit(OpCodes.Br_S, end); + + // is context.Exception is null, goto the end + il.MarkLabel(n1); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Callvirt, exceptionGetter); + il.Emit(OpCodes.Stloc, exception); + il.Emit(OpCodes.Ldloc, exception); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ceq); + il.Emit(OpCodes.Stloc, v2); + il.Emit(OpCodes.Ldloc, v2); + il.Emit(OpCodes.Brfalse_S, track); + il.Emit(OpCodes.Br_S, end); + + // telemetryClient.TrackException(new ExceptionTelemetry(context.Exception)) + il.MarkLabel(track); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldfld, telemetryClientField); + il.Emit(OpCodes.Ldloc, exception); + il.Emit(OpCodes.Newobj, exceptionTelemetryCtor); + il.Emit(OpCodes.Callvirt, trackException); + il.Emit(OpCodes.Br_S, end); + + // base.OnLog(context); + il.MarkLabel(end); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, baseOnLog); + + il.Emit(OpCodes.Ret); + } + + /// + /// Gets GlobalConfiguration.Configuration.Services value and type. + /// + /// Services collection. + /// ServicesContainer type of Services. + private static void GetServicesContainerWebApiOrFail(out dynamic serviceContaner, out Type servicesContainerType) + { + var globalConfigurationType = GetTypeOrFail("System.Web.Http.GlobalConfiguration, System.Web.Http.WebHost"); + var httpConfigurationType = GetTypeOrFail("System.Web.Http.HttpConfiguration, System.Web.Http"); + servicesContainerType = GetTypeOrFail("System.Web.Http.Controllers.ServicesContainer, System.Web.Http"); + + var configuration = GetStaticPropertyValueOrFail(globalConfigurationType, "Configuration"); + serviceContaner = GetPropertyValueOrFail(httpConfigurationType, configuration, "Services"); + } + + /// + /// Checks if another auto injected logger was already added into the Services collection. + /// + /// GlobalConfiguration.Configuration.Services value. + /// ServicesContainer type. + /// IExceptionLogger type. + /// True if injection needs to be done, false when collection already contains another auto injected logger. + private static bool NeedToInjectWebApi(dynamic servicesContainer, Type servicesContainerType, Type iexceptionLoggerType) + { + // call ServicesContainer.GetServices(Type) to get collection of all exception loggers. + var getServicesMethod = GetMethodOrFail(servicesContainerType, "GetServices", new[] { typeof(Type) }); + + var exceptionLoggersObj = getServicesMethod.Invoke(servicesContainer, new object[] { iexceptionLoggerType }); + var exceptionLoggers = (IEnumerable)exceptionLoggersObj; + if (exceptionLoggers == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Unexpected type of {0}", exceptionLoggersObj.GetType())); + } + + foreach (var filter in exceptionLoggers) + { + var isAutoInjectedField = filter.GetType().GetField(IsAutoInjectedFieldName, BindingFlags.Public | BindingFlags.Static); + if (isAutoInjectedField != null) + { + var isAutoInjectedFilter = (bool)isAutoInjectedField.GetValue(null); + if (isAutoInjectedFilter) + { + // if logger defines isAutoInjected property, stop WebApi injection. + WebEventSource.Log.InjectionSkipped(filter.GetType().AssemblyQualifiedName, "WebApi"); + return false; + } + } + } + + return true; + } + #endregion + + /// + /// Emits constructor for MVC Filter and WebAPI logger. + /// + /// TypeBuilder of MVC filter or WebAPI logger. + /// Type of TelemetryClient. + /// FieldInfo to assign TelemetryClient instance to. + /// ConstructorInfo of the base class. + private static void EmitConstructor(TypeBuilder typeBuilder, Type telemetryClientType, FieldInfo field, ConstructorInfo baseCtorInfo) + { + // this method emits following code: + // public ClassName(TelemetryClient tc) : base() + // { + // telemetryClient = tc; + // } + + // public (TelemetryClient tc) + var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { telemetryClientType }); + var il = ctor.GetILGenerator(); + + // call base constructor + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, baseCtorInfo); + + // assign telemetryClient field + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Stfld, field); + il.Emit(OpCodes.Ret); + } + + /// + /// Emits IsAutoInjected field. The field is used to mark injected filter/logger and prevent double-injection. + /// + /// Type of the exception handler. + private static void DefineAutoInjectedField(TypeBuilder exceptionHandlerType) + { + // This method emits following code: + // public const bool IsAutoInjected = true; + // we mark our types by using IsAutoInjected field - this is prevention mechanism. + // If this code is shipped as a standalone nuget, the Web SDK and lightup might both register filters. + // all components re-using this code must check for IsAutoInjected on the filter/handler + // and do not inject itself if there is already a filter with such field + var field = exceptionHandlerType.DefineField(IsAutoInjectedFieldName, typeof(bool), FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault); + field.SetConstant(true); + } + + #region Helpers + + /// + /// Gets attribute builder for AttributeUsageAttribute with AllowMultiple set to true. + /// + /// CustomAttributeBuilder for the AttributeUsageAttribute. + private static CustomAttributeBuilder GetUsageAttributeOrFail() + { + // This method emits following code: + // [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] + var attributeUsageCtor = GetConstructorOrFail(typeof(AttributeUsageAttribute), new[] { typeof(AttributeTargets) }); + var allowMultipleInfo = typeof(AttributeUsageAttribute).GetProperty("AllowMultiple", BindingFlags.Instance | BindingFlags.Public); + if (attributeUsageCtor == null || allowMultipleInfo == null) + { + // must not ever happen + throw new ResolutionException("Failed to get AttributeUsageAttribute ctor or AllowMultiple property"); + } + + return new CustomAttributeBuilder(attributeUsageCtor, new object[] { AttributeTargets.Class | AttributeTargets.Method }, new[] { allowMultipleInfo }, new object[] { true }); + } + + /// + /// Gets type by it's name and throws if type is not found. + /// + /// Name of the type to be found. It could be a short namespace qualified name or assembly qualified name, as appropriate. + /// Resolved . + private static Type GetTypeOrFail(string typeName) + { + var type = Type.GetType(typeName); + if (type == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Failed to get {0} type", typeName)); + } + + return type; + } + + /// + /// Gets public instance method info from the given type with the given of parameters. Throws if method is not found. + /// + /// Type to get method from. + /// Method name. + /// Array of method parameters. Optional (empty array by default). + /// Resolved . + private static MethodInfo GetMethodOrFail(Type type, string methodName, Type[] paramTypes = null) + { + var method = type.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public, null, paramTypes ?? new Type[0], null); + if (method == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Failed to get {0} method info from type {1}", methodName, type)); + } + + return method; + } + + /// + /// Gets public instance constructor info from the given type with the given of parameters. Throws if constructor is not found. + /// + /// Type to get constructor from. + /// Array of constructor parameters. + /// Resolved . + private static ConstructorInfo GetConstructorOrFail(Type type, Type[] paramTypes) + { + var ctor = type.GetConstructor(paramTypes); + if (ctor == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Failed to get constructor info from type {0}", type)); + } + + return ctor; + } + + /// + /// Gets public instance property info from the given type. Throws if property is not found. + /// + /// Type to get property from. + /// Name or the property to get. + /// Resolved . + private static PropertyInfo GetPropertyOrFail(Type type, string propertyName) + { + var prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); + if (prop == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Failed to get {0} property info from type {1}", propertyName, type)); + } + + return prop; + } + + /// + /// Gets public instance property value from the given type. Throws if property is not found. + /// + /// Type to get property from. + /// Instance of type to get property value from. + /// Name or the property to get. + /// Value of the property. + private static dynamic GetPropertyValueOrFail(Type type, dynamic instance, string propertyName) + { + var prop = GetPropertyOrFail(type, propertyName); + + var value = prop.GetValue(instance); + if (value == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Failed to get {0} property info from type {1}", propertyName, type)); + } + + return value; + } + + /// + /// Gets public static property value from the given type. Throws if property is not found. + /// + /// Type to get property from. + /// Name of the property to get. + /// Value of the property. + private static dynamic GetStaticPropertyValueOrFail(Type type, string propertyName) + { + var prop = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Static); + if (prop == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Failed to get {0} property info from type {1}", propertyName, type)); + } + + var value = prop.GetValue(null); + if (value == null) + { + throw new ResolutionException(string.Format(CultureInfo.InvariantCulture, "Failed to get {0} property value from type {1}", propertyName, type)); + } + + return value; + } + + /// + /// Represents specific resolution exception. + /// + [Serializable] + private class ResolutionException : Exception + { + public ResolutionException(string message) : base(message) + { + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Src/Web/Web.Shared.Net/Implementation/WebEventSource.cs b/Src/Web/Web.Shared.Net/Implementation/WebEventSource.cs index aadea81be..69a0c2315 100644 --- a/Src/Web/Web.Shared.Net/Implementation/WebEventSource.cs +++ b/Src/Web/Web.Shared.Net/Implementation/WebEventSource.cs @@ -411,6 +411,62 @@ public void RequestTrackingTelemetryModuleRequestWasNotLoggedVerbose(string requ this.ApplicationName); } + [Event(42, + Keywords = Keywords.Diagnostics, + Message = "Injection started.", + Level = EventLevel.Verbose)] + public void InjectionStarted(string appDomainName = "Incorrect") + { + this.WriteEvent(42, this.ApplicationName); + } + + [Event(43, + Keywords = Keywords.Diagnostics, + Message = "{0} Injection failed. Error message: {1}", + Level = EventLevel.Informational)] + public void InjectionFailed(string component, string error, string appDomainName = "Incorrect") + { + this.WriteEvent(43, component ?? string.Empty, error ?? string.Empty, this.ApplicationName); + } + + [Event(44, + Keywords = Keywords.Diagnostics, + Message = "Version '{0}' of component '{1}' is not supported", + Level = EventLevel.Verbose)] + public void InjectionVersionNotSupported(string version, string component, string appDomainName = "Incorrect") + { + this.WriteEvent(44, version ?? string.Empty, component ?? string.Empty, this.ApplicationName); + } + + [Event( + 45, + Keywords = Keywords.Diagnostics, + Message = "Unknown exception. Error message: {0}.", + Level = EventLevel.Error)] + public void InjectionUnknownError(string error, string appDomainName = "Incorrect") + { + this.WriteEvent(45, error ?? string.Empty, this.ApplicationName); + } + + [Event( + 46, + Keywords = Keywords.Diagnostics, + Message = "Another exception filter or logger is already injected. Type: '{0}', component: '{1}'", + Level = EventLevel.Verbose)] + public void InjectionSkipped(string type, string component, string appDomainName = "Incorrect") + { + this.WriteEvent(46, type ?? string.Empty, component ?? string.Empty, this.ApplicationName); + } + + [Event(47, + Keywords = Keywords.Diagnostics, + Message = "Injection completed.", + Level = EventLevel.Verbose)] + public void InjectionCompleted(string appDomainName = "Incorrect") + { + this.WriteEvent(47, this.ApplicationName); + } + [NonEvent] private string GetApplicationName() { diff --git a/Src/Web/Web.Shared.Net/RequestTrackingTelemetryModule.cs b/Src/Web/Web.Shared.Net/RequestTrackingTelemetryModule.cs index 6072b5758..f9c0b0b70 100644 --- a/Src/Web/Web.Shared.Net/RequestTrackingTelemetryModule.cs +++ b/Src/Web/Web.Shared.Net/RequestTrackingTelemetryModule.cs @@ -462,9 +462,9 @@ internal bool OnEndRequest_ShouldLog(HttpContext context) try { var rootRequestId = headers[HeaderRootRequestId]; - rootRequestId = StringUtilities.EnforceMaxLength(rootRequestId, InjectionGuardConstants.RequestHeaderMaxLength); if (rootRequestId != null) { + rootRequestId = StringUtilities.EnforceMaxLength(rootRequestId, InjectionGuardConstants.RequestHeaderMaxLength); if (!this.IsRequestKnown(rootRequestId)) { // doesn't exist add to dictionary and return true diff --git a/Src/Web/Web.Shared.Net/Web.Shared.Net.projitems b/Src/Web/Web.Shared.Net/Web.Shared.Net.projitems index ad32a87c1..fcbaf93a1 100644 --- a/Src/Web/Web.Shared.Net/Web.Shared.Net.projitems +++ b/Src/Web/Web.Shared.Net/Web.Shared.Net.projitems @@ -19,6 +19,7 @@ + diff --git a/Test/Web/FunctionalTests/FunctionalTests/WebAppFW45SampledTests.cs b/Test/Web/FunctionalTests/FunctionalTests/WebAppFW45SampledTests.cs index aa471f0a6..38d16ea1d 100644 --- a/Test/Web/FunctionalTests/FunctionalTests/WebAppFW45SampledTests.cs +++ b/Test/Web/FunctionalTests/FunctionalTests/WebAppFW45SampledTests.cs @@ -58,7 +58,7 @@ public void TestCleanup() } [TestMethod] - public void TestMvcRequestWithExceptionSampled() + public void TestWebApiRequestWithExceptionSampledCustomErrorsOff() { const string requestPath = "api/products/5"; diff --git a/Test/Web/FunctionalTests/FunctionalTests/WebAppFW45Tests.cs b/Test/Web/FunctionalTests/FunctionalTests/WebAppFW45Tests.cs index 929512dac..e03a0fae7 100644 --- a/Test/Web/FunctionalTests/FunctionalTests/WebAppFW45Tests.cs +++ b/Test/Web/FunctionalTests/FunctionalTests/WebAppFW45Tests.cs @@ -69,7 +69,7 @@ public static void ClassCleanup() } [TestMethod] - public void Mvc200RequestFW45BasicRequestValidationAndHeaders() + public void WebApi200RequestFW45BasicRequestValidationAndHeaders() { const string requestPath = "api/products"; const string expectedRequestName = "GET products"; @@ -107,9 +107,42 @@ public void Mvc200RequestFW45BasicRequestValidationAndHeaders() Assert.IsTrue(request.data.baseData.id.StartsWith("|guid."), "Request Id is not properly set"); } + [TestMethod] + public void WebApi500RequestFW45ExceptionTracking() + { + const string requestPath = "api/products/5"; + const string expectedRequestName = "POST products [id]"; + string expectedRequestUrl = this.Config.ApplicationUri + "/" + requestPath; + + DateTimeOffset testStart = DateTimeOffset.UtcNow; + + //Call an applicaiton page + var client = new HttpClient(); + var requestMessage = new HttpRequestMessage + { + RequestUri = new Uri(expectedRequestUrl), + Method = HttpMethod.Post, + }; + + var responseTask = client.SendAsync(requestMessage); + responseTask.Wait(TimeoutInMs); + + var telemetry = Listener.ReceiveAllItemsDuringTime(TimeoutInMs); + var requests = telemetry.OfType>().ToArray(); + Assert.AreEqual(1, requests.Length); + + var allExceptions = telemetry.OfType>(); + // select only test exception, and filter out those that are collected by first chance module - the module is not enabled by default + var controllerException = allExceptions.Where(i => i.data.baseData.exceptions.FirstOrDefault()?.message == "Test exception to get 500" && i.tags[new ContextTagKeys().InternalSdkVersion].StartsWith("web")); + Assert.AreEqual(1, controllerException.Count()); + + var testFinish = DateTimeOffset.UtcNow; + + this.TestWebApplicationHelper(expectedRequestName, expectedRequestUrl, "500", false, requests.Single(), testStart, testFinish); + } [TestMethod] - public void Mvc200RequestFW45BasicRequestValidationAndLegacyIdHeaders() + public void WebApi200RequestFW45BasicRequestValidationAndLegacyIdHeaders() { const string requestPath = "api/products"; string expectedRequestUrl = this.Config.ApplicationUri + "/" + requestPath; @@ -142,7 +175,7 @@ public void Mvc200RequestFW45BasicRequestValidationAndLegacyIdHeaders() } [TestMethod] - public void Mvc200RequestFW45BasicRequestSyntheticFiltering() + public void WebApi200RequestFW45BasicRequestSyntheticFiltering() { const string requestPath = "api/products"; const string expectedRequestName = "GET products"; @@ -175,7 +208,7 @@ public void Mvc200RequestFW45BasicRequestSyntheticFiltering() } [TestMethod] - public void TestMvc404Request() + public void TestWebApi404Request() { const string requestPath = "api/products/101"; const string expectedRequestName = "GET products [id]"; diff --git a/Test/Web/FunctionalTests/TestApps/WebAppFW45/Controllers/ProductsController.cs b/Test/Web/FunctionalTests/TestApps/WebAppFW45/Controllers/ProductsController.cs index bd0903a07..7b4ab487a 100644 --- a/Test/Web/FunctionalTests/TestApps/WebAppFW45/Controllers/ProductsController.cs +++ b/Test/Web/FunctionalTests/TestApps/WebAppFW45/Controllers/ProductsController.cs @@ -42,5 +42,4 @@ public void PostProduct(int id) } } } - } \ No newline at end of file diff --git a/Test/Web/FunctionalTests/TestApps/WebAppFW45/Global.asax.cs b/Test/Web/FunctionalTests/TestApps/WebAppFW45/Global.asax.cs index 65630e654..f67967444 100644 --- a/Test/Web/FunctionalTests/TestApps/WebAppFW45/Global.asax.cs +++ b/Test/Web/FunctionalTests/TestApps/WebAppFW45/Global.asax.cs @@ -1,14 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.IO; -using System.Linq; -using System.Web; -using System.Web.Http; -using System.Web.Routing; - -namespace WebAppFW45 +namespace WebAppFW45 { + using System.Configuration; + using System.Web.Http; using Microsoft.ApplicationInsights; public class WebApiApplication : System.Web.HttpApplication @@ -22,7 +15,6 @@ protected void Application_Start() } GlobalConfiguration.Configure(WebApiConfig.Register); - // To remove 1 minute wait for items to apprear we can: // - set MaxNumberOfItemsPerTransmission to 1 so each item is delivered immidiately // - call telemetryQueue.Flush each X ms diff --git a/Test/Web/FunctionalTests/TestApps/WebAppFW45/Web.config b/Test/Web/FunctionalTests/TestApps/WebAppFW45/Web.config index 09a83e827..a84d6c1b7 100644 --- a/Test/Web/FunctionalTests/TestApps/WebAppFW45/Web.config +++ b/Test/Web/FunctionalTests/TestApps/WebAppFW45/Web.config @@ -23,6 +23,7 @@ + @@ -46,10 +47,6 @@ - - - - diff --git a/Test/Web/FunctionalTests/TestApps/WebAppFW45Sampled/Web.config b/Test/Web/FunctionalTests/TestApps/WebAppFW45Sampled/Web.config index 053df472d..4093770a9 100644 --- a/Test/Web/FunctionalTests/TestApps/WebAppFW45Sampled/Web.config +++ b/Test/Web/FunctionalTests/TestApps/WebAppFW45Sampled/Web.config @@ -23,6 +23,7 @@ +