From a8bfd0e1a42b77f7d77d77b51eeb5c7511e1fee7 Mon Sep 17 00:00:00 2001 From: ancplua Date: Wed, 1 Jul 2026 20:30:32 +0200 Subject: [PATCH] =?UTF-8?q?fix(otel):=20semconv=20completeness=20=E2=80=94?= =?UTF-8?q?=20url.scheme,=20http.request.method=5Foriginal,=20Azure=20oper?= =?UTF-8?q?ation=20(CODE=20RED=20#6/#7/#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #6 (url.scheme, Required on HTTP server spans): StartServerActivity now sets url.scheme from context.Request.Scheme (threaded through QylInterceptedAspNetCore). Added to the AOT demo keep-list so the fixture asserts it. #7 (http.request.method_original): the interceptor client + server paths dropped the original method when Normalize collapsed a non-standard verb to _OTHER. New QylHttpMethod.Normalize(method, out original) returns the raw value for non-standard methods; SetRequestMethod emits http.request.method_original. Threaded through StartClientActivity / StartServerActivity and all three callers (HttpClient, HttpWebRequest, AspNetCore). #9 (Azure operation): QylInterceptedAzure.StartActivity received methodName and dropped it (every Azure span named the fixed "Azure SDK"). Now records it on code.function.name so spans are distinguishable (span name stays low-cardinality per Azure semconv). New public consts QylSemanticAttributes.UrlScheme + CodeFunctionName (PublicAPI.Unshipped updated). Verified: core 0/0; verify-webapi-aot-demo (fixture now asserts url.scheme=http on the server span), source-interceptor-consumer, public-api-baseline all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Internal/QylHttpActivityPolicy.cs | 19 ++++++++++++++----- .../PublicAPI.Unshipped.txt | 2 ++ .../QylHttpMethod.cs | 15 +++++++++++++++ .../QylInterceptedAspNetCore.cs | 6 ++++-- .../QylInterceptedAzure.cs | 8 +++++++- .../QylInterceptedHttpClient.cs | 3 ++- .../QylInterceptedHttpWebRequest.cs | 3 ++- .../QylSemanticAttributes.cs | 4 ++++ .../Program.cs | 1 + .../verified/report.json | 6 ++++-- 10 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylHttpActivityPolicy.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylHttpActivityPolicy.cs index e4f31de..80ca71b 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylHttpActivityPolicy.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylHttpActivityPolicy.cs @@ -7,6 +7,7 @@ internal static class QylHttpActivityPolicy public static Activity? StartClientActivity( string instrumentationDomain, string method, + string? methodOriginal, Uri? requestUri, string? rawRequestUri) { @@ -18,7 +19,7 @@ internal static class QylHttpActivityPolicy if (activity is null) return null; - SetRequestMethod(activity, method); + SetRequestMethod(activity, method, methodOriginal); if (requestUri is not null) SetClientUrl(activity, requestUri, rawRequestUri); @@ -27,9 +28,11 @@ internal static class QylHttpActivityPolicy public static Activity? StartServerActivity( string method, + string? methodOriginal, string? route, string? path, - string? query) + string? query, + string? scheme) { var activity = QylActivityFactory.StartTraceActivity( QylAutoInstrumentationIds.AspNetCore, @@ -39,7 +42,9 @@ internal static class QylHttpActivityPolicy if (activity is null) return null; - SetRequestMethod(activity, method); + SetRequestMethod(activity, method, methodOriginal); + if (!string.IsNullOrEmpty(scheme)) + activity.SetTag(QylSemanticAttributes.UrlScheme, scheme); if (path is not null) activity.SetTag(QylSemanticAttributes.UrlPath, path); if (!string.IsNullOrEmpty(query)) @@ -73,8 +78,12 @@ public static void SetResponseStatus(Activity activity, int statusCode, int erro public static void SetResponseStatus(Activity activity, int statusCode) => activity.SetTag(QylSemanticAttributes.HttpResponseStatusCode, statusCode); - private static void SetRequestMethod(Activity activity, string method) - => activity.SetTag(QylSemanticAttributes.HttpRequestMethod, method); + private static void SetRequestMethod(Activity activity, string method, string? methodOriginal) + { + activity.SetTag(QylSemanticAttributes.HttpRequestMethod, method); + if (!string.IsNullOrEmpty(methodOriginal)) + activity.SetTag(QylSemanticAttributes.HttpRequestMethodOriginal, methodOriginal); + } private static void SetClientUrl(Activity activity, Uri requestUri, string? rawRequestUri) { diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/PublicAPI.Unshipped.txt b/src/Qyl.OpenTelemetry.AutoInstrumentation/PublicAPI.Unshipped.txt index e0ce27c..1bf8e86 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/PublicAPI.Unshipped.txt +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +const Qyl.OpenTelemetry.AutoInstrumentation.QylSemanticAttributes.CodeFunctionName = "code.function.name" -> string! +const Qyl.OpenTelemetry.AutoInstrumentation.QylSemanticAttributes.UrlScheme = "url.scheme" -> string! const Qyl.OpenTelemetry.AutoInstrumentation.QylMetricMeters.AspNetCoreAuthenticationMeterName = "Microsoft.AspNetCore.Authentication" -> string! const Qyl.OpenTelemetry.AutoInstrumentation.QylMetricMeters.AspNetCoreAuthorizationMeterName = "Microsoft.AspNetCore.Authorization" -> string! const Qyl.OpenTelemetry.AutoInstrumentation.QylMetricMeters.AspNetCoreDiagnosticsMeterName = "Microsoft.AspNetCore.Diagnostics" -> string! diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylHttpMethod.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylHttpMethod.cs index cb4b9b6..3c662e1 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylHttpMethod.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylHttpMethod.cs @@ -36,4 +36,19 @@ public static string Normalize(string? method) return QylSemanticAttributes.HttpRequestMethodOther; } + + /// + /// Normalizes and reports the raw value as when it + /// is non-standard. Per OTel, http.request.method_original MUST be set whenever http.request.method is + /// _OTHER; is null for the standard methods. + /// + public static string Normalize(string? method, out string? original) + { + var normalized = Normalize(method); + original = !string.IsNullOrEmpty(method) + && string.Equals(normalized, QylSemanticAttributes.HttpRequestMethodOther, StringComparison.Ordinal) + ? method + : null; + return normalized; + } } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAspNetCore.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAspNetCore.cs index cd7316f..65324b5 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAspNetCore.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAspNetCore.cs @@ -63,13 +63,15 @@ public static Task InvokeAsync(RequestDelegate requestDelegate, HttpContext cont if (!options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.AspNetCore)) return null; - var method = QylHttpMethod.Normalize(context.Request.Method); + var method = QylHttpMethod.Normalize(context.Request.Method, out var methodOriginal); var route = GetRoute(context); var activity = QylHttpActivityPolicy.StartServerActivity( method, + methodOriginal, route, context.Request.Path.Value, - context.Request.QueryString.HasValue ? context.Request.QueryString.Value![1..] : null); + context.Request.QueryString.HasValue ? context.Request.QueryString.Value![1..] : null, + context.Request.Scheme); if (activity is null) return null; diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAzure.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAzure.cs index 12a4aae..4d68a19 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAzure.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAzure.cs @@ -12,11 +12,17 @@ public static class QylInterceptedAzure /// Runs the Start Activity runtime helper used by source-generated qyl interceptors. public static Activity? StartActivity(string methodName) { - return QylActivityFactory.StartTraceActivity( + var activity = QylActivityFactory.StartTraceActivity( QylAutoInstrumentationIds.Azure, QylActivityNames.AzureSdk, ActivityKind.Client, QylInstrumentationDomains.AzureSdk); + // Record the intercepted operation so Azure spans are distinguishable. The span name stays the + // stable low-cardinality "Azure SDK"; the specific method goes on code.function.name (CODE RED #9: + // the methodName parameter was previously received and dropped). + if (activity is not null && !string.IsNullOrEmpty(methodName)) + activity.SetTag(QylSemanticAttributes.CodeFunctionName, methodName); + return activity; } /// Runs the Record Exception runtime helper used by source-generated qyl interceptors. diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpClient.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpClient.cs index 53914ae..abe692c 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpClient.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpClient.cs @@ -584,7 +584,7 @@ private static HttpClientObservation StartHttpClientObservation( Uri? requestUri, string? rawRequestUri) { - method = QylHttpMethod.Normalize(method); + method = QylHttpMethod.Normalize(method, out var methodOriginal); var startTimeUtc = QylDurationMetrics.GetHttpClientStartTimeUtc(metricsEnabled); Activity? activity = null; @@ -594,6 +594,7 @@ private static HttpClientObservation StartHttpClientObservation( activity = QylHttpActivityPolicy.StartClientActivity( QylInstrumentationDomains.HttpClient, method, + methodOriginal, requestUri, rawRequestUri); } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpWebRequest.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpWebRequest.cs index 8ac1a64..685b2fa 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpWebRequest.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpWebRequest.cs @@ -21,10 +21,11 @@ public static DateTime GetStartTimeUtc() if (!options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.HttpClient)) return null; - var method = QylHttpMethod.Normalize(request.Method); + var method = QylHttpMethod.Normalize(request.Method, out var methodOriginal); var activity = QylHttpActivityPolicy.StartClientActivity( QylInstrumentationDomains.HttpWebRequest, method, + methodOriginal, request.RequestUri, request.RequestUri?.ToString()); if (activity is null) diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylSemanticAttributes.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylSemanticAttributes.cs index 6873b76..a199519 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylSemanticAttributes.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylSemanticAttributes.cs @@ -62,6 +62,10 @@ public static class QylSemanticAttributes public const string UrlQuery = UrlAttributes.Query; /// Well-known Url Full value used by qyl auto-instrumentation. public const string UrlFull = UrlAttributes.Full; + /// Well-known Url Scheme value used by qyl auto-instrumentation. + public const string UrlScheme = "url.scheme"; + /// Well-known Code Function Name value used by qyl auto-instrumentation. + public const string CodeFunctionName = "code.function.name"; /// Well-known Dotnet Gc Heap Generation value used by qyl auto-instrumentation. public const string DotnetGcHeapGeneration = DotnetAttributes.GcHeapGeneration; diff --git a/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/Program.cs b/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/Program.cs index 3da041e..2b6455a 100644 --- a/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/Program.cs +++ b/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/Program.cs @@ -218,6 +218,7 @@ private static IReadOnlyDictionary Canonicalize(IReadOnlyDiction "qyl.instrumentation.domain", "http.request.method", "http.route", + "url.scheme", "http.response.status_code", "server.address", "server.port", diff --git a/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/verified/report.json b/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/verified/report.json index 51b2342..f71175e 100644 --- a/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/verified/report.json +++ b/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/verified/report.json @@ -71,7 +71,8 @@ "http.request.method": "GET", "http.response.status_code": "204", "http.route": "/probe/{id:int}", - "qyl.instrumentation.domain": "aspnetcore.server" + "qyl.instrumentation.domain": "aspnetcore.server", + "url.scheme": "http" } }, { @@ -101,7 +102,8 @@ "http.request.method": "GET", "http.response.status_code": "204", "http.route": "/probe/{id:int}", - "qyl.instrumentation.domain": "aspnetcore.server" + "qyl.instrumentation.domain": "aspnetcore.server", + "url.scheme": "http" } }, {