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
13 changes: 6 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -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. -->
<PackageVersion Include="Qyl.OpenTelemetry.AutoInstrumentation" Version="3.1.0"/>
<PackageVersion Include="Qyl.OpenTelemetry.AutoInstrumentation" Version="3.1.2"/>
<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 @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,20 @@ 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)
.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);
}
Expand Down Expand Up @@ -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<BuilderCallSite> callSites)
private static string GenerateBuilderInterceptorSource(
ImmutableArray<BuilderCallSite> callSites,
bool otelAutoInstrumentationAvailable)
{
var sb = new StringBuilder();

Expand All @@ -114,19 +124,31 @@ private static string GenerateBuilderInterceptorSource(ImmutableArray<BuilderCal
foreach (var callSite in callSites.OrderBy(static c => c.SortKey, StringComparer.Ordinal))
{
if (callSite.Kind is BuilderCallKind.Build)
AppendBuildInterceptorMethod(sb, callSite, index);
AppendBuildInterceptorMethod(sb, callSite, index, otelAutoInstrumentationAvailable);
index++;
}

sb.AppendLine(InterceptorsNamespaceClose);
return sb.ToString();
}

private static void AppendBuildInterceptorMethod(StringBuilder sb, BuilderCallSite callSite, int index)
private static void AppendBuildInterceptorMethod(
StringBuilder sb,
BuilderCallSite callSite,
int index,
bool otelAutoInstrumentationAvailable)
{
var displayLocation = callSite.Location.GetDisplayLocation();
var interceptAttribute = callSite.Location.GetInterceptsLocationAttributeSyntax();

// When Qyl.OpenTelemetry.AutoInstrumentation is referenced, route the build through its
// 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). Without the package, build the host directly.
var buildExpression = otelAutoInstrumentationAvailable
? $"global::{GeneratorPipelineHelpers.QylInterceptedAspNetCoreTypeName}.Build(builder)"
: "builder.Build()";

sb.AppendLine($$"""
// Intercepted call at {{displayLocation}}
{{interceptAttribute}}
Expand All @@ -137,7 +159,7 @@ private static void AppendBuildInterceptorMethod(StringBuilder sb, BuilderCallSi
global::Qyl.Instrumentation.Generators.QylGeneratedRegistry.RegisterQylHostedServices(builder.Services);
global::Qyl.Instrumentation.Generators.QylGeneratedRegistry.RegisterQylServices(builder.Services);
global::Qyl.Instrumentation.Generators.QylGeneratedRegistry.RegisterQylHealthChecks(builder.Services);
var app = builder.Build();
var app = {{buildExpression}};
app.MapQylDefaultEndpoints();
return app;
}
Expand All @@ -147,10 +169,12 @@ private static void AppendBuildInterceptorMethod(StringBuilder sb, BuilderCallSi
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 QylRuntimeAvailable,
bool OtelAutoInstrumentationAvailable);
}
5 changes: 5 additions & 0 deletions services/qyl.collector/qyl.collector.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
<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>
<QylDatabase>false</QylDatabase>
</PropertyGroup>

Expand Down