From 531d37cc391f9da919a0fde085bfe07c95e940b2 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Wed, 28 Oct 2020 11:28:23 +1000 Subject: [PATCH] Normalize transaction names for ASP.NET Full Framework (#973) This commit normalizes the transaction name for ASP.NET MVC Full Framework to use the convention of // [*] which aligns with the convention used in ASP.NET Core transaction names. The transaction name is determined from the RouteData values. - set unknown route only when the route data did not end up routing to a controller action. This is determined by looking for a 404 HttpException coming from System.Web.Mvc. - Add webforms page, routed webforms page, web api controller, and area controller, to assert transaction name in each case. - Integration tests introduce an MVC area with a HomeController and Index action, to assert that the area route datatoken is taken into account. Without taking into account the area, same named controller actions in areas would end up aggregrated under the same transaction name. Closes #201 --- .../App_Start/RouteConfig.cs | 33 ++- .../App_Start/WebApiConfig.cs | 21 ++ .../MyArea/Controllers/HomeController.cs | 16 ++ .../Areas/MyArea/MyAreaRegistration.cs | 20 ++ .../Areas/MyArea/Views/Home/Index.cshtml | 39 ++++ .../Areas/MyArea/Web.config | 43 ++++ .../Areas/MyArea/_ViewStart.cshtml | 3 + .../AspNetFullFrameworkSampleApp.csproj | 38 +++- .../Controllers/HomeController.cs | 56 ++--- .../Controllers/WebApiController.cs | 22 ++ .../Global.asax.cs | 12 + .../Views/Diagnostics/Index.cshtml | 9 +- .../Views/Shared/_Layout.cshtml | 8 +- .../Views/Web.config | 1 + .../Webforms.aspx | 79 +++++++ .../Webforms.aspx.cs | 39 ++++ .../MyArea/Controllers/HomeController.cs | 16 ++ .../Areas/MyArea/Views/Home/Index.cshtml | 10 + .../MyOtherArea/Controllers/HomeController.cs | 16 ++ .../Areas/MyOtherArea/Views/Home/Index.cshtml | 10 + .../SampleAspNetCoreApp.csproj | 6 + sample/SampleAspNetCoreApp/Startup.cs | 25 ++- .../{Views => }/_ViewStart.cshtml | 0 .../WebRequestTransactionCreator.cs | 47 +--- .../ElasticApmModule.cs | 53 ++++- src/Elastic.Apm/Api/ITransaction.cs | 8 + src/Elastic.Apm/Model/Transaction.cs | 64 +++++- test/Elastic.Apm.AspNetCore.Tests/Helper.cs | 33 +-- .../TransactionNameTests.cs | 32 +++ .../CaptureHeadersConfigDisabledTest.cs | 2 +- .../CentralConfigTests.cs | 4 +- .../CustomEnvironmentViaSettings.cs | 2 +- .../CustomFlushInterval.cs | 2 +- .../CustomServiceNameSetViaSettings.cs | 2 +- .../CustomServiceNodeNameViaSettings.cs | 2 +- .../DbSpanTests.cs | 8 +- .../DistributedTracingTests.cs | 98 +-------- .../ErrorsTests.cs | 12 +- .../GlobalLabelsTests.cs | 2 +- .../IisAdministration.cs | 2 +- .../MetadataTests.cs | 4 +- .../MetricsTestsBase.cs | 2 +- .../SampleRate0Tests.cs | 2 +- .../TestWithDefaultSettings.cs | 6 +- .../TestsBase.cs | 128 +++++++---- .../TestsWithApmServerStopped.cs | 2 +- .../TestsWithSampleAppLogDisabled.cs | 2 +- .../TransactionIgnoreUrlsTest.cs | 2 +- .../TransactionNameTests.cs | 208 ++++++++++++++++++ 49 files changed, 969 insertions(+), 282 deletions(-) create mode 100644 sample/AspNetFullFrameworkSampleApp/App_Start/WebApiConfig.cs create mode 100644 sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Controllers/HomeController.cs create mode 100644 sample/AspNetFullFrameworkSampleApp/Areas/MyArea/MyAreaRegistration.cs create mode 100644 sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Views/Home/Index.cshtml create mode 100644 sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Web.config create mode 100644 sample/AspNetFullFrameworkSampleApp/Areas/MyArea/_ViewStart.cshtml create mode 100644 sample/AspNetFullFrameworkSampleApp/Controllers/WebApiController.cs create mode 100644 sample/AspNetFullFrameworkSampleApp/Webforms.aspx create mode 100644 sample/AspNetFullFrameworkSampleApp/Webforms.aspx.cs create mode 100644 sample/SampleAspNetCoreApp/Areas/MyArea/Controllers/HomeController.cs create mode 100644 sample/SampleAspNetCoreApp/Areas/MyArea/Views/Home/Index.cshtml create mode 100644 sample/SampleAspNetCoreApp/Areas/MyOtherArea/Controllers/HomeController.cs create mode 100644 sample/SampleAspNetCoreApp/Areas/MyOtherArea/Views/Home/Index.cshtml rename sample/SampleAspNetCoreApp/{Views => }/_ViewStart.cshtml (100%) create mode 100644 test/Elastic.Apm.AspNetFullFramework.Tests/TransactionNameTests.cs diff --git a/sample/AspNetFullFrameworkSampleApp/App_Start/RouteConfig.cs b/sample/AspNetFullFrameworkSampleApp/App_Start/RouteConfig.cs index 6d1a51645..d06629673 100644 --- a/sample/AspNetFullFrameworkSampleApp/App_Start/RouteConfig.cs +++ b/sample/AspNetFullFrameworkSampleApp/App_Start/RouteConfig.cs @@ -2,6 +2,9 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Web; +using System.Web.Http; +using System.Web.Http.Batch; using System.Web.Mvc; using System.Web.Routing; @@ -9,6 +12,21 @@ namespace AspNetFullFrameworkSampleApp { public class RouteConfig { + /// + /// Registers Web API routes + /// + public static void RegisterWebApiRoutes(HttpConfiguration configuration, HttpBatchHandler batchHandler) + { + configuration.MapHttpAttributeRoutes(); + + configuration.Routes.MapHttpBatchRoute("Batch", "api/batch", batchHandler); + configuration.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional }); + } + + /// + /// Registers MVC and Webpage routes + /// + /// public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); @@ -16,8 +34,21 @@ public static void RegisterRoutes(RouteCollection routes) routes.MapRoute( "Default", "{controller}/{action}/{id}", - new { controller = "Home", action = "Index", id = UrlParameter.Optional } + new { controller = "Home", action = "Index", id = UrlParameter.Optional }, + new { notRoutedWebforms = new NotRoutedWebformsConstraint() }, + new[] { "AspNetFullFrameworkSampleApp.Controllers" } + ); + + routes.MapPageRoute(Webforms.RoutedWebforms, + Webforms.RoutedWebforms, + "~/Webforms.aspx" ); } } + + public class NotRoutedWebformsConstraint : IRouteConstraint + { + public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) => + values.TryGetValue("controller", out var controller) && (string)controller != Webforms.RoutedWebforms; + } } diff --git a/sample/AspNetFullFrameworkSampleApp/App_Start/WebApiConfig.cs b/sample/AspNetFullFrameworkSampleApp/App_Start/WebApiConfig.cs new file mode 100644 index 000000000..60b080095 --- /dev/null +++ b/sample/AspNetFullFrameworkSampleApp/App_Start/WebApiConfig.cs @@ -0,0 +1,21 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Linq; +using System.Web.Http; + +namespace AspNetFullFrameworkSampleApp +{ + public static class WebApiConfig + { + public static void Register(HttpConfiguration configuration) + { + // remove xml support + var appXmlType = configuration.Formatters.XmlFormatter.SupportedMediaTypes + .FirstOrDefault(t => t.MediaType == "application/xml"); + configuration.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType); + } + } +} diff --git a/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Controllers/HomeController.cs b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Controllers/HomeController.cs new file mode 100644 index 000000000..a494cfcdd --- /dev/null +++ b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Controllers/HomeController.cs @@ -0,0 +1,16 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Web.Mvc; + +namespace AspNetFullFrameworkSampleApp.Areas.MyArea.Controllers +{ + public class HomeController : Controller + { + internal const string HomePageRelativePath = "MyArea/Home"; + + public ActionResult Index() => View(); + } +} diff --git a/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/MyAreaRegistration.cs b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/MyAreaRegistration.cs new file mode 100644 index 000000000..51db63a7b --- /dev/null +++ b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/MyAreaRegistration.cs @@ -0,0 +1,20 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Web.Mvc; + +namespace AspNetFullFrameworkSampleApp.Areas.MyArea +{ + public class MyAreaRegistration : AreaRegistration + { + public override void RegisterArea(AreaRegistrationContext context) => + context.MapRoute("MyArea_Default", + "MyArea/{controller}/{action}/{id}", + new { action = "Index", id = UrlParameter.Optional }, + new[] { "AspNetFullFrameworkSampleApp.Areas.MyArea.Controllers" }); + + public override string AreaName => "MyArea"; + } +} diff --git a/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Views/Home/Index.cshtml b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Views/Home/Index.cshtml new file mode 100644 index 000000000..7698848f9 --- /dev/null +++ b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Views/Home/Index.cshtml @@ -0,0 +1,39 @@ +@{ + ViewBag.Title = "My Area Home Page"; +} + +
+

ASP.NET

+

ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS and JavaScript.

+

+ Learn more » +

+
+ +
+
+

Getting started

+

+ ASP.NET MVC gives you a powerful, patterns-based way to build dynamic websites that + enables a clean separation of concerns and gives you full control over markup + for enjoyable, agile development. +

+

+ Learn more » +

+
+
+

Get more libraries

+

NuGet is a free Visual Studio extension that makes it easy to add, remove, and update libraries and tools in Visual Studio projects.

+

+ Learn more » +

+
+
+

Web Hosting

+

You can easily find a web hosting company that offers the right mix of features and price for your applications.

+

+ Learn more » +

+
+
\ No newline at end of file diff --git a/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Web.config b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Web.config new file mode 100644 index 000000000..be8136e17 --- /dev/null +++ b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/Web.config @@ -0,0 +1,43 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/_ViewStart.cshtml b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/_ViewStart.cshtml new file mode 100644 index 000000000..25291bc2f --- /dev/null +++ b/sample/AspNetFullFrameworkSampleApp/Areas/MyArea/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "~/Views/Shared/_Layout.cshtml"; +} \ No newline at end of file diff --git a/sample/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj b/sample/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj index 0f0e5507c..bed3f6aeb 100644 --- a/sample/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj +++ b/sample/AspNetFullFrameworkSampleApp/AspNetFullFrameworkSampleApp.csproj @@ -33,6 +33,7 @@ DEBUG;TRACE prompt 4 + AnyCPU true @@ -42,6 +43,7 @@ TRACE prompt 4 + AnyCPU true @@ -113,6 +115,16 @@ + + + + + + + + ASPXCodeBehind + Webforms.aspx + @@ -189,6 +201,11 @@ + + + + + @@ -267,13 +284,13 @@ - + + - - - - + + 5.2.4 + 10.0 @@ -292,6 +309,13 @@ Latest + true + + + + + + @@ -308,7 +332,9 @@ True 51565 / - http://localhost:51565/ + http://localhost/Elastic.Apm.AspNetFullFramework.Tests.SampleApp + false + http://localhost:51565 False False diff --git a/sample/AspNetFullFrameworkSampleApp/Controllers/HomeController.cs b/sample/AspNetFullFrameworkSampleApp/Controllers/HomeController.cs index edc56262c..cb90a3506 100644 --- a/sample/AspNetFullFrameworkSampleApp/Controllers/HomeController.cs +++ b/sample/AspNetFullFrameworkSampleApp/Controllers/HomeController.cs @@ -12,6 +12,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using System.Web; using System.Web.Mvc; using AspNetFullFrameworkSampleApp.Data; using Elastic.Apm; @@ -22,49 +23,41 @@ namespace AspNetFullFrameworkSampleApp.Controllers /// Note that this application is used by Elastic.Apm.AspNetFullFramework.Tests so changing it might break the tests public class HomeController : ControllerBase { + internal const string HomePageRelativePath = "Home"; + internal const string ConcurrentDbTestPageRelativePath = HomePageRelativePath + "/" + nameof(ConcurrentDbTest); + internal const string ContactPageRelativePath = HomePageRelativePath + "/" + nameof(Contact); + internal const string CustomChildSpanThrowsPageRelativePath = HomePageRelativePath + "/" + nameof(CustomChildSpanThrows); internal const string AboutPageRelativePath = HomePageRelativePath + "/" + nameof(About); - + internal const string ChildHttpSpanWithResponseForbiddenPath = HomePageRelativePath + "/" + nameof(ChildHttpSpanWithResponseForbidden); internal const string CallReturnBadRequestPageRelativePath = HomePageRelativePath + "/" + nameof(CallReturnBadRequest); + internal const string CustomSpanThrowsPageRelativePath = HomePageRelativePath + "/" + nameof(CustomSpanThrows); + internal const string DbOperationOutsideTransactionTestPageRelativePath = + HomePageRelativePath + "/" + nameof(DbOperationOutsideTransactionTest); + internal const string FailingDbCallTestPageRelativePath = HomePageRelativePath + "/" + nameof(FailingDbCallTest); + internal const string GenNSpansPageRelativePath = HomePageRelativePath + "/" + nameof(GenNSpans); + internal const string GetDotNetRuntimeDescriptionPageRelativePath = HomePageRelativePath + "/" + nameof(GetDotNetRuntimeDescription); + internal const string NotFoundPageRelativePath = HomePageRelativePath + "/" + nameof(NotFound); + internal const string ReturnBadRequestPageRelativePath = HomePageRelativePath + "/" + nameof(ReturnBadRequest); + internal const string SimpleDbTestPageRelativePath = HomePageRelativePath + "/" + nameof(SimpleDbTest); + internal const string ThrowsInvalidOperationPageRelativePath = HomePageRelativePath + "/" + nameof(ThrowsInvalidOperation); + internal const string ThrowsHttpException404PageRelativePath = HomePageRelativePath + "/" + nameof(ThrowsHttpException404); internal const string CaptureControllerActionAsSpanQueryStringKey = "captureControllerActionAsSpan"; - - internal const string ChildHttpSpanWithResponseForbiddenPath = HomePageRelativePath + "/" + nameof(ChildHttpSpanWithResponseForbidden); - internal const int ConcurrentDbTestNumberOfIterations = 10; - internal const string ConcurrentDbTestPageRelativePath = HomePageRelativePath + "/" + nameof(ConcurrentDbTest); internal const string ConcurrentDbTestSpanType = "concurrent"; - internal const string ContactPageRelativePath = HomePageRelativePath + "/" + nameof(Contact); internal const string ContactSpanPrefix = nameof(Contact); - - internal const string CustomChildSpanThrowsPageRelativePath = HomePageRelativePath + "/" + nameof(CustomChildSpanThrows); - internal const string CustomSpanThrowsInternalMethodName = nameof(CustomSpanThrowsInternal); - internal const string CustomSpanThrowsPageRelativePath = HomePageRelativePath + "/" + nameof(CustomSpanThrows); - - internal const string DbOperationOutsideTransactionTestPageRelativePath = - HomePageRelativePath + "/" + nameof(DbOperationOutsideTransactionTest); - internal const int DbOperationOutsideTransactionTestStatusCode = (int)HttpStatusCode.Accepted; internal const string DotNetRuntimeDescriptionHttpHeaderName = "DotNetRuntimeDescription"; internal const int DummyHttpStatusCode = 599; internal const string ExceptionMessage = "For testing purposes"; - internal const string FailingDbCallTestPageRelativePath = - HomePageRelativePath + "/" + nameof(FailingDbCallTest); - internal const int FailingDbCallTestStatusCode = (int)HttpStatusCode.OK; - internal const string GenNSpansPageRelativePath = HomePageRelativePath + "/" + nameof(GenNSpans); - - internal const string GetDotNetRuntimeDescriptionPageRelativePath = HomePageRelativePath + "/" + nameof(GetDotNetRuntimeDescription); - internal const string HomePageRelativePath = "Home"; - internal const string NumberOfSpansQueryStringKey = "numberOfSpans"; - internal const string ReturnBadRequestPageRelativePath = HomePageRelativePath + "/" + nameof(ReturnBadRequest); - internal const string SimpleDbTestPageRelativePath = HomePageRelativePath + "/" + nameof(SimpleDbTest); internal const string SpanActionSuffix = "_span_action"; internal const string SpanNameSuffix = "_span_name"; internal const string SpanSubtypeSuffix = "_span_subtype"; @@ -73,7 +66,6 @@ public class HomeController : ControllerBase internal const string TestChildSpanPrefix = "test_child"; internal const string TestSpanPrefix = "test"; - internal const string ThrowsInvalidOperationPageRelativePath = HomePageRelativePath + "/" + nameof(ThrowsInvalidOperation); internal static readonly Uri ChildHttpCallToExternalServiceUrl = new Uri("https://elastic.co"); internal static readonly Uri ChildHttpSpanWithResponseForbiddenUrl = new Uri("https://httpstat.us/403"); @@ -126,6 +118,10 @@ async Task GetContentFromUrl(Uri urlToGet) } } + public ActionResult Sample(int id) => Content(id.ToString()); + + public ActionResult NotFound() => HttpNotFound(); + internal static async Task CustomSpanThrowsInternal() { await Task.Delay(1); @@ -159,6 +155,13 @@ public async Task ThrowsNameCouldNotBeResolved() return null; } + public ActionResult ThrowsHttpException404() + { + var notFound = (int)HttpStatusCode.NotFound; + throw new HttpException(notFound, $"/{nameof(ThrowsHttpException404)} always returns " + + $"{notFound} ({HttpStatusCode.NotFound}) - for testing purposes"); + } + public ActionResult ThrowsInvalidOperation() => throw new InvalidOperationException($"/{nameof(ThrowsInvalidOperation)} always returns " + $"{(int)HttpStatusCode.InternalServerError} ({HttpStatusCode.InternalServerError}) - for testing purposes"); @@ -206,7 +209,8 @@ public HttpStatusCodeResult FailingDbCallTest() { try { - using (var dbCtx = new SampleDataDbContext()) dbCtx.Database.ExecuteSqlCommand("Select * From NonExistingTable"); + using var dbCtx = new SampleDataDbContext(); + dbCtx.Database.ExecuteSqlCommand("Select * From NonExistingTable"); } catch { diff --git a/sample/AspNetFullFrameworkSampleApp/Controllers/WebApiController.cs b/sample/AspNetFullFrameworkSampleApp/Controllers/WebApiController.cs new file mode 100644 index 000000000..ab2a266c3 --- /dev/null +++ b/sample/AspNetFullFrameworkSampleApp/Controllers/WebApiController.cs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Web.Http; + +namespace AspNetFullFrameworkSampleApp.Controllers +{ + public class WebApiController : ApiController + { + public const string Path = "api/WebApi"; + + public WebApiResponse Get() => + new WebApiResponse { Content = "This is an example response from a web api controller" }; + } + + public class WebApiResponse + { + public string Content { get; set; } + } +} diff --git a/sample/AspNetFullFrameworkSampleApp/Global.asax.cs b/sample/AspNetFullFrameworkSampleApp/Global.asax.cs index 82062dc55..007d8fb2c 100644 --- a/sample/AspNetFullFrameworkSampleApp/Global.asax.cs +++ b/sample/AspNetFullFrameworkSampleApp/Global.asax.cs @@ -6,6 +6,8 @@ using System.Collections.Specialized; using System.Diagnostics; using System.Web; +using System.Web.Http; +using System.Web.Http.Batch; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; @@ -24,6 +26,16 @@ protected void Application_Start() logger.Info("Current process ID: {ProcessID}, ELASTIC_APM_SERVER_URLS: {ELASTIC_APM_SERVER_URLS}", Process.GetCurrentProcess().Id, Environment.GetEnvironmentVariable("ELASTIC_APM_SERVER_URLS")); + // Web API setup + HttpBatchHandler batchHandler = new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer) + { + ExecutionOrder = BatchExecutionOrder.NonSequential + }; + var configuration = GlobalConfiguration.Configuration; + RouteConfig.RegisterWebApiRoutes(configuration, batchHandler); + GlobalConfiguration.Configure(WebApiConfig.Register); + + // MVC setup AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); diff --git a/sample/AspNetFullFrameworkSampleApp/Views/Diagnostics/Index.cshtml b/sample/AspNetFullFrameworkSampleApp/Views/Diagnostics/Index.cshtml index 282a4773e..7e2a4fe08 100644 --- a/sample/AspNetFullFrameworkSampleApp/Views/Diagnostics/Index.cshtml +++ b/sample/AspNetFullFrameworkSampleApp/Views/Diagnostics/Index.cshtml @@ -1,7 +1,8 @@ -@using System.Diagnostics -@model DiagnosticsViewModel - -Diagnostics +@model DiagnosticsViewModel +@{ + ViewBag.Title = "Diagnostics"; +} +@ViewBag.Title

