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