Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,12 @@ public void OnStopActivity(Activity activity, object? payload)
var response = context.Response;

#if !NETSTANDARD
var routePattern = (context.Features.Get<IExceptionHandlerPathFeature>()?.Endpoint as RouteEndpoint ??
context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText;
if (!string.IsNullOrEmpty(routePattern))
var endpoint = context.Features.Get<IExceptionHandlerPathFeature>()?.Endpoint as RouteEndpoint
?? context.GetEndpoint() as RouteEndpoint;

if (endpoint != null)
{
TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, context.Request.Method, routePattern);
activity.SetTag(SemanticConventions.AttributeHttpRoute, routePattern);
activity.SetRouteAttributeTag(endpoint, context.Request);
}
#endif

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,12 @@ public static void OnStopEventWritten(string name, object? payload)

#if NET
// Check the exception handler feature first in case the endpoint was overwritten
var route = (context.Features.Get<IExceptionHandlerPathFeature>()?.Endpoint as RouteEndpoint ??
context.GetEndpoint() as RouteEndpoint)?.RoutePattern.RawText;
if (!string.IsNullOrEmpty(route))
var endpoint = context.Features.Get<IExceptionHandlerPathFeature>()?.Endpoint as RouteEndpoint
?? context.GetEndpoint() as RouteEndpoint;

if (endpoint != null)
{
tags.Add(new KeyValuePair<string, object?>(SemanticConventions.AttributeHttpRoute, route));
tags.AddRouteAttribute(endpoint, context.Request);
}
#endif
if (context.Items.TryGetValue(ErrorTypeHttpContextItemsKey, out var errorType))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#if !NETSTANDARD

using System.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using OpenTelemetry.Trace;

namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation;

internal static class RouteAttributeHelper
{
public static void AddRouteAttribute(this TagList tags, RouteEndpoint endpoint, HttpRequest request)
{
var routePattern = GetRoutePattern(endpoint.RoutePattern, request.RouteValues);

if (!string.IsNullOrEmpty(routePattern))
{
tags.Add(new KeyValuePair<string, object?>(SemanticConventions.AttributeHttpRoute, routePattern));
}
}

public static void SetRouteAttributeTag(this Activity activity, RouteEndpoint endpoint, HttpRequest request)
{
var routePattern = GetRoutePattern(endpoint.RoutePattern, request.RouteValues);

if (!string.IsNullOrEmpty(routePattern))
{
TelemetryHelper.RequestDataHelper.SetActivityDisplayName(activity, request.Method, routePattern);
activity.SetTag(SemanticConventions.AttributeHttpRoute, routePattern);
}
}

private static string GetRoutePattern(RoutePattern routePattern, RouteValueDictionary routeValues)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need a very large suite of unit tests to ensure every possible route is correctly converted into a string.

Copy link
Contributor

@JamesNK JamesNK Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but I think an empty string route should resolve to /. An empty string by itself is confusing. We're already doing that with http.route tag in metrics based on customer feedback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This always runs, right? Should it only run if the route needs to change? i.e. there are area/controller/action/page attributes and a value is present in the requests route values collection.

{
if (routePattern.PathSegments.Count == 0)
{
// RazorPage default route
if (routePattern.Defaults.TryGetValue("page", out var pageValue))
{
return pageValue?.ToString()?.Trim('/')
?? string.Empty;
}

return string.Empty;
}

var sb = new StringBuilder();

foreach (var segment in routePattern.PathSegments)
{
foreach (var part in segment.Parts)
{
if (part is RoutePatternLiteralPart literalPart)
{
sb.Append(literalPart.Content);
sb.Append('/');
}
else if (part is RoutePatternParameterPart parameterPart)
{
switch (parameterPart.Name)
{
case "area":
case "controller":
case "action":
routePattern.RequiredValues.TryGetValue(parameterPart.Name, out var parameterValue);
if (parameterValue != null)
{
sb.Append(parameterValue);
sb.Append('/');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not look for separator part for adding separator?

break;
}

goto default;
default:
if (!parameterPart.IsOptional ||
(parameterPart.IsOptional && routeValues.ContainsKey(parameterPart.Name)))
{
sb.Append('{');
sb.Append(parameterPart.Name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if there are default values? Or it is catch all? Or has constraints? Or has optional path extension.

There are also complex segments. Are those being handled correctly? See https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-9.0#complex-segments

Maybe these scenarios are already handled correctly, but unit tests to verify are important.

sb.Append('}');
sb.Append('/');
}

break;
}
}
}
}

// Remove the trailing '/'
return sb.ToString(0, sb.Length - 1);
}
}

#endif
Loading