Current process ID

diff --git a/sample/AspNetFullFrameworkSampleApp/Views/Shared/_Layout.cshtml b/sample/AspNetFullFrameworkSampleApp/Views/Shared/_Layout.cshtml index 6ec348721..e573d6287 100644 --- a/sample/AspNetFullFrameworkSampleApp/Views/Shared/_Layout.cshtml +++ b/sample/AspNetFullFrameworkSampleApp/Views/Shared/_Layout.cshtml @@ -25,7 +25,13 @@ @Html.ActionLink("Contact", "Contact", "Home", null, new { @class = "nav-link" }) + + @Html.Partial("_LoginPartial") diff --git a/sample/AspNetFullFrameworkSampleApp/Views/Web.config b/sample/AspNetFullFrameworkSampleApp/Views/Web.config index 75f946eb3..60d297323 100644 --- a/sample/AspNetFullFrameworkSampleApp/Views/Web.config +++ b/sample/AspNetFullFrameworkSampleApp/Views/Web.config @@ -12,6 +12,7 @@ + diff --git a/sample/AspNetFullFrameworkSampleApp/Webforms.aspx b/sample/AspNetFullFrameworkSampleApp/Webforms.aspx new file mode 100644 index 000000000..bf4b60314 --- /dev/null +++ b/sample/AspNetFullFrameworkSampleApp/Webforms.aspx @@ -0,0 +1,79 @@ +<%@ Page Language="C#" CodeBehind="Webforms.aspx.cs" Inherits="AspNetFullFrameworkSampleApp.Webforms" %> +<%@ Import Namespace="System.Web.Mvc" %> +<%@ Import Namespace="System.Web.Mvc.Html" %> +<%@ Import Namespace="Microsoft.AspNet.Identity" %> + + + + + + <%: Title %> - My ASP.NET Application + <%: System.Web.Optimization.Styles.Render("~/Content/css") %> + <%: System.Web.Optimization.Scripts.Render("~/bundles/modernizr") %> + + +
+ +
+
+
+

