Skip to content

Instantly share code, notes, and snippets.

@angularsen
Created November 20, 2021 19:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save angularsen/551bcbc5f770d85ff9c4dfbab4465546 to your computer and use it in GitHub Desktop.
Save angularsen/551bcbc5f770d85ff9c4dfbab4465546 to your computer and use it in GitHub Desktop.
Simple request telemetry names
#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;
}
}
}
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;
}
}
}
}
}
namespace Foo
{
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
}
}
}
@jigarce
Copy link

jigarce commented Jun 7, 2022

@angularsen Can you share proper example. I have tried implementing your code but it is not working.

what should I write in place of "SimpleOperationNameInitializer:TelemetryName" ? its Name or key ?

@angularsen
Copy link
Author

@jigarce That is a just a unique string to store the value in HttpContext.Items, you don't need to change it.

I can't see any missing pieces here although I had to adapt from our code base.

Make sure you register everything as described in Startup.cs example.

I think you just have to debug a bit:

  • Is SimpleRequestTelemetryNameActionFilter.OnActionExecuting() run as part of your API request? Does it compute the simplified name and store it in HttpContext.Items?
  • Is SimpleRequestTelemetryNameInitializer.Initialize() run after the request? What values does it get?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment