Iain
Iain

Reputation: 1960

Stop Application Insights including path parameters in the Operation Name

Our ASP.NET MVC application includes some URI path parameters, like:

https://example.com/api/query/14hes1017ceimgS2ESsIec

In Application Insights, this URI above becomes Operation Name

GET /api/query/14hes1017ceimgS2ESsIec

We don't want millions of unique Operations like this; it's just one code method serving them all (see below). We want to roll them up under an Operation Name like

GET /api/query/{path}

Here is the code method - I think App Insights could detect that the URI contains a query parameter... but it doesn't.

    [Route("api/query/{hash}")]
    public HttpResponseMessage Get(string hash)
    {
        ...

Upvotes: 13

Views: 5923

Answers (6)

DunnoMan
DunnoMan

Reputation: 103

Here is a solution for ASP.NET using Minimal API's. This will use the routePattern from the Endpoint as the name and operation_Name.

using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;

namespace Web.ApplicationInsights;

public class UseRoutePatternAsNameTelemetryInitializer : ITelemetryInitializer
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UseRoutePatternAsNameTelemetryInitializer(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    public void Initialize(ITelemetry telemetry)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        if (telemetry is RequestTelemetry requestTelemetry && httpContext != null)
        {
            var endpoint = httpContext.GetEndpoint();
            if (endpoint is RouteEndpoint routeEndpoint)
            {
                var telemetryName = CreateTelemetryName(routeEndpoint, httpContext);
                requestTelemetry.Name = telemetryName;
                requestTelemetry.Context.Operation.Name = telemetryName;
            }
        }
    }
    
    private static string CreateTelemetryName(RouteEndpoint routeEndpoint, HttpContext httpContext)
    {
        var routePattern = routeEndpoint.RoutePattern.RawText ?? "";
        var routeName = routePattern.StartsWith("/") ? routePattern : $"/{routePattern}";
        var telemetryName = $"{httpContext.Request.Method} {routeName}";
        return telemetryName;
    }
}

And the following in Program.cs


AddApplicationInsightsTelemetry(builder);

static void AddApplicationInsightsTelemetry(WebApplicationBuilder webApplicationBuilder)
{
    webApplicationBuilder.Services.AddApplicationInsightsTelemetry();
    webApplicationBuilder.Services.AddHttpContextAccessor();
    webApplicationBuilder.Services.AddSingleton<ITelemetryInitializer, UseRoutePatternAsNameTelemetryInitializer>();
}

Upvotes: 4

angularsen
angularsen

Reputation: 8668

Inspired by @Mike's answer.

  • Updated for ASP.NET Core 5/6
  • Uses route name, if specified.
  • Uses template and API version, if route data is available.

Telemetry name before/after:
GET /chat/ba1ce6bb-01e8-4633-918b-08d9a363a631/since/2021-11-18T18:51:08
GET /chat/{id}/since/{timestamp}

https://gist.github.com/angularsen/551bcbc5f770d85ff9c4dfbab4465546

The solution consists of:

  • Global MVC action filter, to compute telemetry name from route data.
  • ITelemetryInitializer to update the telemetry name.
  • Configure filter and initializer in ASP.NET's Startup class

Global filter to compute the telemetry name from the API action route data.

#nullable enable
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Digma.Api.Common.Telemetry
{
    /// <summary>
    /// Action filter to construct a simpler telemetry name from the route name or the route template.
    /// <br/><br/>
    /// Out of the box, Application Insights sometimes uses request telemetry names like "GET /chat/ba1ce6bb-01e8-4633-918b-08d9a363a631/since/2021-11-18T18:51:08".
    /// This makes it hard to see how many requests were for a particular API action.
    /// This is a <a href="https://github.com/microsoft/ApplicationInsights-dotnet/issues/1418">known issue</a>.
    /// <br/><br/>
    /// - If route name is defined, then use that.<br/>
    /// - If route template is defined, then the name is formatted as "{method} /{template} v{version}".
    /// </summary>
    /// <example>
    /// - <b>"MyCustomName"</b> if route name is explicitly defined with <c>[Route("my_path", Name="MyCustomName")]</c><br/>
    /// - <b>"GET /config v2.0"</b> if template is "config" and API version is 2.0.<br/>
    /// - <b>"GET /config"</b> if no API version is defined.
    /// </example>
    /// <remarks>
    /// The value is passed on via <see cref="HttpContext.Items"/> with the key <see cref="SimpleRequestTelemetryNameInitializer.TelemetryNameKey"/>.
    /// </remarks>
    public class SimpleRequestTelemetryNameActionFilter : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var httpContext = context.HttpContext;
            var attributeRouteInfo = context.ActionDescriptor.AttributeRouteInfo;

            if (attributeRouteInfo?.Name is { } name)
            {
                // If route name is defined, it takes precedence.
                httpContext.Items.Add(SimpleRequestTelemetryNameInitializer.TelemetryNameKey, name);
            }
            else if (attributeRouteInfo?.Template is { } template)
            {
                // Otherwise, use the route template if defined.
                string method = httpContext.Request.Method;
                string versionSuffix = GetVersionSuffix(httpContext);

                httpContext.Items.Add(SimpleRequestTelemetryNameInitializer.TelemetryNameKey, $"{method} /{template}{versionSuffix}");
            }

            base.OnActionExecuting(context);
        }

        private static string GetVersionSuffix(HttpContext httpContext)
        {
            try
            {
                var requestedApiVersion = httpContext.GetRequestedApiVersion()?.ToString();

                // Add leading whitespace so we can simply append version string to telemetry name.
                if (!string.IsNullOrWhiteSpace(requestedApiVersion))
                    return $" v{requestedApiVersion}";
            }
            catch (Exception)
            {
                // Some requests lack the IApiVersioningFeature, like requests to get swagger doc
            }

            return string.Empty;
        }
    }
}

