From 3382f9605d6548046b835d5529518db1b71c7ea4 Mon Sep 17 00:00:00 2001 From: ancplua Date: Wed, 1 Jul 2026 08:22:07 +0200 Subject: [PATCH 1/3] refactor(generators)!: drop the Build()-interceptor coordination layer Qyl.OpenTelemetry.AutoInstrumentation no longer intercepts WebApplicationBuilder.Build() (it moved its ASP.NET Core middleware to an IStartupFilter). The cross-generator coordination that let qyl's ServiceDefaults Build() interceptor coexist with the package's Build() interceptor is now dead, so remove it: - GeneratorPipelineHelpers: drop QylInterceptedAspNetCoreTypeName + IsOtelAutoInstrumentationReferenced. - ServiceDefaultsSourceGenerator: drop the otelAutoInstrumentationAvailable pipeline stage, the BuilderInterceptorInput field it fed, and the QYL0138 fail-fast diagnostic (there is no longer a package whose absence would silently drop OTel middleware). - AnalyzerReleases.Unshipped.md: remove the QYL0138 row to match the deleted descriptor (fixes RS2002). Generator project builds green (0 warnings/0 errors). Pairs with the AutoInstrumentation-side cut. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../AnalyzerReleases.Unshipped.md | 1 - .../GeneratorPipelineHelpers.cs | 8 --- .../ServiceDefaultsSourceGenerator.cs | 49 ++++--------------- 3 files changed, 9 insertions(+), 49 deletions(-) diff --git a/internal/qyl.instrumentation.generators/AnalyzerReleases/AnalyzerReleases.Unshipped.md b/internal/qyl.instrumentation.generators/AnalyzerReleases/AnalyzerReleases.Unshipped.md index 0f171488b..221debedc 100644 --- a/internal/qyl.instrumentation.generators/AnalyzerReleases/AnalyzerReleases.Unshipped.md +++ b/internal/qyl.instrumentation.generators/AnalyzerReleases/AnalyzerReleases.Unshipped.md @@ -5,4 +5,3 @@ QYL0135 | Qyl.Instrumentation | Warning | Agent invoked without composition-root OpenTelemetry wrapping QYL0136 | Qyl.Instrumentation | Warning | Inline system-prompt literal — move to `Data/Instructions/*.md` QYL0137 | Qyl.Instrumentation | Warning | Provider SDK client instantiated outside a sanctioned ChatClientBuilder/Factory - QYL0138 | Qyl.Instrumentation | Error | WebApplicationBuilder.Build() intercepted but Qyl.OpenTelemetry.AutoInstrumentation not referenced diff --git a/internal/qyl.instrumentation.generators/GeneratorPipelineHelpers.cs b/internal/qyl.instrumentation.generators/GeneratorPipelineHelpers.cs index e29d21fef..eb4b67618 100644 --- a/internal/qyl.instrumentation.generators/GeneratorPipelineHelpers.cs +++ b/internal/qyl.instrumentation.generators/GeneratorPipelineHelpers.cs @@ -8,15 +8,7 @@ internal static class GeneratorPipelineHelpers public const string QylServiceDefaultsTypeName = "Qyl.Instrumentation.QylServiceDefaults"; public const string WebApplicationBuilderTypeName = "Microsoft.AspNetCore.Builder.WebApplicationBuilder"; public const string WebApplicationTypeName = "Microsoft.AspNetCore.Builder.WebApplication"; - public const string QylInterceptedAspNetCoreTypeName = "Qyl.OpenTelemetry.AutoInstrumentation.QylInterceptedAspNetCore"; public static bool IsQylRuntimeReferenced(Compilation compilation, CancellationToken _) => compilation.GetTypeByMetadataName(QylServiceDefaultsTypeName) is not null; - - // When Qyl.OpenTelemetry.AutoInstrumentation is referenced, our Build() interceptor routes the - // build through its QylInterceptedAspNetCore.Build wrapper (which adds the OTel middleware) so a - // single interceptor owns the call site. The consumer sets the package's opt-out property so the - // OTel generator yields Build() to us — otherwise the two interceptors would collide (CS9153). - public static bool IsOtelAutoInstrumentationReferenced(Compilation compilation, CancellationToken _) => - compilation.GetTypeByMetadataName(QylInterceptedAspNetCoreTypeName) is not null; } diff --git a/internal/qyl.instrumentation.generators/ServiceDefaultsSourceGenerator.cs b/internal/qyl.instrumentation.generators/ServiceDefaultsSourceGenerator.cs index 00c87cad2..dcf8fcd71 100644 --- a/internal/qyl.instrumentation.generators/ServiceDefaultsSourceGenerator.cs +++ b/internal/qyl.instrumentation.generators/ServiceDefaultsSourceGenerator.cs @@ -14,20 +14,6 @@ public sealed class ServiceDefaultsSourceGenerator : IIncrementalGenerator { private const string BuilderInterceptorsFile = "Intercepts.g.cs"; - // Fail fast (QYL0138): if a WebApplicationBuilder.Build() call site exists and the Qyl runtime is - // referenced but Qyl.OpenTelemetry.AutoInstrumentation is not, we do NOT emit a degraded interceptor - // that silently drops the OTel middleware — we surface a build error instead. Nothing is swallowed. - private static readonly DiagnosticDescriptor MissingAutoInstrumentation = new( - "QYL0138", - "Qyl.OpenTelemetry.AutoInstrumentation package is required", - "A WebApplicationBuilder.Build() call site was found and the Qyl runtime is referenced, but " - + "Qyl.OpenTelemetry.AutoInstrumentation is not. The Build() auto-instrumentation interceptor was " - + "not generated. Add so the " - + "OTel middleware is wired, or remove the Qyl runtime dependency.", - "Qyl.Instrumentation", - DiagnosticSeverity.Error, - isEnabledByDefault: true); - private const string InterceptsLocationAttributeDeclaration = """ using Qyl.Instrumentation; @@ -60,10 +46,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select(GeneratorPipelineHelpers.IsQylRuntimeReferenced) .WithTrackingName(PipelineStage.QylRuntimeCheck); - var otelAutoInstrumentationAvailable = context.CompilationProvider - .Select(GeneratorPipelineHelpers.IsOtelAutoInstrumentationReferenced) - .WithTrackingName(PipelineStage.OtelAutoInstrumentationCheck); - var builderCallSites = context.SyntaxProvider .CreateSyntaxProvider(CouldBeBuildInvocation, ExtractBuilderCallSite) .WhereNotNull() @@ -71,9 +53,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var builderInput = builderCallSites.CollectAsEquatableArray() .Combine(runtimeAvailable) - .Combine(otelAutoInstrumentationAvailable) .Select(static (input, _) => - new BuilderInterceptorInput(input.Left.Left, input.Left.Right, input.Right)); + new BuilderInterceptorInput(input.Left, input.Right)); context.RegisterSourceOutput(builderInput, EmitBuilderInterceptors); } @@ -120,15 +101,6 @@ private static void EmitBuilderInterceptors(SourceProductionContext context, Bui if (callSites.IsEmpty) return; - if (!input.OtelAutoInstrumentationAvailable) - { - // Fail fast rather than silently emitting an interceptor that builds the host without the OTel - // middleware wrapper. Reported once (Location.None) — it is a missing-package configuration - // error, not a per-call-site code error. - context.ReportDiagnostic(Diagnostic.Create(MissingAutoInstrumentation, Location.None)); - return; - } - var source = GenerateBuilderInterceptorSource(callSites); context.AddSource(BuilderInterceptorsFile, SourceText.From(source, Encoding.UTF8)); } @@ -163,13 +135,12 @@ private static void AppendBuildInterceptorMethod( var displayLocation = callSite.Location.GetDisplayLocation(); var interceptAttribute = callSite.Location.GetInterceptsLocationAttributeSyntax(); - // Route the build through Qyl.OpenTelemetry.AutoInstrumentation's QylInterceptedAspNetCore.Build - // wrapper (build + OTel middleware) so this stays the single interceptor on the call site. The - // consumer opts the OTel generator out of Build() so the two do not collide (CS9153). The package - // is guaranteed present here — EmitBuilderInterceptors raises QYL0138 when it is absent, so there - // is no silent no-OTel fallback. - var buildExpression = $"global::{GeneratorPipelineHelpers.QylInterceptedAspNetCoreTypeName}.Build(builder)"; - + // Wire qyl ServiceDefaults (conventions, generated service + health-check registration, and + // default endpoints) at the single WebApplicationBuilder.Build() call site, then build the host. + // The OTel ASP.NET Core server-span middleware is registered independently via + // AddQylAspNetCoreInstrumentation() (an IStartupFilter), so this interceptor no longer composes it + // and no longer contends with the OTel package for the Build() call site (the CS9153 collision that + // the old QylInterceptedAspNetCore.Build wrapper + opt-out property existed to work around). sb.AppendLine($$""" // Intercepted call at {{displayLocation}} {{interceptAttribute}} @@ -178,7 +149,7 @@ private static void AppendBuildInterceptorMethod( { builder.TryUseQylConventions(); global::Qyl.Instrumentation.Generators.QylGeneratedRegistry.RegisterQylHealthChecks(builder.Services); - var app = {{buildExpression}}; + var app = builder.Build(); app.MapQylDefaultEndpoints(); return app; } @@ -188,12 +159,10 @@ private static void AppendBuildInterceptorMethod( private static class PipelineStage { public const string QylRuntimeCheck = nameof(QylRuntimeCheck); - public const string OtelAutoInstrumentationCheck = nameof(OtelAutoInstrumentationCheck); public const string BuilderCallSitesDiscovered = nameof(BuilderCallSitesDiscovered); } private readonly record struct BuilderInterceptorInput( EquatableArray CallSites, - bool QylRuntimeAvailable, - bool OtelAutoInstrumentationAvailable); + bool QylRuntimeAvailable); } From ec4475181cdaafa6d92896624fe61a5f585ea650 Mon Sep 17 00:00:00 2001 From: ancplua Date: Wed, 1 Jul 2026 11:19:00 +0200 Subject: [PATCH 2/3] feat(collector)!: complete the Qyl.OpenTelemetry.AutoInstrumentation 4.0.0 cutover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with the generator-side cut (drop the Build() compose + QYL0138) to finish the lockstep migration to 4.0.0, which removed QylInterceptedAspNetCore.Build and injects the ASP.NET Core server-span middleware via IStartupFilter instead. - Directory.Packages.props: pin 3.1.2 -> 4.0.0 - qyl.collector.csproj: drop the QylAutoInstrumentationInterceptWebApplicationBuilderBuild opt-out knob (no longer needed — the OTel generator no longer intercepts Build()). - Program.cs: builder.Services.AddQylAspNetCoreInstrumentation() so the collector keeps its server-request span (the Build() interceptor that used to wire it is gone). Verified: `dotnet build services/qyl.collector -c Release` succeeds 0/0 against the published 4.0.0 (ServiceDefaults owns Build() alone -> no CS9153). The unrelated NU1903 Microsoft.OpenApi advisory (transitive via Microsoft.AspNetCore.OpenApi) is orthogonal to this change and blocks any collector build repo-wide until deps are refreshed. Co-Authored-By: Claude Opus 4.8 (1M context) --- Directory.Packages.props | 2 +- services/qyl.collector/Program.cs | 4 ++++ services/qyl.collector/qyl.collector.csproj | 5 ----- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1a08d52a6..469129989 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,7 +74,7 @@ the contrib donation goes through its ~6-month review (see [[dual-target-otel-shipping]]). When #4424 merges these PackageReference rows swap to the upstream id — type identities preserved, so consumer code keeps compiling. --> - + diff --git a/services/qyl.collector/Program.cs b/services/qyl.collector/Program.cs index 9c006d7f4..7a1810595 100644 --- a/services/qyl.collector/Program.cs +++ b/services/qyl.collector/Program.cs @@ -2,6 +2,7 @@ using Qyl.Collector.Hosting; using Qyl.Collector.Telemetry; using Qyl.Instrumentation.Instrumentation; +using Qyl.OpenTelemetry.AutoInstrumentation; Console.WriteLine($"[qyl] Process starting at {TimeProvider.System.GetUtcNow():O}"); @@ -24,6 +25,9 @@ builder.Services.AddQylCollectorStorage(); builder.Services.AddQylCollectorAuth(builder.Configuration, builder.Environment); builder.Services.AddQylCollectorTelemetry(builder.Environment); +// Server-request spans: 4.0.0 injects the middleware via IStartupFilter (the Build() interceptor that +// used to wire it is gone), so register it explicitly. ServiceDefaults owns Build() alone now — no CS9153. +builder.Services.AddQylAspNetCoreInstrumentation(); builder.WebHost.ConfigureQylCollectorKestrel(ports); var app = builder.Build(); diff --git a/services/qyl.collector/qyl.collector.csproj b/services/qyl.collector/qyl.collector.csproj index c215737ef..e39f33eed 100644 --- a/services/qyl.collector/qyl.collector.csproj +++ b/services/qyl.collector/qyl.collector.csproj @@ -8,11 +8,6 @@ false false $(InterceptorsNamespaces);Qyl.Instrumentation.Generators - - false From 2f92eb41bd041f76910037fb774c8b613b80ec7e Mon Sep 17 00:00:00 2001 From: ancplua Date: Wed, 1 Jul 2026 11:24:12 +0200 Subject: [PATCH 3/3] fix(deps): pin Microsoft.OpenApi 2.7.5 to clear NU1903 (GHSA-v5pm-xwqc-g5wc) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Microsoft.AspNetCore.OpenApi pulls Microsoft.OpenApi 2.0.0 transitively, which a newly-live advisory (GHSA-v5pm-xwqc-g5wc, high) flags — failing the Dependency Audit + Backend builds repo-wide (main was last green 2026-06-30, before the advisory). 2.7.5 is the first patched 2.x. Collector runs with EnableOpenApi=false, so this is not functionally exercised — it only lifts the transitive off the vulnerable range. Verified: collector Release builds 0/0 with NuGetAudit ON. Co-Authored-By: Claude Opus 4.8 (1M context) --- Directory.Packages.props | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Directory.Packages.props b/Directory.Packages.props index 469129989..59d97a66f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -42,6 +42,10 @@ + +