Skip to content

Commit

Permalink
Add HttpContextCurrentExecutionSegmentsContainer (#992)
Browse files Browse the repository at this point in the history
* Add HttpContextCurrentExecutionSegmentsContainer

This commit adds an implementation of ICurrentExecutionSegmentsContainer
that gets/sets the current transaction and span in HttpContext.Items,
in addition to AsyncLocal<T>, making them available within the
ExecutionContext of IIS.

AsyncLocal<T> are not propagated across the ASP.NET ExecutionContext
running in IIS, resulting in the current transaction set in Application_BeginRequest
returning as null from Agent.Tracer.CurrentTransaction in later subsequent
IIS events, when such events execute asynchronously. ASP.NET does
however propagate the current HttpContext through HttpContext.Current,
allowing values to be stored in HttpContext.Items hashtable and shared
across IIS events. The HttpContextCurrentExecutionSegmentsContainer
uses both AsyncLocal<T> and HttpContext.Items to get/set the current
transaction and span, allowing them to be available in both IIS
events and async contexts. AsyncLocal<T> is still needed for places
where HttpContext.Current is null, for example, DiagnosticSourceListeners
running on a separate thread.

It seems it may be possible for the current transaction and current
span instances stored in AsyncLocal<T> and HttpContext.Items to be different
in such scenarios where one is available but not the other on beginning
the transaction/span, and where availability is reversed on end. Unsure
how likely this is in practice however, and using both AsyncLocal<T> and
HttpContext.Items is the common approach in resolving this issue in < .NET 4.7.1.

For .NET 4.7.1, a new OnExecuteRequestStep method has been added to
HttpApplication, to allow the execution context to be restored/saved:

https://devblogs.microsoft.com/dotnet/net-framework-4-7-1-asp-net-and-configuration-features/#asp-net-execution-step-feature

Closes #934
Closes #972

* tidy after rebase

* Add concurrent requests test

This commit adds an integration test for
HttpContextCurrentExecutionSegmentsContainer
that makes multiple concurrent requests
and asserts that the captured transactions
and span ids align with expectations.
  • Loading branch information
russcam committed Oct 29, 2020
1 parent 85a2871 commit 30038c4
Show file tree
Hide file tree
Showing 17 changed files with 661 additions and 51 deletions.
Expand Up @@ -125,6 +125,13 @@
<SubType>ASPXCodeBehind</SubType>
<DependentUpon>Webforms.aspx</DependentUpon>
</Compile>
<Compile Include="App_Start\WebApiConfig.cs" />
<Compile Include="Controllers\WebApiController.cs" />
<Compile Include="Controllers\DatabaseController.cs" />
<Compile Include="Models\CreateSampleDataViewModel.cs" />
<Compile Include="Mvc\JsonBadRequestResult.cs" />
<Compile Include="Mvc\JsonNetValueProviderFactory.cs" />
<Compile Include="Mvc\StreamResult.cs" />
</ItemGroup>
<ItemGroup Condition="'$(OS)' == 'WINDOWS_NT'">
<Content Include="Content\bootstrap-grid.css" />
Expand Down Expand Up @@ -202,6 +209,9 @@
<Content Include="Views\Account\ResetPassword.cshtml" />
<Content Include="Views\Account\ResetPasswordConfirmation.cshtml" />
<Content Include="Views\Shared\_LoginPartial.cshtml" />
<Content Include="Views\Shared\_LoginPartial.cshtml" />
<Content Include="Views\Database\Create.cshtml" />
<Content Include="Views\Database\Index.cshtml" />
<Content Include="Areas\MyArea\Views\Home\Index.cshtml" />
<Content Include="Areas\MyArea\Web.config" />
<Content Include="Areas\MyArea\_ViewStart.cshtml" />
Expand Down Expand Up @@ -246,18 +256,30 @@
<PackageReference Include="EntityFramework">
<Version>6.3.0</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.Identity.EntityFramework">
<Version>2.2.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.Identity.Owin">
<Version>2.2.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.Mvc">
<Version>5.2.4</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.TelemetryCorrelation">
<Version>1.0.7</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.WebApi">
<Version>5.2.4</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.Web.Optimization">
<Version>1.1.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform">
<Version>2.0.1</Version>
</PackageReference>
<PackageReference Include="Microsoft.Owin.Host.SystemWeb">
<Version>4.1.1</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>11.0.2</Version>
</PackageReference>
Expand All @@ -273,24 +295,6 @@
<PackageReference Include="System.Memory">
<Version>4.5.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.Identity.EntityFramework">
<Version>2.2.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.AspNet.Identity.Owin">
<Version>2.2.3</Version>
</PackageReference>
<PackageReference Include="Microsoft.Owin.Host.SystemWeb">
<Version>4.1.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Include="App_Start\WebApiConfig.cs" />
<Compile Include="Controllers\WebApiController.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNet.WebApi">
<Version>5.2.4</Version>
</PackageReference>
</ItemGroup>
<PropertyGroup Condition="'$(OS)' == 'WINDOWS_NT'">
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
Expand Down
7 changes: 7 additions & 0 deletions sample/AspNetFullFrameworkSampleApp/Bootstrap/Alert.cs
Expand Up @@ -18,6 +18,13 @@ public class SuccessAlert : Alert
public SuccessAlert(string title, string message) : base(title, message) => Status = AlertStatus.Success;
}