Telemetry initializer that updates the name of RequestTelemetry.

using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Http;

namespace Digma.Api.Common.Telemetry
{
    /// <summary>
    /// Changes the name of request telemetry to the value assigned by <see cref="SimpleRequestTelemetryNameActionFilter"/>.
    /// </summary>
    /// <remarks>
    /// The value is passed on via <see cref="HttpContext.Items"/> with the key <see cref="TelemetryNameKey"/>.
    /// </remarks>
    public class SimpleRequestTelemetryNameInitializer : ITelemetryInitializer
    {
        internal const string TelemetryNameKey = "SimpleOperationNameInitializer:TelemetryName";
        private readonly IHttpContextAccessor _httpContextAccessor;

        public SimpleRequestTelemetryNameInitializer(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public void Initialize(ITelemetry telemetry)
        {
            var httpContext = _httpContextAccessor.HttpContext;
            if (telemetry is RequestTelemetry requestTelemetry && httpContext != null)
            {
                if (httpContext.Items.TryGetValue(TelemetryNameKey, out var telemetryNameObj)
                    && telemetryNameObj is string telemetryName
                    && !string.IsNullOrEmpty(telemetryName))
                {
                    requestTelemetry.Name = telemetryName;
                }
            }
        }
    }
}

ASP.NET startup class to configure the global filter and telemetry initializer.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register telemetry initializer.
        services.AddApplicationInsightsTelemetry();
        services.AddSingleton<ITelemetryInitializer, SimpleRequestTelemetryNameInitializer>();

        services.AddMvc(opt =>
        {
            // Global MVC filters.
            opt.Filters.Add<SimpleRequestTelemetryNameActionFilter>();
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...other configuration
    }
}

Upvotes: 2

Mike
Mike

Reputation: 834

When I ran into this I felt like it would be more useful to have the actual text of the route as the operation name, rather than try to identify all the different ways an ID could be constructed.

The problem is that route template exists down the tree from HttpRequestMessage, but in a TelemetryInitializer you end up with only access to HttpContext.Current.Request which is an HttpRequest.

They don't make it easy but this code works:


    // This class runs at the start of each request and gets the name of the
    // route template from actionContext.ControllerContext?.RouteData?.Route?.RouteTemplate
    // then stores it in HttpContext.Current.Items
    public class AiRewriteUrlsFilter : System.Web.Http.Filters.ActionFilterAttribute
    {
        internal const string AiTelemetryName = "AiTelemetryName";
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            string method = actionContext.Request?.Method?.Method;
            string routeData = actionContext.ControllerContext?.RouteData?.Route?.RouteTemplate;
            if (!string.IsNullOrEmpty(routeData) && routeData.StartsWith("api/1.0/") && HttpContext.Current != null)
            {
                HttpContext.Current.Items.Add(AiTelemetryName, $"{method} /{routeData}");
            }
            base.OnActionExecuting(actionContext);
        }
    }

    // This class runs when the telemetry is initialized and pulls
    // the value we set in HttpContext.Current.Items and uses it
    // as the new name of the telemetry.
    public class AiRewriteUrlsInitializer : ITelemetryInitializer
    {
        public void Initialize(ITelemetry telemetry)
        {
            if (telemetry is RequestTelemetry rTelemetry && HttpContext.Current != null)
            {
                string telemetryName = HttpContext.Current.Items[AiRewriteUrlsFilter.AiTelemetryName] as string;
                if (!string.IsNullOrEmpty(telemetryName))
                {
                    rTelemetry.Name = telemetryName;
                }
            }
        }
    }

Upvotes: 1

Iain
Iain

Reputation: 1960

MS are working on this feature with https://github.com/Microsoft/ApplicationInsights-dotnet-server/issues/176

Upvotes: 3

Iain
Iain

Reputation: 1960

I hacked it with this hardcoded OperationNameMunger (using these docs for inspiration).

I wired it into the ApplicationInsights.config, straight after the OperationNameTelemetryInitializer.


using System.Text.RegularExpressions;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;

namespace My.Namespace
{
    public class OperationNameMunger : ITelemetryInitializer
    {
        public void Initialize(ITelemetry telemetry)
        {
            var existingOpName = telemetry.Context?.Operation?.Name;
            if (existingOpName == null)
                return;

            const string matchesInterestingOps = "^([A-Z]+ /api/query/)[^ ]+$";
            var match = Regex.Match(existingOpName, matchesInterestingOps);
            if (match.Success)
            {
                telemetry.Context.Operation.Name = match.Groups[1].Value + "{hash}";
            }
        }
    }
}

Upvotes: 2

EranG
EranG

Reputation: 862

The reason Application Insights does not detect that the suffix of your Operation Name is a parameter is because the SDK does not look at your code, and for all practical purposes that's a valid URI.
Two options to get what you want:

  1. Change your API to pass the parameter in the query string (that is stripped out of the Operation Name)
  2. Implement your own ITelemetryProcessor (detailed explanation can be found here), and remove the suffix hash from the Operation Name yourself

Upvotes: 6

Related Questions