Swimburger

Capturing ASP.NET Core original URL with Azure Application Insights

Niels Swimberghe

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);

            ...
        }
    }
}

Source at GitHub

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;
            }
        }
    }
}

Source at GitHub

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>();
        }

        ...
    }
}

Full source at GitHub

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.

Related Posts

Related Posts