Capturing ASP.NET Core original URL with Azure Application Insights
Niels Swimberghe - - Azure
Follow me on Twitter, buy me a coffee
Azure Application Insights is an Application Performance Management (APM) tool providing insights into the state of your application. By default, Application Insights will capture a lot of data about your ASP.NET Core applications including HTTP Requests made to your website. Unfortunately, the URL captured by Application Insights doesn't always match the URL originally requested by the client.
In many ASP.NET applications, the path of the HTTP request is internally rewritten by the time Application Insights records the data. This results in the rewritten URL to be captured and not the original URL. You can simulate this scenario by using the URL Rewriting Middleware provided by using the Microsoft.AspNetCore.Rewrite
package which is already included implicitly in ASP.NET Core apps.
In the Startup
class, update the Configure
method by adding the following RewriteOptions
and passing it to app.UseRewriter
to configure the rewrite middleware.
using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Rewrite; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace AddRawUrlToApplicationInsights_Core { public class Startup { ... public void Configure(IApplicationBuilder app) { // this is for demonstration purposes only // it's recommended to use a redirect (not rewrite) in these scenario's for SEO purposes to avoid duplicate URL's var options = new RewriteOptions() .AddRewrite(@"^privacy$", "home/privacy", false); app.UseRewriter(options); ... } } }
In this sample, when /privacy
is requested, the path is rewritten to /home/privacy
which will resolve to the HomeController.Privacy
action. When you browse to /privacy
and look at the recorded HTTP request inside of Application Insights, the URL will be https://domain.tld/home/privacy
instead of https://domain.tld/privacy
. The rewritten URL is useful information, but you may also need the original URL for proper debugging/analyzing.
Luckily, you can access the original path before it was rewritten by calling httpContext.Features.Get<IHttpRequestFeature>()
and then grabbing the RawTarget
property.
"IHttpRequestFeature.RawTarget Property: The request target as it was sent in the HTTP request. This property contains the raw path and full query, as well as other request targets such as * for OPTIONS requests"
Source: Microsoft Documentation
Application Insights provides an extensibility point for you to capture additional data using the ITelemetryInitializer
interface. You can capture the original URL by implementing ITelemetryInitializer
and adding the RawTarget
data to the telemetry properties:
using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using System; namespace AddRawUrlToApplicationInsights_Core { public class OriginalUrlTelemetryInitializer : ITelemetryInitializer { readonly IHttpContextAccessor httpContextAccessor; public OriginalUrlTelemetryInitializer(IHttpContextAccessor httpContextAccessor) { this.httpContextAccessor = httpContextAccessor; } public void Initialize(ITelemetry telemetry) { if (telemetry is RequestTelemetry requestTelemetry) { var httpContext = httpContextAccessor.HttpContext; if (httpContext?.Request == null) { return; } var request = httpContext.Request; var httpRequestFeature = httpContext.Features.Get<IHttpRequestFeature>(); var rawTarget = httpRequestFeature.RawTarget; // RawTarget returns original path and querystring requestTelemetry.Properties["OriginalUrl"] = rawTarget; // to extract the path and query from the RawTarget, you need to use the Uri class // the Uri class requires at least the scheme://host to parse; // adding the dummy base uri "http://0.0.0.0" will ensure it parses properly var rawUri = new Uri($"http://0.0.0.0{rawTarget}"); var fdnUrl = new UriBuilder( scheme: request.Scheme, host: request.Host.Host, port: request.Host.Port ?? -1, // use -1 in case there's no port specified, the port will be left out when converting `ToString` path: rawUri.AbsolutePath, extraValue: rawUri.Query ).ToString(); requestTelemetry.Properties["OriginalUrlFqdn"] = fdnUrl; } } } }
Additionally, you can grab the HttpRequest
by grabbing the HttpContext
via the IHttpContextAccessor
. The IHttpContextAccessor
is automatically injected by ASP.NET's dependency injection.
Using the Uri class, you can extract the path and querystring out of the RawTarget
and combine it with the HttpRequest
data to create the original URL including the fully qualified domain name.
Next, you need to add the OriginalUrlTelemetryInitializer
to ASP.NET Core's dependency injection system. Update the Startup
class as below:
using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Rewrite; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace AddRawUrlToApplicationInsights_Core { public class Startup { ... public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddApplicationInsightsTelemetry(); // register OriginalUrlTelemetryInitializer to make it available to Application Insights services.AddSingleton<ITelemetryInitializer, OriginalUrlTelemetryInitializer>(); } ... } }
This is what the resulting data looks like when sent to Application Insights:
{ "name": "AppRequests", "time": "2020-09-12T04:12:47.768786Z", "tags": { "ai.application.ver": "1.0.0.0", "ai.cloud.roleInstance": "yourhostname", "ai.operation.id": "47519341849a5d448f363d488ef3a71e", "ai.operation.name": "GET Home/Privacy", "ai.location.ip": "::1", "ai.internal.sdkVersion": "aspnet5c:2.14.0", "ai.internal.nodeName": "yourhostname" }, "data": { "baseType": "RequestData", "baseData": { "ver": 2, "id": "925c17c66525b141", "name": "GET Home/Privacy", "duration": "00:00:00.0179295", "success": true, "responseCode": "200", "url": "https://localhost:5001/home/privacy", "properties": { "OriginalUrl": "/privacy", "OriginalUrlFqdn": "https://localhost:5001/privacy", "DeveloperMode": "true", "AspNetCoreEnvironment": "Development", "_MS.ProcessedByMetricExtractors": "(Name:'Requests', Ver:'1.1')" } } } }
Notice how the url
, properties.OriginalUrl
, and properties.OriginalUrlFqdn
are all captured, providing a more complete picture of the request:
url | https://localhost:5001/home/privacy |
properties.OriginalUrl | /privacy |
properties.OriginalUrlFqdn | https://localhost:5001/privacy |
Summary #
Using the ITelemetryInitializer
extensibility point in Application Insights, you can capture additional data. Using the IHttpRequestFeature.RawTarget
property, you can capture the original URL in addition to the rewritten URL which is captured by default.