diff --git a/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md index 3a4aed516e..7a06bb3844 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Improved performance of replacing static tokens with actual values + in the route template. + ([#3241](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3241)) + ## 1.13.0-beta.1 Released 2025-Oct-15 diff --git a/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpRequestRouteHelper.cs b/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpRequestRouteHelper.cs index 3c4c48613b..db952a7914 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpRequestRouteHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpRequestRouteHelper.cs @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Runtime.CompilerServices; +using System.Text; using System.Web; using System.Web.Routing; @@ -48,6 +50,20 @@ internal sealed class HttpRequestRouteHelper return template; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CompareStringToSubstring(string example, string target, int start) + { + for (int i = 0; i < example.Length; i++) + { + if (target[start + 1 + i] != example[i]) + { + return false; + } + } + + return true; + } + private static string PrepareRouteTemplate(Route route, RouteData routeData) { const string controllerToken = "controller"; @@ -56,32 +72,58 @@ private static string PrepareRouteTemplate(Route route, RouteData routeData) var template = route.Url; var controller = (string)routeData.Values[controllerToken]; var action = (string)routeData.Values[actionToken]; + var hasController = !string.IsNullOrWhiteSpace(controller); + var hasAction = !string.IsNullOrWhiteSpace(action); + var sb = new StringBuilder(template.Length); - if (!string.IsNullOrWhiteSpace(controller)) + int i = 0; + while (i < template.Length) { - template = template.Replace($"{{{controllerToken}}}", controller); - } + if (template[i] == '{') + { + int end = template.IndexOf('}', i + 1); + if (end != -1) + { + if (hasController && CompareStringToSubstring(controllerToken, template, i)) + { + sb.Append(controller); + } + else if (hasAction && CompareStringToSubstring(actionToken, template, i)) + { + sb.Append(action); + } + else + { + var defaults = route.Defaults; + var values = routeData.Values; + var token = template.Substring(i + 1, end - i - 1); + + if (defaults.ContainsKey(token) && !values.ContainsKey(token)) + { + // Ignore defaults with no values. + } + else + { + sb.Append('{').Append(token).Append('}'); + } + } + + i = end + 1; + continue; + } + } - if (!string.IsNullOrWhiteSpace(action)) - { - template = template.Replace($"{{{actionToken}}}", action); + sb.Append(template[i]); + i++; } - // Remove defaults with no values. - var defaultKeys = route.Defaults.Keys; - var valueKeys = routeData.Values.Keys; - - foreach (var token in defaultKeys) + // Normalizes endings by removing trailing slashes. + int len = sb.Length; + while (len > 0 && sb[len - 1] == '/') { - if (valueKeys.Contains(token)) - { - continue; - } - - template = template.Replace($"{{{token}}}", string.Empty); + len--; } - return template - .TrimEnd('/'); // Normalizes endings + return sb.ToString(0, len); } }