Reputation: 1960
Our ASP.NET MVC application includes some URI path parameters, like:
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
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
Reputation: 8668
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
}
}
Upvotes: 2
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
Reputation: 1960
MS are working on this feature with https://github.com/Microsoft/ApplicationInsights-dotnet-server/issues/176
Upvotes: 3
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
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:
ITelemetryProcessor
(detailed explanation can be found here), and remove the suffix hash from the Operation Name yourselfUpvotes: 6