I have some old applications on ASP.NET MVC 5 and 6, and I want to accomplish something like the automatic W3C trace ID propagation that ASP.NET Core does. (E.g., reading the incoming traceparent
request header setting the Activity.Current property accordingly. Storing the value in the Request context is not ideal, as it won't automatically be available to the other tasks, but it would be a great start.)
I would like to have some middleware that runs before every controller and sets the Activity. To create this middleware, do I need to use OWIN? Or is there something built into ASP.NET MVC that I can use to run code to read the request before it is handed over to the controller methods?
I tried setting up an OWIN middleware to try setting the Activity.Current property and the OwinContext. I don't know if I'm doing it right, but the Activity.Current
is always null, as is the Request.GetOwinContext()
call in the controller is always null as well, even though I can see the values before it passes from the OWIN middleware to the controller, and the thread ID is the same in the lambda function in Startup.cs and the ValuesController.Get method.
It seems like the OWIN middleware is exiting before the controller is even being called, so maybe that's why? I'm not sure if I wired up OWIN with ASP.NET correctly.
// Startup.cs
using Microsoft.Owin;
using Owin;
using System;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using System.Web.Http;
[assembly: OwinStartup(typeof(ActivityDemo.Startup))]
namespace ActivityDemo
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
app.Use(async (ctx, next) =>
{
ActivityContext activityCtx = default;
string traceparent = ctx.Request.Headers.Get("traceparent");
if (traceparent != null)
{
try
{
string tracestate = ctx.Request.Headers.Get("tracestate");
ActivityContext.TryParse(traceparent, tracestate, out activityCtx);
}
catch { }
}
// We depend on the activity being set (for logging with Serilog),
// so create one manually even if no one is listening to the
// ActivitySource.
Activity activity = new Activity("ReceiveHttpRequest").SetParentId(traceparent).Start();
// ctx.Request.Set("traceparent", traceparent);
ctx.Environment["traceparent"] = traceparent;
var current = Activity.Current; // inserted to inspect with debugger
await next();
});
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
app.UseWebApi(config);
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
// app.Run();
}
}
}
// ValuesController.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
namespace ActivityDemo.Controllers
{
public class ValuesController : ApiController
{
// GET api/values
public IEnumerable<string> Get()
{
var activity = Activity.Current;
var owinContext = Request.GetOwinContext(); // inserted to inspect with debugger, always `null`
var owinEnv = Request.GetOwinEnvironment(); // inserted to inspect with debugger, always `null`
return new string[] { "value1", "value2", Activity.Current?.TraceId.ToString() };
}
}
}
This is just the first thing that I tried; I'm open to non-OWIN solutions as well.
I think I got it!
System.Diagnostics.DiagnosticSource
and System.Reactive.Core
packages to your ASP.NET project.App_Start\DiagnosticsConfig.cs
with the following content:// DiagnosticsConfig.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Web;
namespace DiagnosticsTest
{
public class DiagnosticsConfig
{
// Set up an ActivitySource
private static readonly AssemblyName AssemblyName =
typeof(DiagnosticsConfig).Assembly.GetName();
internal static readonly ActivitySource ActivitySource =
new ActivitySource(AssemblyName.Name, AssemblyName.Version.ToString());
public static void StartDiagnosticsListeners()
{
// Set the format to W3C
Activity.DefaultIdFormat = ActivityIdFormat.W3C;
Activity.ForceDefaultIdFormat = true;
// Start the ActivityListener
ActivitySource.AddActivityListener(new ActivityListener()
{
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
ShouldListenTo = _ => true,
}) ;
// Using the Subscribe(IObservable<T>, Action<T>) extension method from
// the System.Reactive.Core package.
var subscription = DiagnosticListener.AllListeners.Subscribe(delegate (DiagnosticListener listener)
{
if (listener.Name == "System.Net.Http.Desktop")
{
listener.Subscribe(delegate (KeyValuePair<string, object> diagnostic)
{
if (
diagnostic.Key == "System.Net.Http.Desktop.HttpRequestOut.Start"
&& Activity.Current != null
)
{
// Set the parent to workaround HttpDiagnosticListener adding
// an extra intermediate span.
HttpWebRequest request = diagnostic.Value.GetType()
.GetRuntimeProperty("Request")
.GetValue(diagnostic.Value) as HttpWebRequest;
request.Headers.Add("traceparent", Activity.Current.ParentId);
var tracestate = Activity.Current.TraceStateString;
if (!string.IsNullOrWhiteSpace(tracestate))
{
request.Headers.Add("tracestate", tracestate);
}
}
});
}
// You can listen to other DiagnosticSources by adding more
// handlers here.
});
}
}
}
traceparent
header Application_BeginRequest()
and Application_EndRequest()
methods of Global.Asax.cs
. Also, register the listener.// Global.asax.cs
using System.Diagnostics;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
namespace DiagnosticsTest
{
public class WebApiApplication : HttpApplication
{
protected void Application_Start()
{
// <Other startup boilerplate>
// ...
// Register your DiagnosticSource
DiagnosticsConfig.StartDiagnosticsListeners();
}
protected void Application_BeginRequest()
{
// Parse traceparent on incoming requests.
var ctx = HttpContext.Current;
const string operationName = "MyCompany.AspNet.HandleRequest";
var traceParent = ctx.Request.Headers["traceparent"];
var traceState = ctx.Request.Headers["tracestate"];
Activity activity = (traceParent != null && ActivityContext.TryParse(traceParent, traceState, out ActivityContext activityCtx))
? DiagnosticsConfig.ActivitySource.StartActivity(operationName, ActivityKind.Server, activityCtx)
: DiagnosticsConfig.ActivitySource.StartActivity(operationName, ActivityKind.Server);
ctx.Items["activity"] = activity;
}
protected void Application_EndRequest()
{
// Clean up the Activity at the end of the request.
var ctx = HttpContext.Current;
var activity = (Activity)ctx.Items["activity"];
activity.Stop();
}
}
}
After that, you should have Activity.Current
set to the incoming
traceparent
header for your Controller code/logs, and outgoing requests from
System.Net.Http
will automatically begin a new span.