A <%: Title %> page

+

+ This is an example of a ASP.NET Webforms page +

+
+
+
+
+

© <%: DateTime.Now.Year %> - Elastic APM

+
+
+ +<%: System.Web.Optimization.Scripts.Render("~/bundles/jquery") %> +<%: System.Web.Optimization.Scripts.Render("~/bundles/bootstrap") %> +
+ + \ No newline at end of file diff --git a/sample/AspNetFullFrameworkSampleApp/Webforms.aspx.cs b/sample/AspNetFullFrameworkSampleApp/Webforms.aspx.cs new file mode 100644 index 000000000..dcb77a474 --- /dev/null +++ b/sample/AspNetFullFrameworkSampleApp/Webforms.aspx.cs @@ -0,0 +1,39 @@ +using System; +using System.Web.UI; +using System.Web.Mvc; +using AspNetFullFrameworkSampleApp.Controllers; + +namespace AspNetFullFrameworkSampleApp +{ + public partial class Webforms : Page + { + internal const string RoutedWebforms = nameof(RoutedWebforms); + + protected void Page_Load(object sender, EventArgs e) + { + Title = RouteData.RouteHandler == null + ? nameof(Webforms) + : RoutedWebforms; + + // create a Html helper to use to render links to MVC actions + var controllerContext = new ControllerContext(Request.RequestContext, new HomeController()); + Html = new HtmlHelper( + new ViewContext( + controllerContext, + new WebFormView(controllerContext, "~/" + nameof(Webforms) + ".aspx"), + new ViewDataDictionary(), + new TempDataDictionary(), + Response.Output + ), + new PageViewDataContainer()); + } + + public class PageViewDataContainer : IViewDataContainer + { + public ViewDataDictionary ViewData { get; set; } + } + + public HtmlHelper Html { get; private set; } + } +} + diff --git a/sample/SampleAspNetCoreApp/Areas/MyArea/Controllers/HomeController.cs b/sample/SampleAspNetCoreApp/Areas/MyArea/Controllers/HomeController.cs new file mode 100644 index 000000000..915b99db4 --- /dev/null +++ b/sample/SampleAspNetCoreApp/Areas/MyArea/Controllers/HomeController.cs @@ -0,0 +1,16 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Microsoft.AspNetCore.Mvc; + +namespace SampleAspNetCoreApp.Areas.MyArea.Controllers +{ + [Area("MyArea")] + public class HomeController : Controller + { + // GET + public IActionResult Index() => View(); + } +} diff --git a/sample/SampleAspNetCoreApp/Areas/MyArea/Views/Home/Index.cshtml b/sample/SampleAspNetCoreApp/Areas/MyArea/Views/Home/Index.cshtml new file mode 100644 index 000000000..6915f9054 --- /dev/null +++ b/sample/SampleAspNetCoreApp/Areas/MyArea/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ +@{ + ViewData["Title"] = "MyArea Home Page"; +} +@model List + +
+ + diff --git a/sample/SampleAspNetCoreApp/Areas/MyOtherArea/Controllers/HomeController.cs b/sample/SampleAspNetCoreApp/Areas/MyOtherArea/Controllers/HomeController.cs new file mode 100644 index 000000000..b96810cbb --- /dev/null +++ b/sample/SampleAspNetCoreApp/Areas/MyOtherArea/Controllers/HomeController.cs @@ -0,0 +1,16 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Microsoft.AspNetCore.Mvc; + +namespace SampleAspNetCoreApp.Areas.MyOtherArea.Controllers +{ + [Area("MyOtherArea")] + public class HomeController : Controller + { + // GET + public IActionResult Index() => View(); + } +} diff --git a/sample/SampleAspNetCoreApp/Areas/MyOtherArea/Views/Home/Index.cshtml b/sample/SampleAspNetCoreApp/Areas/MyOtherArea/Views/Home/Index.cshtml new file mode 100644 index 000000000..fc9d42f9f --- /dev/null +++ b/sample/SampleAspNetCoreApp/Areas/MyOtherArea/Views/Home/Index.cshtml @@ -0,0 +1,10 @@ +@{ + ViewData["Title"] = "MyOtherArea Home Page"; +} +@model List + +
+ + diff --git a/sample/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj b/sample/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj index e33925368..501091d87 100644 --- a/sample/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj +++ b/sample/SampleAspNetCoreApp/SampleAspNetCoreApp.csproj @@ -47,5 +47,11 @@ PreserveNewest PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/sample/SampleAspNetCoreApp/Startup.cs b/sample/SampleAspNetCoreApp/Startup.cs index 69756e025..cb44a2d68 100644 --- a/sample/SampleAspNetCoreApp/Startup.cs +++ b/sample/SampleAspNetCoreApp/Startup.cs @@ -70,6 +70,11 @@ public static void ConfigureAllExceptAgent(IApplicationBuilder app) app.UseStaticFiles(); app.UseCookiePolicy(); + ConfigureRoutingAndMvc(app); + } + + public static void ConfigureRoutingAndMvc(IApplicationBuilder app) + { #if NETCOREAPP3_0 || NETCOREAPP3_1 app.UseRouting(); @@ -77,7 +82,16 @@ public static void ConfigureAllExceptAgent(IApplicationBuilder app) app.UseEndpoints(endpoints => { - endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); + endpoints.MapAreaControllerRoute( + "MyOtherArea", + "MyOtherArea", + "MyOtherArea/{controller=Home}/{action=Index}/{id?}"); + endpoints.MapControllerRoute( + "MyArea", + "{area:exists}/{controller=Home}/{action=Index}/{id?}"); + endpoints.MapControllerRoute( + "default", + "{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllers(); endpoints.MapRazorPages(); }); @@ -86,6 +100,15 @@ public static void ConfigureAllExceptAgent(IApplicationBuilder app) app.UseMvc(routes => { + routes.MapAreaRoute( + "MyOtherArea", + "MyOtherArea", + "MyOtherArea/{controller=Home}/{action=Index}/{id?}"); + + routes.MapRoute( + "MyArea", + "{area:exists}/{controller=Home}/{action=Index}/{id?}"); + routes.MapRoute( "default", "{controller=Home}/{action=Index}/{id?}"); diff --git a/sample/SampleAspNetCoreApp/Views/_ViewStart.cshtml b/sample/SampleAspNetCoreApp/_ViewStart.cshtml similarity index 100% rename from sample/SampleAspNetCoreApp/Views/_ViewStart.cshtml rename to sample/SampleAspNetCoreApp/_ViewStart.cshtml diff --git a/src/Elastic.Apm.AspNetCore/WebRequestTransactionCreator.cs b/src/Elastic.Apm.AspNetCore/WebRequestTransactionCreator.cs index 55bcdfb92..d05a53fec 100644 --- a/src/Elastic.Apm.AspNetCore/WebRequestTransactionCreator.cs +++ b/src/Elastic.Apm.AspNetCore/WebRequestTransactionCreator.cs @@ -188,7 +188,7 @@ internal static void StopTransaction(Transaction transaction, HttpContext contex if (routeData != null && context.Response.StatusCode != StatusCodes.Status404NotFound) { logger?.Trace()?.Log("Calculating transaction name based on route data"); - var name = GetNameFromRouteContext(routeData); + var name = Transaction.GetNameFromRouteContext(routeData); if (!string.IsNullOrWhiteSpace(name)) transaction.Name = $"{context.Request.Method} {name}"; } @@ -311,50 +311,5 @@ string GetClaimWithFallbackValue(string claimType, string fallbackClaimType) return enumerable.Any() ? enumerable.First().Value : string.Empty; } } - - //credit: https://github.com/Microsoft/ApplicationInsights-aspnetcore - private static string GetNameFromRouteContext(IDictionary routeValues) - { - string name = null; - - if (routeValues.Count <= 0) return null; - - routeValues.TryGetValue("controller", out var controller); - var controllerString = controller == null ? string.Empty : controller.ToString(); - - if (!string.IsNullOrEmpty(controllerString)) - { - name = controllerString; - - routeValues.TryGetValue("action", out var action); - var actionString = action == null ? string.Empty : action.ToString(); - - if (!string.IsNullOrEmpty(actionString)) name += "/" + actionString; - - if (routeValues.Keys.Count <= 2) return name; - - // Add parameters - var sortedKeys = routeValues.Keys - .Where(key => - !string.Equals(key, "controller", StringComparison.OrdinalIgnoreCase) && - !string.Equals(key, "action", StringComparison.OrdinalIgnoreCase) && - !string.Equals(key, "!__route_group", StringComparison.OrdinalIgnoreCase)) - .OrderBy(key => key, StringComparer.OrdinalIgnoreCase) - .ToArray(); - - if (sortedKeys.Length <= 0) return name; - - var arguments = string.Join(@"/", sortedKeys); - name += " {" + arguments + "}"; - } - else - { - routeValues.TryGetValue("page", out var page); - var pageString = page == null ? string.Empty : page.ToString(); - if (!string.IsNullOrEmpty(pageString)) name = pageString; - } - - return name; - } } } diff --git a/src/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs b/src/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs index 43934a61f..19d435892 100644 --- a/src/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs +++ b/src/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs @@ -24,7 +24,6 @@ internal static class OpenIdClaimTypes internal const string Email = "email"; internal const string UserId = "sub"; } - public class ElasticApmModule : IHttpModule { private static bool _isCaptureHeadersEnabled; @@ -243,11 +242,56 @@ private void ProcessEndRequest(object eventSender) var httpApp = (HttpApplication)eventSender; var httpCtx = httpApp.Context; var httpResponse = httpCtx.Response; + var transaction = _currentTransaction; - if (_currentTransaction == null) return; + if (transaction == null) return; SendErrorEventIfPresent(httpCtx); + // update the transaction name based on route values, if applicable + if (transaction is Transaction t && !t.HasCustomName) + { + var values = httpApp.Request.RequestContext?.RouteData?.Values; + if (values?.Count > 0) + { + // Determine if the route data *actually* routed to a controller action or not i.e. + // we need to differentiate between + // 1. route data that didn't route to a controller action and returned a 404 + // 2. route data that did route to a controller action, and the action result returned a 404 + // + // In normal MVC setup, the former will set a HttpException with a 404 status code with System.Web.Mvc as the source. + // We need to check the source of the exception because we want to differentiate between a 404 HttpException from the + // framework and a 404 HttpException from the application. + if (httpCtx.Error is null || !(httpCtx.Error is HttpException httpException) || + httpException.Source != "System.Web.Mvc" || httpException.GetHttpCode() != StatusCodes.Status404NotFound) + { + // handle MVC areas. The area name will be included in the DataTokens. + object area = null; + httpApp.Request.RequestContext?.RouteData?.DataTokens?.TryGetValue("area", out area); + IDictionary routeData; + if (area != null) + { + routeData = new Dictionary(values.Count + 1); + foreach (var value in values) routeData.Add(value.Key, value.Value); + routeData.Add("area", area); + } + else + routeData = values; + + _logger?.Trace()?.Log("Calculating transaction name based on route data"); + var name = Transaction.GetNameFromRouteContext(routeData); + if (!string.IsNullOrWhiteSpace(name)) _currentTransaction.Name = $"{httpCtx.Request.HttpMethod} {name}"; + } + else + { + // dealing with a 404 HttpException that came from System.Web.Mvc + _logger?.Trace()? + .Log("Route data found but a HttpException with 404 status code was thrown from System.Web.Mvc - setting transaction name to 'unknown route"); + transaction.Name = $"{httpCtx.Request.HttpMethod} unknown route"; + } + } + } + _currentTransaction.Result = Transaction.StatusCodeToResult("HTTP", httpResponse.StatusCode); if (httpResponse.StatusCode >= 500) @@ -406,4 +450,9 @@ private static void SafeAgentSetup(string dbgInstanceName) } } } + + internal static class StatusCodes + { + public const int Status404NotFound = 404; + } } diff --git a/src/Elastic.Apm/Api/ITransaction.cs b/src/Elastic.Apm/Api/ITransaction.cs index 328c20609..3f627bef8 100644 --- a/src/Elastic.Apm/Api/ITransaction.cs +++ b/src/Elastic.Apm/Api/ITransaction.cs @@ -8,6 +8,14 @@ namespace Elastic.Apm.Api { + /// + /// A transaction describes an event captured by the APM agent instrumentation. They are a special kind of Span that have additional + /// attributes associated with them. + /// + /// + /// This interface is the public contract for a transaction. It is not intended to be used by a consumer of the agent to + /// provide different transaction implementations. + /// public interface ITransaction : IExecutionSegment { /// diff --git a/src/Elastic.Apm/Model/Transaction.cs b/src/Elastic.Apm/Model/Transaction.cs index 99c9dcd1c..b788c45a0 100644 --- a/src/Elastic.Apm/Model/Transaction.cs +++ b/src/Elastic.Apm/Model/Transaction.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading.Tasks; using Elastic.Apm.Api; using Elastic.Apm.Api.Constraints; @@ -218,7 +219,7 @@ void StartActivity() /// /// If true, then the transaction name was modified by external code, and transaction name should not be changed - /// or "fixed" automatically ref https://github.com/elastic/apm-agent-dotnet/pull/258. + /// or "fixed" automatically. /// [JsonIgnore] internal bool HasCustomName { get; private set; } @@ -423,5 +424,66 @@ public Task CaptureSpan(string name, string type, Func> fun => ExecutionSegmentCommon.CaptureSpan(StartSpanInternal(name, type, subType, action), func); internal static string StatusCodeToResult(string protocolName, int statusCode) => $"{protocolName} {statusCode.ToString()[0]}xx"; + + /// + /// Determines a name from the route values + /// + /// + /// Based on: https://github.com/Microsoft/ApplicationInsights-aspnetcore + /// + internal static string GetNameFromRouteContext(IDictionary routeValues) + { + if (routeValues.Count <= 0) return null; + + string name = null; + var count = routeValues.TryGetValue("controller", out var controller) ? 1 : 0; + var controllerString = controller == null ? string.Empty : controller.ToString(); + + if (!string.IsNullOrEmpty(controllerString)) + { + // Check for MVC areas + string areaString = null; + if (routeValues.TryGetValue("area", out var area)) + { + count++; + areaString = area.ToString(); + } + + name = !string.IsNullOrEmpty(areaString) + ? areaString + "/" + controllerString + : controllerString; + + count = routeValues.TryGetValue("action", out var action) ? (count + 1) : count; + var actionString = action == null ? string.Empty : action.ToString(); + + if (!string.IsNullOrEmpty(actionString)) name += "/" + actionString; + + // if there are no other key/values other than area/controller/action, skip parsing parameters + if (routeValues.Keys.Count == count) return name; + + // Add parameters + var sortedKeys = routeValues.Keys + .Where(key => + !string.Equals(key, "area", StringComparison.OrdinalIgnoreCase) && + !string.Equals(key, "controller", StringComparison.OrdinalIgnoreCase) && + !string.Equals(key, "action", StringComparison.OrdinalIgnoreCase) && + !string.Equals(key, "!__route_group", StringComparison.OrdinalIgnoreCase)) + .OrderBy(key => key, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (sortedKeys.Length <= 0) return name; + + var arguments = string.Join(@"/", sortedKeys); + name += " {" + arguments + "}"; + } + else + { + routeValues.TryGetValue("page", out var page); + var pageString = page == null ? string.Empty : page.ToString(); + if (!string.IsNullOrEmpty(pageString)) name = pageString; + } + + return name; + } } } diff --git a/test/Elastic.Apm.AspNetCore.Tests/Helper.cs b/test/Elastic.Apm.AspNetCore.Tests/Helper.cs index 1c784df95..409eb3fc8 100644 --- a/test/Elastic.Apm.AspNetCore.Tests/Helper.cs +++ b/test/Elastic.Apm.AspNetCore.Tests/Helper.cs @@ -51,7 +51,7 @@ internal static HttpClient GetClient(ApmAgent agent, WebApplicationFactory app.UseStaticFiles(); app.UseCookiePolicy(); - RegisterRoutingAndMvc(app); + Startup.ConfigureRoutingAndMvc(app); }); n.ConfigureServices(ConfigureServices); @@ -84,7 +84,7 @@ internal static HttpClient GetClientWithoutExceptionPage(ApmAgent agent, WebA app.UseElasticApm(agent, agent.Logger, new HttpDiagnosticsSubscriber(), new EfCoreDiagnosticsSubscriber()); } - RegisterRoutingAndMvc(app); + Startup.ConfigureRoutingAndMvc(app); }); n.ConfigureServices(ConfigureServices); @@ -106,45 +106,18 @@ internal static HttpClient GetClientWithoutDiagnosticListeners(ApmAgent agent app.UseMiddleware(agent.Tracer, agent); app.UseDeveloperExceptionPage(); - app.UseHsts(); - app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseCookiePolicy(); - RegisterRoutingAndMvc(app); + Startup.ConfigureRoutingAndMvc(app); }); n.ConfigureServices(ConfigureServices); }) .CreateClient(); - private static void RegisterRoutingAndMvc(IApplicationBuilder app) - { -#if NETCOREAPP3_0 || NETCOREAPP3_1 - app.UseRouting(); - - app.UseAuthentication(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); - endpoints.MapControllers(); - endpoints.MapRazorPages(); - }); -#else - app.UseAuthentication(); - - app.UseMvc(routes => - { - routes.MapRoute( - "default", - "{controller=Home}/{action=Index}/{id?}"); - }); -#endif - } - internal static void ConfigureServices(IServiceCollection services) { Startup.ConfigureServicesExceptMvc(services); diff --git a/test/Elastic.Apm.AspNetCore.Tests/TransactionNameTests.cs b/test/Elastic.Apm.AspNetCore.Tests/TransactionNameTests.cs index 627fe1853..e9b739df7 100644 --- a/test/Elastic.Apm.AspNetCore.Tests/TransactionNameTests.cs +++ b/test/Elastic.Apm.AspNetCore.Tests/TransactionNameTests.cs @@ -123,6 +123,38 @@ public async Task DefaultRouteParameterValues(bool diagnosticSourceOnly) _payloadSender.FirstTransaction.Context.Request.Url.Full.Should().Be("http://localhost/"); } + /// + /// Tests a URL that maps to an area route with an area route value and default values. Calls "/MyArea", which maps to "MyArea/Home/Index". + /// Makes sure "GET MyArea/Home/Index" is the Transaction.Name. + /// + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task Name_Should_Be_Area_Controller_Action_When_Mvc_Area_Controller_Action(bool diagnosticSourceOnly) + { + var httpClient = Helper.GetClient(_agent, _factory, diagnosticSourceOnly); + await httpClient.GetAsync("/MyArea"); + + _payloadSender.FirstTransaction.Name.Should().Be("GET MyArea/Home/Index"); + _payloadSender.FirstTransaction.Context.Request.Url.Full.Should().Be("http://localhost/MyArea"); + } + + /// + /// Tests a URL that maps to an explicit area route with default values. Calls "/MyOtherArea", which maps to "MyOtherArea/Home/Index". + /// Makes sure "GET MyOtherArea/Home/Index" is the Transaction.Name. + /// + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task Name_Should_Be_Area_Controller_Action_When_Mvc_Area_Controller_Action_With_Area_Route(bool diagnosticSourceOnly) + { + var httpClient = Helper.GetClient(_agent, _factory, diagnosticSourceOnly); + await httpClient.GetAsync("/MyOtherArea"); + + _payloadSender.FirstTransaction.Name.Should().Be("GET MyOtherArea/Home/Index"); + _payloadSender.FirstTransaction.Context.Request.Url.Full.Should().Be("http://localhost/MyOtherArea"); + } + /// /// Calls a URL that maps to no route and causes a 404 /// diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/CaptureHeadersConfigDisabledTest.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/CaptureHeadersConfigDisabledTest.cs index d930a7ac5..2053b061b 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/CaptureHeadersConfigDisabledTest.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/CaptureHeadersConfigDisabledTest.cs @@ -22,7 +22,7 @@ public CaptureHeadersConfigDisabledTest(ITestOutputHelper xUnitOutputHelper) [MemberData(nameof(AllSampleAppUrlPaths))] public async Task Test(SampleAppUrlPathData sampleAppUrlPathData) { - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); await WaitAndVerifyReceivedDataSharedConstraints(sampleAppUrlPathData); } diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/CentralConfigTests.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/CentralConfigTests.cs index 1b589056b..19ef20866 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/CentralConfigTests.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/CentralConfigTests.cs @@ -253,9 +253,9 @@ await ConfigState.UpdateAndWaitForAgentToApply(new Dictionary async Task SendRequestAssertReceivedData(int maxSpans, bool isSampled, int spansToExecCount) { - var urlPath = srcPageData.RelativeUrlPath + $"?{HomeController.NumberOfSpansQueryStringKey}={spansToExecCount}"; + var urlPath = srcPageData.RelativePath + $"?{HomeController.NumberOfSpansQueryStringKey}={spansToExecCount}"; var pageData = srcPageData.Clone(urlPath, spansCount: isSampled ? Math.Min(spansToExecCount, maxSpans) : 0); - await SendGetRequestToSampleAppAndVerifyResponse(pageData.RelativeUrlPath, pageData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(pageData.Uri, pageData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/CustomEnvironmentViaSettings.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/CustomEnvironmentViaSettings.cs index 15626a9a0..27813abc6 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/CustomEnvironmentViaSettings.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/CustomEnvironmentViaSettings.cs @@ -24,7 +24,7 @@ public CustomEnvironmentViaSettings(ITestOutputHelper xUnitOutputHelper) [MemberData(nameof(AllSampleAppUrlPaths))] public async Task Test(SampleAppUrlPathData sampleAppUrlPathData) { - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); await WaitAndVerifyReceivedDataSharedConstraints(sampleAppUrlPathData); } } diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/CustomFlushInterval.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/CustomFlushInterval.cs index f69301f09..6bb3cd48c 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/CustomFlushInterval.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/CustomFlushInterval.cs @@ -27,7 +27,7 @@ public CustomFlushInterval(ITestOutputHelper xUnitOutputHelper) [MemberData(nameof(AllSampleAppUrlPaths))] public async Task Test(SampleAppUrlPathData sampleAppUrlPathData) { - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); await WaitAndVerifyReceivedDataSharedConstraints(sampleAppUrlPathData); } } diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/CustomServiceNameSetViaSettings.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/CustomServiceNameSetViaSettings.cs index da5d5af54..6a8986ce8 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/CustomServiceNameSetViaSettings.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/CustomServiceNameSetViaSettings.cs @@ -24,7 +24,7 @@ public CustomServiceNameSetViaSettings(ITestOutputHelper xUnitOutputHelper) [MemberData(nameof(AllSampleAppUrlPaths))] public async Task Test(SampleAppUrlPathData sampleAppUrlPathData) { - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); await WaitAndVerifyReceivedDataSharedConstraints(sampleAppUrlPathData); } } diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/CustomServiceNodeNameViaSettings.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/CustomServiceNodeNameViaSettings.cs index aecb99e38..31930f9cb 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/CustomServiceNodeNameViaSettings.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/CustomServiceNodeNameViaSettings.cs @@ -24,7 +24,7 @@ public CustomServiceNodeNameSetViaSettings(ITestOutputHelper xUnitOutputHelper) [MemberData(nameof(AllSampleAppUrlPaths))] public async Task Test(SampleAppUrlPathData sampleAppUrlPathData) { - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); await WaitAndVerifyReceivedDataSharedConstraints(sampleAppUrlPathData); } } diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/DbSpanTests.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/DbSpanTests.cs index bd6756718..a0f40634b 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/DbSpanTests.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/DbSpanTests.cs @@ -37,7 +37,7 @@ public async Task SimpleDbTest() // 4) SELECT for // if (dbCtx.Set().First().Name != simpleDbTestSampleDataName) var pageData = new SampleAppUrlPathData(HomeController.SimpleDbTestPageRelativePath, 200, spansCount: 4); - await SendGetRequestToSampleAppAndVerifyResponse(pageData.RelativeUrlPath, pageData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(pageData.Uri, pageData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { @@ -96,7 +96,7 @@ public async Task ConcurrentDbTest() // var pageData = new SampleAppUrlPathData(HomeController.ConcurrentDbTestPageRelativePath, 200 , spansCount: numberOfConcurrentIterations * 3 + 5); - await SendGetRequestToSampleAppAndVerifyResponse(pageData.RelativeUrlPath, pageData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(pageData.Uri, pageData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { @@ -187,7 +187,7 @@ public async Task DbOperationOutsideTransactionTest() var pageData = new SampleAppUrlPathData(HomeController.DbOperationOutsideTransactionTestPageRelativePath , HomeController.DbOperationOutsideTransactionTestStatusCode); - await SendGetRequestToSampleAppAndVerifyResponse(pageData.RelativeUrlPath, pageData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(pageData.Uri, pageData.StatusCode); await WaitAndVerifyReceivedDataSharedConstraints(pageData); } @@ -200,7 +200,7 @@ public async Task FailingDbCallTest() var pageData = new SampleAppUrlPathData(HomeController.FailingDbCallTestPageRelativePath , HomeController.FailingDbCallTestStatusCode); - await SendGetRequestToSampleAppAndVerifyResponse(pageData.RelativeUrlPath, pageData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(pageData.Uri, pageData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/DistributedTracingTests.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/DistributedTracingTests.cs index 99844f8fa..47c7c1c06 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/DistributedTracingTests.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/DistributedTracingTests.cs @@ -32,7 +32,7 @@ public async Task ContactPageCallsAboutPageAndExternalUrl() var rootTxData = SampleAppUrlPaths.ContactPage; var childTxData = SampleAppUrlPaths.AboutPage; - await SendGetRequestToSampleAppAndVerifyResponse(rootTxData.RelativeUrlPath, rootTxData.StatusCode, addTraceContextHeaders: true); + await SendGetRequestToSampleAppAndVerifyResponse(rootTxData.Uri, rootTxData.StatusCode, addTraceContextHeaders: true); await WaitAndCustomVerifyReceivedData(receivedData => { @@ -60,11 +60,11 @@ public async Task ContactPageCallsAboutPageAndExternalUrlWithWrappingControllerA { const string queryString = "?" + HomeController.CaptureControllerActionAsSpanQueryStringKey + "=true"; var rootTxData = SampleAppUrlPaths.ContactPage.Clone( - $"{SampleAppUrlPaths.ContactPage.RelativeUrlPath}{queryString}", + $"{SampleAppUrlPaths.ContactPage.RelativePath}{queryString}", spansCount: SampleAppUrlPaths.ContactPage.SpansCount + 1); - var childTxData = SampleAppUrlPaths.AboutPage.Clone($"{SampleAppUrlPaths.AboutPage.RelativeUrlPath}{queryString}"); + var childTxData = SampleAppUrlPaths.AboutPage.Clone($"{SampleAppUrlPaths.AboutPage.RelativePath}{queryString}"); - await SendGetRequestToSampleAppAndVerifyResponse(rootTxData.RelativeUrlPath, rootTxData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(rootTxData.Uri, rootTxData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { @@ -109,7 +109,7 @@ public async Task CallReturnBadRequestTest() var rootTxData = SampleAppUrlPaths.CallReturnBadRequestPage; var childTxData = SampleAppUrlPaths.ReturnBadRequestPage; - await SendGetRequestToSampleAppAndVerifyResponse(rootTxData.RelativeUrlPath, rootTxData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(rootTxData.Uri, rootTxData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { @@ -119,79 +119,6 @@ await WaitAndCustomVerifyReceivedData(receivedData => }); } - [AspNetFullFrameworkFact] - public async Task CallSoap11Request() - { - var rootTxData = SampleAppUrlPaths.CallSoapServiceProtocolV11; - var fullUrl = Consts.SampleApp.RootUrl + "/" + rootTxData.RelativeUrlPath; - var action = "Ping"; - - var httpContent = new StringContent($@" - - - <{action} xmlns=""http://tempuri.org/"" /> - - ", Encoding.UTF8, "text/xml"); - - using (var client = new HttpClient()) - { - var request = new HttpRequestMessage() - { - RequestUri = new Uri(fullUrl), - Method = HttpMethod.Post, - Content = httpContent - }; - - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml")); - request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml"); - request.Headers.Add("SOAPAction", $"http://tempuri.org/{action}"); - - var response = client.SendAsync(request).Result; - } - - await WaitAndCustomVerifyReceivedData(receivedData => - { - receivedData.Transactions.Count.Should().Be(1); - receivedData.Transactions.First().Name.Should().EndWith(action); - }); - } - - [AspNetFullFrameworkFact] - public async Task CallSoap12Request() - { - var rootTxData = SampleAppUrlPaths.CallSoapServiceProtocolV12; - var fullUrl = Consts.SampleApp.RootUrl + "/" + rootTxData.RelativeUrlPath; - var action = "Ping"; - - var httpContent = new StringContent($@" - - - <{action} xmlns=""http://tempuri.org/"" /> - - ", Encoding.UTF8, "application/soap+xml"); - - using (var client = new HttpClient()) - { - var request = new HttpRequestMessage() - { - RequestUri = new Uri(fullUrl), - Method = HttpMethod.Post, - Content = httpContent - }; - - request.Headers.Accept.Clear(); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); - - var response = client.SendAsync(request).Result; - } - - await WaitAndCustomVerifyReceivedData(receivedData => - { - receivedData.Transactions.Count.Should().Be(1); - receivedData.Transactions.First().Name.Should().EndWith(action); - }); - } - private static void VerifyRootChildTransactions( ReceivedData receivedData, SampleAppUrlPathData rootTxData, @@ -223,24 +150,21 @@ out TransactionDto childTxOut private static TransactionDto FindAndVerifyTransaction(ReceivedData receivedData, SampleAppUrlPathData txData) { - var txUrlPath = Consts.SampleApp.RootUrlPath + "/" + txData.RelativeUrlPath; - var expectedFullUrlAsString = "http://" + Consts.SampleApp.Host + txUrlPath; - var expectedFullUrl = new Uri(expectedFullUrlAsString); - var queryString = expectedFullUrl.Query; + var queryString = txData.Uri.Query; if (queryString.IsEmpty()) { // Uri.Query returns empty string both when query string is empty ("http://host/path?") and // when there's no query string at all ("http://host/path") so we need a way to distinguish between these cases - if (expectedFullUrlAsString.IndexOf('?') == -1) + if (txData.Uri.ToString().IndexOf('?') == -1) queryString = null; } else if (queryString[0] == '?') queryString = queryString.Substring(1, queryString.Length - 1); - var transaction = receivedData.Transactions.Single(tx => tx.Context.Request.Url.PathName == expectedFullUrl.AbsolutePath); + var transaction = receivedData.Transactions.Single(tx => tx.Context.Request.Url.PathName == txData.Uri.AbsolutePath); transaction.Context.Request.Method.ToUpperInvariant().Should().Be("GET"); - transaction.Context.Request.Url.Full.Should().Be(expectedFullUrlAsString); - transaction.Context.Request.Url.PathName.Should().Be(expectedFullUrl.AbsolutePath); + transaction.Context.Request.Url.Full.Should().Be(txData.Uri.ToString()); + transaction.Context.Request.Url.PathName.Should().Be(txData.Uri.AbsolutePath); transaction.Context.Request.Url.Search.Should().Be(queryString); transaction.Context.Response.Finished.Should().BeTrue(); @@ -256,7 +180,7 @@ private static TransactionDto FindAndVerifyTransaction(ReceivedData receivedData transaction.Context.User.Should().BeNull(); transaction.IsSampled.Should().BeTrue(); - transaction.Name.Should().Be($"GET {expectedFullUrl.AbsolutePath}"); + transaction.Name.Should().Be($"GET {txData.RelativePath}"); transaction.SpanCount.Started.Should().Be(txData.SpansCount); transaction.SpanCount.Dropped.Should().Be(0); diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/ErrorsTests.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/ErrorsTests.cs index 39838c2e1..e78133798 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/ErrorsTests.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/ErrorsTests.cs @@ -25,7 +25,7 @@ public ErrorsTests(ITestOutputHelper xUnitOutputHelper) : base(xUnitOutputHelper public async Task CustomSpanThrowsTest() { var errorPageData = SampleAppUrlPaths.CustomSpanThrowsExceptionPage; - await SendGetRequestToSampleAppAndVerifyResponse(errorPageData.RelativeUrlPath, errorPageData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(errorPageData.Uri, errorPageData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { @@ -62,7 +62,7 @@ await WaitAndCustomVerifyReceivedData(receivedData => public async Task HttpCallWithResponseForbidden() { var pageData = SampleAppUrlPaths.ChildHttpSpanWithResponseForbiddenPage; - await SendGetRequestToSampleAppAndVerifyResponse(pageData.RelativeUrlPath, pageData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(pageData.Uri, pageData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { @@ -82,7 +82,7 @@ await WaitAndCustomVerifyReceivedData(receivedData => public async Task CustomChildSpanThrowsTest() { var errorPageData = SampleAppUrlPaths.CustomChildSpanThrowsExceptionPage; - await SendGetRequestToSampleAppAndVerifyResponse(errorPageData.RelativeUrlPath, errorPageData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(errorPageData.Uri, errorPageData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { @@ -134,8 +134,8 @@ await WaitAndCustomVerifyReceivedData(receivedData => [AspNetFullFrameworkFact] public async Task PageThatDoesNotExit_test() { - var pageData = SampleAppUrlPaths.PageThatDoesNotExit; - await SendGetRequestToSampleAppAndVerifyResponse(pageData.RelativeUrlPath, pageData.StatusCode); + var pageData = SampleAppUrlPaths.PageThatDoesNotExist; + await SendGetRequestToSampleAppAndVerifyResponse(pageData.Uri, pageData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { @@ -147,7 +147,7 @@ await WaitAndCustomVerifyReceivedData(receivedData => VerifyTransactionError(error, transaction); error.Exception.Type.Should().Be("System.Web.HttpException"); - error.Exception.Message.Should().ContainAll(pageData.RelativeUrlPath, "not found"); + error.Exception.Message.Should().ContainAll(pageData.Uri.PathAndQuery, "not found"); }); } diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/GlobalLabelsTests.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/GlobalLabelsTests.cs index 0bcf886fa..0b03af854 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/GlobalLabelsTests.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/GlobalLabelsTests.cs @@ -38,7 +38,7 @@ private static string GlobalLabelsToRawOptionValue(IReadOnlyDictionary @@ -76,7 +76,7 @@ private string GetAspNetVersionFromErrorPage(string errorPage) public async Task ServiceRuntimeTest() { var page = SampleAppUrlPaths.GetDotNetRuntimeDescriptionPage; - var sampleAppResponse = await SendGetRequestToSampleAppAndVerifyResponse(page.RelativeUrlPath, page.StatusCode); + var sampleAppResponse = await SendGetRequestToSampleAppAndVerifyResponse(page.Uri, page.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/MetricsTestsBase.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/MetricsTestsBase.cs index 44a7c9bdb..216d20edb 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/MetricsTestsBase.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/MetricsTestsBase.cs @@ -26,7 +26,7 @@ protected async Task VerifyMetricsBasicConstraintsImpl() { // Send any request to the sample application to make sure it's running since IIS might start worker process lazily var sampleAppUrlPathData = RandomSampleAppUrlPath(); - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode, /* timeHttpCall: */ false); // Wait enough time to give agent a chance to gather all the metrics diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/SampleRate0Tests.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/SampleRate0Tests.cs index 42aa2d537..ff3d7fafa 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/SampleRate0Tests.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/SampleRate0Tests.cs @@ -21,7 +21,7 @@ public SampleRate0Tests(ITestOutputHelper xUnitOutputHelper) : base(xUnitOutputH public async Task Test(SampleAppUrlPathData sampleAppUrlPathDataForSampled) { var sampleAppUrlPathData = sampleAppUrlPathDataForSampled.Clone(spansCount: 0); - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/TestWithDefaultSettings.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/TestWithDefaultSettings.cs index dda6da7dd..59ec6a5fd 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/TestWithDefaultSettings.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/TestWithDefaultSettings.cs @@ -19,7 +19,7 @@ public TestWithDefaultSettings(ITestOutputHelper xUnitOutputHelper) : base(xUnit [MemberData(nameof(AllSampleAppUrlPaths))] public async Task TestVariousPages(SampleAppUrlPathData sampleAppUrlPathData) { - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); await WaitAndVerifyReceivedDataSharedConstraints(sampleAppUrlPathData); } @@ -35,8 +35,8 @@ public async Task TestVariousPages(SampleAppUrlPathData sampleAppUrlPathData) // key "?" without value public async Task QueryStringTests(string queryString) { - var homePageAndQueryString = SampleAppUrlPaths.HomePage.Clone(SampleAppUrlPaths.HomePage.RelativeUrlPath + $"?{queryString}"); - await SendGetRequestToSampleAppAndVerifyResponse(homePageAndQueryString.RelativeUrlPath, homePageAndQueryString.StatusCode); + var homePageAndQueryString = SampleAppUrlPaths.HomePage.Clone(SampleAppUrlPaths.HomePage.RelativePath + $"?{queryString}"); + await SendGetRequestToSampleAppAndVerifyResponse(homePageAndQueryString.Uri, homePageAndQueryString.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs index 43ac3446a..b1460030b 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Threading.Tasks; using AspNetFullFrameworkSampleApp; +using AspNetFullFrameworkSampleApp.Asmx; using AspNetFullFrameworkSampleApp.Controllers; using Elastic.Apm.Api; using Elastic.Apm.Config; @@ -53,6 +54,8 @@ public class TestsBase : LoggingTestBase, IAsyncLifetime private readonly bool _startMockApmServer; private readonly DateTime _testStartTime = DateTime.UtcNow; + protected readonly HttpClient HttpClient; + protected TestsBase( ITestOutputHelper xUnitOutputHelper, bool startMockApmServer = true, @@ -72,6 +75,7 @@ protected TestsBase( _sampleAppLogEnabled = sampleAppLogEnabled; _sampleAppLogFilePath = GetSampleAppLogFilePath(); + HttpClient = new HttpClient(); EnvVarsToSetForSampleAppPool = envVarsToSetForSampleAppPool == null ? new Dictionary() @@ -114,7 +118,10 @@ internal static class SampleAppUrlPaths internal static readonly SampleAppUrlPathData HomePage = new SampleAppUrlPathData(HomeController.HomePageRelativePath, 200); - internal static readonly SampleAppUrlPathData PageThatDoesNotExit = + internal static readonly SampleAppUrlPathData NotFoundPage = + new SampleAppUrlPathData(HomeController.NotFoundPageRelativePath, 404, errorsCount: 1); + + internal static readonly SampleAppUrlPathData PageThatDoesNotExist = new SampleAppUrlPathData("dummy_URL_path_to_page_that_does_not_exist", 404, errorsCount: 1); internal static readonly List AllPaths = new List @@ -123,7 +130,7 @@ internal static class SampleAppUrlPaths HomePage, ContactPage, CustomSpanThrowsExceptionPage, - PageThatDoesNotExit + PageThatDoesNotExist }; /// @@ -164,6 +171,21 @@ internal static class SampleAppUrlPaths internal static readonly SampleAppUrlPathData ThrowsInvalidOperationPage = new SampleAppUrlPathData(HomeController.ThrowsInvalidOperationPageRelativePath, 500, errorsCount: 1, outcome: Outcome.Failure); + + internal static readonly SampleAppUrlPathData ThrowsHttpException404PageRelativePath = + new SampleAppUrlPathData(HomeController.ThrowsHttpException404PageRelativePath, 404, errorsCount: 1, outcome: Outcome.Failure); + + internal static readonly SampleAppUrlPathData MyAreaHomePage = + new SampleAppUrlPathData(AspNetFullFrameworkSampleApp.Areas.MyArea.Controllers.HomeController.HomePageRelativePath, 200); + + internal static readonly SampleAppUrlPathData WebformsPage = + new SampleAppUrlPathData(nameof(Webforms) + ".aspx", 200); + + internal static readonly SampleAppUrlPathData RoutedWebformsPage = + new SampleAppUrlPathData(nameof(Webforms.RoutedWebforms), 200); + + internal static readonly SampleAppUrlPathData WebApiPage = + new SampleAppUrlPathData(WebApiController.Path, 200); } private TimedEvent? _sampleAppClientCallTiming; @@ -221,7 +243,7 @@ private string GetSampleAppLogFilePath() private static string BuildApmServerUrl(int apmServerPort) => $"http://localhost:{apmServerPort}/"; - protected async Task SendGetRequestToSampleAppAndVerifyResponse(string relativeUrlPath, int expectedStatusCode, + protected async Task SendGetRequestToSampleAppAndVerifyResponse(Uri uri, int expectedStatusCode, bool timeHttpCall = true, bool addTraceContextHeaders = false ) { @@ -234,16 +256,15 @@ protected async Task SendGetRequestToSampleAppAndVerifyRespon } try { - using (var httpClient = new HttpClient()) + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri); + if (addTraceContextHeaders) { - if (addTraceContextHeaders) - { - httpClient.DefaultRequestHeaders.Add("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); - httpClient.DefaultRequestHeaders.Add("tracestate", "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"); - } - var response = await SendGetRequestToSampleAppAndVerifyResponseImpl(httpClient, relativeUrlPath, expectedStatusCode); - return new SampleAppResponse(response.Headers, await response.Content.ReadAsStringAsync()); + httpRequestMessage.Headers.Add("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"); + httpRequestMessage.Headers.Add("tracestate", "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE"); } + + var response = await SendGetRequestToSampleAppAndVerifyResponseImpl(httpRequestMessage, expectedStatusCode); + return new SampleAppResponse(response.Headers, await response.Content.ReadAsStringAsync()); } finally { @@ -261,14 +282,13 @@ protected async Task SendGetRequestToSampleAppAndVerifyRespon } private async Task SendGetRequestToSampleAppAndVerifyResponseImpl( - HttpClient httpClient, - string relativeUrlPath, + HttpRequestMessage httpRequestMessage, int expectedStatusCode ) { - var url = Consts.SampleApp.RootUrl + "/" + relativeUrlPath; + var url = httpRequestMessage.RequestUri; _logger.Debug()?.Log("Sending request with URL: {url} and expected status code: {HttpStatusCode}...", url, expectedStatusCode); - var response = await httpClient.GetAsync(url); + var response = await HttpClient.SendAsync(httpRequestMessage).ConfigureAwait(false); _logger.Debug() ?.Log("Request sent. Actual status code: {HttpStatusCode} ({HttpStatusCodeEnum})", (int)response.StatusCode, response.StatusCode); @@ -422,10 +442,9 @@ private void LogSampleAppLogFileContent() private async Task LogSampleAppDiagnosticsPage() { - var httpClient = new HttpClient(); - const string url = Consts.SampleApp.RootUrl + "/" + DiagnosticsController.DiagnosticsPageRelativePath; + var url = new SampleAppUri(DiagnosticsController.DiagnosticsPageRelativePath); _logger.Debug()?.Log("Getting content of sample application diagnostics page ({url})...", url); - var response = await httpClient.GetAsync(url); + var response = await HttpClient.GetAsync(url); _logger.Debug() ?.Log("Received sample application's diagnostics page. Status code: {HttpStatusCode} ({HttpStatusCodeEnum})", (int)response.StatusCode, response.StatusCode); @@ -487,37 +506,31 @@ protected void VerifyReceivedDataSharedConstraints(SampleAppUrlPathData sampleAp receivedData.Spans.Count.Should().Be(sampleAppUrlPathData.SpansCount); receivedData.Errors.Count.Should().Be(sampleAppUrlPathData.ErrorsCount); - // ReSharper disable once InvertIf - if (receivedData.Transactions.Count == 1) + if (receivedData.Transactions.Count != 1) + return; + + var transaction = receivedData.Transactions.First(); + if (transaction.Context != null) { - var transaction = receivedData.Transactions.First(); + transaction.Context.Request.Url.Full.Should().Be(sampleAppUrlPathData.Uri.AbsoluteUri); + transaction.Context.Request.Url.PathName.Should().Be(sampleAppUrlPathData.Uri.AbsolutePath); - if (transaction.Context != null) + if (string.IsNullOrEmpty(sampleAppUrlPathData.Uri.Query)) + transaction.Context.Request.Url.Search.Should().BeNull(); + else { - transaction.Context.Request.Url.Full.Should().Be(Consts.SampleApp.RootUrl + "/" + sampleAppUrlPathData.RelativeUrlPath); - - var questionMarkIndex = sampleAppUrlPathData.RelativeUrlPath.IndexOf('?'); - if (questionMarkIndex == -1) - { - transaction.Context.Request.Url.PathName.Should() - .Be(Consts.SampleApp.RootUrlPath + "/" + sampleAppUrlPathData.RelativeUrlPath); - transaction.Context.Request.Url.Search.Should().BeNull(); - } - else - { - transaction.Context.Request.Url.PathName.Should() - .Be(Consts.SampleApp.RootUrlPath + "/" + sampleAppUrlPathData.RelativeUrlPath.Substring(0, questionMarkIndex)); - transaction.Context.Request.Url.Search.Should().Be(sampleAppUrlPathData.RelativeUrlPath.Substring(questionMarkIndex + 1)); - } - - transaction.Context.Response.StatusCode.Should().Be(sampleAppUrlPathData.StatusCode); - transaction.Outcome.Should().Be(sampleAppUrlPathData.Outcome); + // Uri.Query always unescapes the querystring so don't use it, and instead get the escaped querystring. + var queryString = sampleAppUrlPathData.Uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped); + transaction.Context.Request.Url.Search.Should().Be(queryString); } - var httpStatusFirstDigit = sampleAppUrlPathData.StatusCode / 100; - transaction.Result.Should().Be($"HTTP {httpStatusFirstDigit}xx"); - transaction.SpanCount.Started.Should().Be(sampleAppUrlPathData.SpansCount); + transaction.Context.Response.StatusCode.Should().Be(sampleAppUrlPathData.StatusCode); + transaction.Outcome.Should().Be(sampleAppUrlPathData.Outcome); } + + var httpStatusFirstDigit = sampleAppUrlPathData.StatusCode / 100; + transaction.Result.Should().Be($"HTTP {httpStatusFirstDigit}xx"); + transaction.SpanCount.Started.Should().Be(sampleAppUrlPathData.SpansCount); } internal void VerifySpanNameTypeSubtypeAction(SpanDto span, string spanPrefix) @@ -800,20 +813,39 @@ protected class AgentConfiguration internal string HostName; } + /// + /// Uri for the sample application + /// + public class SampleAppUri + { + private readonly UriBuilder _builder; + public SampleAppUri(string relativePathAndQuery) => + _builder = new UriBuilder($"{Consts.SampleApp.RootUrl}/{relativePathAndQuery.TrimStart('/')}") { Port = -1 }; + + public Uri Uri => _builder.Uri; + public string RelativePath => _builder.Uri.AbsolutePath.Substring(Consts.SampleApp.RootUrlPath.Length + 1); + public static implicit operator Uri(SampleAppUri sampleAppUri) => sampleAppUri.Uri; + } + public class SampleAppUrlPathData { + private readonly SampleAppUri _sampleAppUri; + public readonly int ErrorsCount; public readonly Outcome Outcome; - public readonly string RelativeUrlPath; public readonly int SpansCount; + private readonly string _relativePathAndQuery; public readonly int StatusCode; public readonly int TransactionsCount; + public Uri Uri => _sampleAppUri.Uri; + public string RelativePath => _sampleAppUri.RelativePath; - public SampleAppUrlPathData(string relativeUrlPath, int statusCode, int transactionsCount = 1, int spansCount = 0, int errorsCount = 0, + public SampleAppUrlPathData(string relativePathAndQuery, int statusCode, int transactionsCount = 1, int spansCount = 0, int errorsCount = 0, Outcome outcome = Outcome.Success ) { - RelativeUrlPath = relativeUrlPath; + _sampleAppUri = new SampleAppUri(relativePathAndQuery); + _relativePathAndQuery = relativePathAndQuery; StatusCode = statusCode; TransactionsCount = transactionsCount; SpansCount = spansCount; @@ -822,14 +854,14 @@ public SampleAppUrlPathData(string relativeUrlPath, int statusCode, int transact } public SampleAppUrlPathData Clone( - string relativeUrlPath = null, + string relativePathAndQuery = null, int? status = null, int? transactionsCount = null, int? spansCount = null, int? errorsCount = null, Outcome outcome = Outcome.Success ) => new SampleAppUrlPathData( - relativeUrlPath ?? RelativeUrlPath, + relativePathAndQuery ?? _relativePathAndQuery, status ?? StatusCode, transactionsCount ?? TransactionsCount, spansCount ?? SpansCount, diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/TestsWithApmServerStopped.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/TestsWithApmServerStopped.cs index 1656ad3e7..a8de0e1e0 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/TestsWithApmServerStopped.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/TestsWithApmServerStopped.cs @@ -16,6 +16,6 @@ public TestsWithApmServerStopped(ITestOutputHelper xUnitOutputHelper) : base(xUn [AspNetFullFrameworkTheory] [MemberData(nameof(AllSampleAppUrlPaths))] public async Task SampleAppShouldBeAvailableEvenWhenApmServerStopped(SampleAppUrlPathData sampleAppUrlPathData) => - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); } } diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/TestsWithSampleAppLogDisabled.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/TestsWithSampleAppLogDisabled.cs index dec9b4401..45f6ef781 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/TestsWithSampleAppLogDisabled.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/TestsWithSampleAppLogDisabled.cs @@ -17,7 +17,7 @@ public TestsWithSampleAppLogDisabled(ITestOutputHelper xUnitOutputHelper) : base [MemberData(nameof(AllSampleAppUrlPaths))] public async Task TestVariousPages(SampleAppUrlPathData sampleAppUrlPathData) { - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); await WaitAndVerifyReceivedDataSharedConstraints(sampleAppUrlPathData); } diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/TransactionIgnoreUrlsTest.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/TransactionIgnoreUrlsTest.cs index f668c5676..45749cca8 100644 --- a/test/Elastic.Apm.AspNetFullFramework.Tests/TransactionIgnoreUrlsTest.cs +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/TransactionIgnoreUrlsTest.cs @@ -23,7 +23,7 @@ public TransactionIgnoreUrlsTest(ITestOutputHelper xUnitOutputHelper) public async Task Test() { var sampleAppUrlPathData = SampleAppUrlPaths.HomePage; - await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.RelativeUrlPath, sampleAppUrlPathData.StatusCode); + await SendGetRequestToSampleAppAndVerifyResponse(sampleAppUrlPathData.Uri, sampleAppUrlPathData.StatusCode); await WaitAndCustomVerifyReceivedData(receivedData => { diff --git a/test/Elastic.Apm.AspNetFullFramework.Tests/TransactionNameTests.cs b/test/Elastic.Apm.AspNetFullFramework.Tests/TransactionNameTests.cs new file mode 100644 index 000000000..4e8e6b570 --- /dev/null +++ b/test/Elastic.Apm.AspNetFullFramework.Tests/TransactionNameTests.cs @@ -0,0 +1,208 @@ +// Licensed to Elasticsearch B.V under +// one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.AspNetFullFramework.Tests +{ + [Collection(Consts.AspNetFullFrameworkTestsCollection)] + public class TransactionNameTests : TestsBase + { + public TransactionNameTests(ITestOutputHelper xUnitOutputHelper) + : base(xUnitOutputHelper) { } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Controller_Action_When_Mvc_Controller_Action() + { + var pathData = SampleAppUrlPaths.HomePage; + await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + var transaction = receivedData.Transactions.Single(); + transaction.Name.Should().Be("GET Home/Index"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Area_Controller_Action_When_Mvc_Area_Controller_Action() + { + var pathData = SampleAppUrlPaths.MyAreaHomePage; + await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + var transaction = receivedData.Transactions.Single(); + transaction.Name.Should().Be("GET MyArea/Home/Index"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Controller_Action_When_Mvc_Controller_Action_Returns_404_ActionResult() + { + var pathData = SampleAppUrlPaths.NotFoundPage; + await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + var transaction = receivedData.Transactions.Single(); + transaction.Name.Should().Be("GET Home/NotFound"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Unknown_Route_When_Mvc_Controller_Action_Does_Not_Exist() + { + var pathData = SampleAppUrlPaths.PageThatDoesNotExist; + await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + var transaction = receivedData.Transactions.Single(); + transaction.Name.Should().Be("GET unknown route"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Controller_Action_When_Mvc_Controller_Action_Throws_HttpException_404() + { + var pathData = SampleAppUrlPaths.ThrowsHttpException404PageRelativePath; + await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + var transaction = receivedData.Transactions.Single(); + transaction.Name.Should().Be("GET Home/ThrowsHttpException404"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Path_When_Mvc_Controller_Action_Throws_InvalidOperationException() + { + var pathData = SampleAppUrlPaths.ThrowsInvalidOperationPage; + await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + var transaction = receivedData.Transactions.Single(); + transaction.Name.Should().Be("GET Home/ThrowsInvalidOperation"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Controller_When_WebApi_Controller_Action() + { + var pathData = SampleAppUrlPaths.WebApiPage; + await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + var transaction = receivedData.Transactions.Single(); + transaction.Name.Should().Be("GET WebApi"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Path_When_Webforms_Page() + { + var pathData = SampleAppUrlPaths.WebformsPage; + await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + var transaction = receivedData.Transactions.Single(); + transaction.Name.Should().Be($"GET {pathData.Uri.AbsolutePath}"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Path_When_Routed_Webforms_Page() + { + var pathData = SampleAppUrlPaths.RoutedWebformsPage; + await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + var transaction = receivedData.Transactions.Single(); + transaction.Name.Should().Be($"GET {pathData.Uri.AbsolutePath}"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Path_When_Asmx_Soap11_Request() + { + var pathData = SampleAppUrlPaths.CallSoapServiceProtocolV11; + var action = "Ping"; + + var request = new HttpRequestMessage(HttpMethod.Post, pathData.Uri) + { + Content = new StringContent($@" + + + <{action} xmlns=""http://tempuri.org/"" /> + + ", Encoding.UTF8, "text/xml") + }; + + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml")); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml"); + request.Headers.Add("SOAPAction", $"http://tempuri.org/{action}"); + + var response = await HttpClient.SendAsync(request); + response.IsSuccessStatusCode.Should().BeTrue(); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + receivedData.Transactions.First().Name.Should().Be($"POST {pathData.Uri.AbsolutePath} {action}"); + }); + } + + [AspNetFullFrameworkFact] + public async Task Name_Should_Be_Path_When_Asmx_Soap12_Request() + { + var pathData = SampleAppUrlPaths.CallSoapServiceProtocolV12; + var action = "Ping"; + + var request = new HttpRequestMessage(HttpMethod.Post, pathData.Uri) + { + Content = new StringContent($@" + + + <{action} xmlns=""http://tempuri.org/"" /> + + ", Encoding.UTF8, "application/soap+xml") + }; + + request.Headers.Accept.Clear(); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); + + var response = await HttpClient.SendAsync(request); + response.IsSuccessStatusCode.Should().BeTrue(); + + await WaitAndCustomVerifyReceivedData(receivedData => + { + receivedData.Transactions.Count.Should().Be(1); + receivedData.Transactions.First().Name.Should().Be($"POST {pathData.Uri.AbsolutePath} {action}"); + }); + } + } +}