Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)
    {
        ...
like image 229
Iain Avatar asked Jun 22 '17 06:06

Iain


2 Answers

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
    }
}
like image 81
angularsen Avatar answered Oct 05 '22 04:10

angularsen


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;
                }
            }
        }
    }
like image 36
Mike Avatar answered Oct 05 '22 04:10

Mike