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)
{
...
Inspired by @Mike's answer.
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 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
}
}
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;
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With