public class DangerAlert : Alert
{
public DangerAlert(string message) : base(message) => Status = AlertStatus.Danger;

public DangerAlert(string title, string message) : base(title, message) => Status = AlertStatus.Danger;
}

public class Alert : IHtmlString
{
public Alert(string message) => Message = message;
Expand Down
Expand Up @@ -3,14 +3,20 @@
// 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.IO;
using System.Web.Mvc;
using AspNetFullFrameworkSampleApp.Bootstrap;
using AspNetFullFrameworkSampleApp.Extensions;
using AspNetFullFrameworkSampleApp.Mvc;

namespace AspNetFullFrameworkSampleApp.Controllers
{
public abstract class ControllerBase : Controller
{
protected void AddAlert(Alert alert) => TempData.Put("alert", alert);

protected ActionResult JsonBadRequest(object content) => new JsonBadRequestResult { Data = content, };

protected ActionResult Stream(Stream stream, string contentType, int statusCode) => new StreamResult(stream, contentType, statusCode);
}
}
123 changes: 123 additions & 0 deletions sample/AspNetFullFrameworkSampleApp/Controllers/DatabaseController.cs
@@ -0,0 +1,123 @@
// 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.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using AspNetFullFrameworkSampleApp.Bootstrap;
using AspNetFullFrameworkSampleApp.Data;
using AspNetFullFrameworkSampleApp.Models;
using Newtonsoft.Json;

namespace AspNetFullFrameworkSampleApp.Controllers
{
/// <summary>
/// Demonstrates database functionality
/// </summary>
public class DatabaseController : ControllerBase
{
/// <summary>
/// List the sample data in the database
/// </summary>
/// <returns></returns>
public ActionResult Index()
{
using var context = new SampleDataDbContext();
var samples = context.Set<SampleData>().ToList();
return View(samples);
}

/// <summary>
/// Allows sample data to be inserted into the database
/// </summary>
public ActionResult Create() => View(new CreateSampleDataViewModel());

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(CreateSampleDataViewModel model)
{
if (!ModelState.IsValid)
return View(model);

int changes;
using (var context = new SampleDataDbContext())
{
var sampleData = new SampleData { Name = model.Name };
context.Set<SampleData>().Add(sampleData);
changes = await context.SaveChangesAsync();
}

AddAlert(new SuccessAlert("Sample added", $"{changes} sample was saved to the database"));
return RedirectToAction("Index");
}

// TODO: make this a web api controller action
/// <summary>
/// Generates the given count of sample data, and calls the bulk action to insert into the database
/// </summary>
/// <remarks>
/// This intentionally makes an async HTTP call to bulk insert.
/// </remarks>
/// <param name="count">The count of sample data to generate</param>
[HttpPost]
public async Task<ActionResult> Generate(int count)
{
if (!ModelState.IsValid)
return JsonBadRequest(new { success = false, message = "Invalid samples" });

if (count <= 0)
return JsonBadRequest(new { success = false, message = "count must be greater than 0" });

int existingCount;
using (var context = new SampleDataDbContext())
existingCount = await context.Set<SampleData>().CountAsync();

var samples = Enumerable.Range(existingCount, count)
.Select(i => new CreateSampleDataViewModel { Name = $"Generated sample {i}" });

var client = new HttpClient();
var bulkUrl = Url.Action("Bulk", "Database", null, Request.Url.Scheme);
var json = JsonConvert.SerializeObject(samples);
var contentType = "application/json";
var content = new StringContent(json, Encoding.UTF8, contentType);

var response = await client.PostAsync(bulkUrl, content).ConfigureAwait(false);
var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

return Stream(responseStream, contentType, (int)response.StatusCode);
}

// TODO: make this a web api controller action
/// <summary>
/// Bulk inserts sample data into the database
/// </summary>
/// <param name="model">The sample data to insert</param>
[HttpPost]
public async Task<ActionResult> Bulk(IEnumerable<CreateSampleDataViewModel> model)
{
if (!ModelState.IsValid)
return JsonBadRequest(new { success = false, message = "Invalid samples" });

var sampleData = model.Select(m => new SampleData { Name = m.Name });
int changes;

using (var context = new SampleDataDbContext())
using (var transaction = context.Database.BeginTransaction())
{
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ValidateOnSaveEnabled = false;
context.Set<SampleData>().AddRange(sampleData);
changes = await context.SaveChangesAsync();
transaction.Commit();
}

return Json(new { success = true, changes });
}
}
}
21 changes: 10 additions & 11 deletions sample/AspNetFullFrameworkSampleApp/Controllers/HomeController.cs
Expand Up @@ -92,13 +92,20 @@ public async Task<ActionResult> ChildHttpSpanWithResponseForbidden()
public Task<ActionResult> Contact()
{
var httpClient = new HttpClient();
var callToThisAppUrl = new Uri(Request.Url.ToString().Replace(ContactPageRelativePath, AboutPageRelativePath));
var callToExternalServiceUrl = ChildHttpCallToExternalServiceUrl;

return SafeCaptureSpan<ActionResult>($"{ContactSpanPrefix}{SpanNameSuffix}", $"{ContactSpanPrefix}{SpanTypeSuffix}", async () =>
{
var callToThisAppUrl =
new Uri(HttpContext.ApplicationInstance.Request.Url.ToString().Replace(ContactPageRelativePath, AboutPageRelativePath));
async Task<HttpResponseMessage> GetContentFromUrl(Uri url)
{
Console.WriteLine($"Getting `{url}'...");
var response = await httpClient.GetAsync(url).ConfigureAwait(false);
Console.WriteLine($"Response status code from `{url}' - {response.StatusCode}");
return response;
}
var responseFromLocalHost = await GetContentFromUrl(callToThisAppUrl);
var callToExternalServiceUrl = ChildHttpCallToExternalServiceUrl;
var responseFromElasticCo = await GetContentFromUrl(callToExternalServiceUrl);
ViewBag.Message =
Expand All @@ -108,14 +115,6 @@ public Task<ActionResult> Contact()
return View();
}, $"{ContactSpanPrefix}{SpanSubtypeSuffix}", $"{ContactSpanPrefix}{SpanActionSuffix}", GetCaptureControllerActionAsSpan());

async Task<HttpResponseMessage> GetContentFromUrl(Uri urlToGet)
{
Console.WriteLine($"Getting `{urlToGet}'...");
var response = await httpClient.GetAsync(urlToGet);
Console.WriteLine($"Response status code from `{urlToGet}' - {response.StatusCode}");
return response;
}
}

public ActionResult Sample(int id) => Content(id.ToString());
Expand Down
5 changes: 5 additions & 0 deletions sample/AspNetFullFrameworkSampleApp/Global.asax.cs
Expand Up @@ -5,12 +5,14 @@
using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Http.Batch;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using AspNetFullFrameworkSampleApp.Mvc;
using Elastic.Apm;
using NLog;

Expand Down Expand Up @@ -40,6 +42,9 @@ protected void Application_Start()
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);

ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new JsonNetValueProviderFactory());
}

protected void Application_BeginRequest(object sender, EventArgs e)
Expand Down
@@ -0,0 +1,15 @@
// 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.ComponentModel.DataAnnotations;

namespace AspNetFullFrameworkSampleApp.Models
{
public class CreateSampleDataViewModel
{
[Required]
public string Name { get; set; }
}
}
13 changes: 13 additions & 0 deletions sample/AspNetFullFrameworkSampleApp/Mvc/JsonBadRequestResult.cs
@@ -0,0 +1,13 @@
using System.Web.Mvc;

namespace AspNetFullFrameworkSampleApp.Mvc
{
public class JsonBadRequestResult : JsonResult
{
public override void ExecuteResult(ControllerContext context)
{
context.RequestContext.HttpContext.Response.StatusCode = 400;
base.ExecuteResult(context);
}
}
}

0 comments on commit 30038c4

Please sign in to comment.