From 41f948ad02f620994f6ae79db6762e66fb10238e Mon Sep 17 00:00:00 2001 From: ancplua Date: Tue, 30 Jun 2026 03:25:32 +0200 Subject: [PATCH 1/3] fix(collector): compose OTel Build() into ServiceDefaults to resolve CS9153 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qyl.collector ran two interceptor generators that both targeted WebApplicationBuilder.Build(): ServiceDefaults (DI/hosted/health/endpoints) and Qyl.OpenTelemetry.AutoInstrumentation (OTel middleware). C# forbids two interceptors per call site, so the Release build failed with CS9153. Make ServiceDefaults the single owner of Build() and compose the OTel wrapper: - ServiceDefaultsSourceGenerator detects Qyl.OpenTelemetry.AutoInstrumentation (via QylInterceptedAspNetCore) and routes its Build() interceptor through QylInterceptedAspNetCore.Build(builder) (build + OTel middleware) instead of builder.Build(); without the package it builds the host directly (unchanged). - qyl.collector sets QylAutoInstrumentationInterceptWebApplicationBuilderBuild=false so the OTel generator yields Build() to ServiceDefaults. Requires Qyl.OpenTelemetry.AutoInstrumentation 3.1.1 (adds the opt-out). Version pin bump is gated on 3.1.1 indexing on nuget.org — WIP until then. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../GeneratorPipelineHelpers.cs | 8 ++++ .../ServiceDefaultsSourceGenerator.cs | 38 +++++++++++++++---- services/qyl.collector/qyl.collector.csproj | 5 +++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/internal/qyl.instrumentation.generators/GeneratorPipelineHelpers.cs b/internal/qyl.instrumentation.generators/GeneratorPipelineHelpers.cs index f2ce0c709..688f82750 100644 --- a/internal/qyl.instrumentation.generators/GeneratorPipelineHelpers.cs +++ b/internal/qyl.instrumentation.generators/GeneratorPipelineHelpers.cs @@ -9,10 +9,18 @@ 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; + public static bool IsPipelineEnabled(AnalyzerConfigOptionsProvider options, string propertyName) => !options.GlobalOptions.TryGetValue($"build_property.{propertyName}", out var value) || !string.Equals(value, "false", StringComparison.OrdinalIgnoreCase); diff --git a/internal/qyl.instrumentation.generators/ServiceDefaultsSourceGenerator.cs b/internal/qyl.instrumentation.generators/ServiceDefaultsSourceGenerator.cs index b98b11e63..577eab655 100644 --- a/internal/qyl.instrumentation.generators/ServiceDefaultsSourceGenerator.cs +++ b/internal/qyl.instrumentation.generators/ServiceDefaultsSourceGenerator.cs @@ -46,6 +46,10 @@ 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() @@ -53,7 +57,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var builderInput = builderCallSites.CollectAsEquatableArray() .Combine(runtimeAvailable) - .Select(static (input, _) => new BuilderInterceptorInput(input.Left, input.Right)); + .Combine(otelAutoInstrumentationAvailable) + .Select(static (input, _) => + new BuilderInterceptorInput(input.Left.Left, input.Left.Right, input.Right)); context.RegisterSourceOutput(builderInput, EmitBuilderInterceptors); } @@ -96,11 +102,15 @@ private static void EmitBuilderInterceptors(SourceProductionContext context, Bui if (!input.QylRuntimeAvailable) return; - var source = GenerateBuilderInterceptorSource(input.CallSites.AsImmutableArray()); + var source = GenerateBuilderInterceptorSource( + input.CallSites.AsImmutableArray(), + input.OtelAutoInstrumentationAvailable); context.AddSource(BuilderInterceptorsFile, SourceText.From(source, Encoding.UTF8)); } - private static string GenerateBuilderInterceptorSource(ImmutableArray callSites) + private static string GenerateBuilderInterceptorSource( + ImmutableArray callSites, + bool otelAutoInstrumentationAvailable) { var sb = new StringBuilder(); @@ -114,7 +124,7 @@ private static string GenerateBuilderInterceptorSource(ImmutableArray c.SortKey, StringComparer.Ordinal)) { if (callSite.Kind is BuilderCallKind.Build) - AppendBuildInterceptorMethod(sb, callSite, index); + AppendBuildInterceptorMethod(sb, callSite, index, otelAutoInstrumentationAvailable); index++; } @@ -122,11 +132,23 @@ private static string GenerateBuilderInterceptorSource(ImmutableArray CallSites, - bool QylRuntimeAvailable); + bool QylRuntimeAvailable, + bool OtelAutoInstrumentationAvailable); } diff --git a/services/qyl.collector/qyl.collector.csproj b/services/qyl.collector/qyl.collector.csproj index 9c7e8333c..6e3e55ddc 100644 --- a/services/qyl.collector/qyl.collector.csproj +++ b/services/qyl.collector/qyl.collector.csproj @@ -8,6 +8,11 @@ false false $(InterceptorsNamespaces);Qyl.Instrumentation.Generators + + false false From 541bac8081a153568eb7b7b0c992519d4a872d04 Mon Sep 17 00:00:00 2001 From: ancplua Date: Tue, 30 Jun 2026 04:15:49 +0200 Subject: [PATCH 2/3] fix(collector): pin Qyl.OpenTelemetry.AutoInstrumentation 3.1.2 (resolves CS9153 + CS9207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the OTel auto-instrumentation pin to 3.1.2, which carries both fixes the collector build needs: - #16 (3.1.1): WebApplicationBuilder.Build() interceptor opt-out, so ServiceDefaults owns Build() and composes QylInterceptedAspNetCore.Build — resolves CS9153. - #17 (3.1.2): the generator no longer emits an un-interceptable RequestDelegate.Invoke interceptor for hand-written middleware next(context) hops — resolves CS9207. Verified: a clean-cache restore resolves 3.1.2 from nuget.org (not a local feed), and `dotnet build services/qyl.collector -c Release` succeeds with 0 warnings / 0 errors. Health endpoints preserved — ServiceDefaults still registers health checks and maps MapQylDefaultEndpoints around the composed OTel Build(). Co-Authored-By: Claude Opus 4.8 (1M context) --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f9cb6ce66..ff9e10220 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. --> - + From b2f33265ce7d2855a9c7bf9c35f7eb64ef48543c Mon Sep 17 00:00:00 2001 From: ancplua Date: Tue, 30 Jun 2026 04:51:43 +0200 Subject: [PATCH 3/3] ci: drop redundant Actions NuGet cache on the self-hosted runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Backend (.NET) job cached ~/.nuget/packages via setup-dotnet, keyed on dependency files including Directory.Packages.props. On the self-hosted runner that folder already persists on local disk between jobs, so the cache is redundant — and this PR's pin bump changed the key, forcing a post-job tar+upload of the accumulated 8.1 GB global packages folder (~17 min at ~5 MB/s) that serialized the whole PR (Frontend queued behind it). Disable the cache; restore stays fast from local disk. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15b5e6876..a27b0612d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,13 +57,12 @@ jobs: uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: global-json-file: global.json - cache: true - cache-dependency-path: | - **/Directory.Packages.props - **/*.csproj - **/*.fsproj - **/packages.lock.json - **/nuget.config + # No Actions NuGet cache on the self-hosted runner: ~/.nuget/packages already persists on + # the runner's local disk between jobs, so the cache is redundant. Worse, any change to a + # dependency file (e.g. a Directory.Packages.props version bump) misses the key and forces a + # post-job tar+upload of the accumulated multi-GB global packages folder (~17 min at ~5 MB/s), + # which serializes the whole PR behind one cache-save step. Restore stays fast from local disk. + cache: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6