Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
<!-- ASP.NET Core -->
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.Middleware" Version="$(MicrosoftExtensionsVersion)"/>
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="$(DotNetRuntimeVersion)"/>
<!-- Transitive security override: Microsoft.AspNetCore.OpenApi pulls Microsoft.OpenApi 2.0.0, which
has GHSA-v5pm-xwqc-g5wc (NU1903, high). 2.7.5 is the first patched 2.x. Not functionally exercised
(collector runs with EnableOpenApi=false); this only lifts the transitive off the vulnerable range. -->
<PackageVersion Include="Microsoft.OpenApi" Version="2.7.5"/>
<!-- Service Discovery & Resilience -->
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(DotNetRuntimeVersion)"/>
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(MicrosoftExtensionsVersion)"/>
Expand Down Expand Up @@ -74,7 +78,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. -->
<PackageVersion Include="Qyl.OpenTelemetry.AutoInstrumentation" Version="3.1.2"/>
<PackageVersion Include="Qyl.OpenTelemetry.AutoInstrumentation" Version="4.0.0"/>
<PackageVersion Include="Qyl.OpenTelemetry.SemanticConventions" Version="3.1.0"/>
<PackageVersion Include="Qyl.OpenTelemetry.SemanticConventions.Incubating" Version="3.1.0"/>
<!-- C# API contract types emitted from @ancplua/qyl-api-schema. -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PackageReference Include=\"Qyl.OpenTelemetry.AutoInstrumentation\" /> 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;

Expand Down Expand Up @@ -60,20 +46,15 @@ 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()
.WithTrackingName(PipelineStage.BuilderCallSitesDiscovered);

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);
}
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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}}
Expand All @@ -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;
}
Expand All @@ -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<BuilderCallSite> CallSites,
bool QylRuntimeAvailable,
bool OtelAutoInstrumentationAvailable);
bool QylRuntimeAvailable);
}
4 changes: 4 additions & 0 deletions services/qyl.collector/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");

Expand All @@ -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();
Expand Down
5 changes: 0 additions & 5 deletions services/qyl.collector/qyl.collector.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@
<PublishTrimmed>false</PublishTrimmed>
<IsAotCompatible>false</IsAotCompatible>
<InterceptorsNamespaces>$(InterceptorsNamespaces);Qyl.Instrumentation.Generators</InterceptorsNamespaces>
<!-- qyl.collector runs two interceptor generators that both target WebApplicationBuilder.Build():
ServiceDefaults (DI/health/endpoints) and Qyl.OpenTelemetry.AutoInstrumentation (OTel middleware).
C# forbids two interceptors per call site (CS9153), so hand Build() to ServiceDefaults — which
composes QylInterceptedAspNetCore.Build — and opt the OTel generator out of intercepting Build(). -->
<QylAutoInstrumentationInterceptWebApplicationBuilderBuild>false</QylAutoInstrumentationInterceptWebApplicationBuilderBuild>
</PropertyGroup>

<PropertyGroup>
Expand Down
Loading