diff --git a/.github/workflows/otlp-collector-fixtures.yml b/.github/workflows/otlp-collector-fixtures.yml
new file mode 100644
index 0000000..78b44b2
--- /dev/null
+++ b/.github/workflows/otlp-collector-fixtures.yml
@@ -0,0 +1,26 @@
+name: qyl-otlp-collector-fixtures
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ otlp-collector-fixtures:
+ name: otlp collector fixtures (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Verify collector-backed OTLP transport fixtures
+ run: python3 tools/verify-otlp-collector-fixtures.py
diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml
new file mode 100644
index 0000000..a9f2bf5
--- /dev/null
+++ b/.github/workflows/smoketest.yml
@@ -0,0 +1,26 @@
+name: qyl-smoketest
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ smoke:
+ name: smoke (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Run PackageReference and ProjectReference smoke tests
+ run: bash tools/smoketest.sh
diff --git a/.github/workflows/webapi-aot-demo.yml b/.github/workflows/webapi-aot-demo.yml
new file mode 100644
index 0000000..1bfce33
--- /dev/null
+++ b/.github/workflows/webapi-aot-demo.yml
@@ -0,0 +1,29 @@
+name: qyl-webapi-aot-demo
+
+on:
+ pull_request:
+ push:
+ branches:
+ - main
+ - claude/**
+
+jobs:
+ webapi-aot-demo:
+ name: webapi-aot-demo (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Publish and run NativeAOT web API demo
+ run: python3 tools/verify-webapi-aot-demo.py
diff --git a/Directory.Build.props b/Directory.Build.props
index 8022006..a8d6f11 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,7 +3,7 @@
- 0.2.0-pre.1
+ 0.3.0-pre.1
qyl
https://github.com/ANcpLua/qyl-dotnet-autoinstrumentation
git
@@ -51,6 +51,8 @@
0.6.1-beta.1
4.15.0
1.0.0-beta.6
+ 3.3.4
+ true
@@ -82,6 +84,11 @@
Version="$(JonSkeetRoslynAnalyzersVersion)"
PrivateAssets="all"
IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
+
diff --git a/README.md b/README.md
index c1c44cf..e8afa61 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ current compile-time lane classifies the contract as 33 source-generated signal
unsupported NativeAOT parity/dynamic signal promises, seven global controls, and 16 instrumentation
options. Nothing is silently dropped; unsupported parity requires an explicit reason.
-## Substrate-swap note (v0.2.0-pre.1)
+## Substrate-swap note (v0.3.0-pre.1)
Pre-v0.2.0 the runtime was the OpenTelemetry .NET auto-instrumentation native CLR profiler,
attached via `CORECLR_PROFILER` / `OTEL_DOTNET_AUTO_PLUGINS` / a `dotnet tool` (`qyl install`)
@@ -55,6 +55,21 @@ Runtime projects inherit `IsAotCompatible`, trim, AOT, and single-file analyzers
`PublishAot=true` excludes the generator project so NativeAOT publish never tries to publish a
build-time Roslyn assembly.
+Zero-code interception is a package build-asset contract. A normal consumer uses
+`PackageReference` to `Qyl.AutoInstrumentation`, which supplies the analyzer plus `build/` and
+`buildTransitive/` targets for `InterceptsLocationAttribute` and the interceptor namespace.
+Source-tree dogfooding through `ProjectReference` must make the same build-time pieces explicit:
+reference `Qyl.AutoInstrumentation.csproj` as runtime, reference
+`Qyl.AutoInstrumentation.SourceGenerators.csproj` with
+`OutputItemType="Analyzer" ReferenceOutputAssembly="false"`, and import
+`src/Qyl.AutoInstrumentation/buildTransitive/Qyl.AutoInstrumentation.targets`. For NativeAOT
+publish in that source-tree path, prebuild the generator and include its output DLL as an
+`Analyzer`; do not let `PublishAot=true` traverse the netstandard2.0 generator project. A bare
+runtime `ProjectReference` is not a supported zero-code path because MSBuild resolves it as a
+reference assembly, not as compiler analyzer/build assets.
+`tools/verify-projectreference-behavior.py` proves the supported ProjectReference dogfooding path
+under managed execution and NativeAOT.
+
Current evidence proves the qyl runtime closure is NativeAOT-clean and emits spans under
NativeAOT. Library-specific packages call out upstream app-side warning boundaries when the
instrumented library itself is not warning-clean. The active generator direction is compile-time
@@ -119,6 +134,11 @@ The agent emits runtime values only when the instrumented library supplied them
`DiagnosticSource` payloads or the current `Activity`. It does not invent fallback URLs, database
names, SQL statements, routes, methods, status codes, or RPC methods.
+The semconv conformance processor is a development gate, not a production hot-path cost. Its
+`qyl.semconv.attribute.checks` counter is default-off and runs only when
+`QYL_CONFORMANCE_ENABLED=1` is set or the host calls
+`AddQylAutoInstrumentation(o => o.EnableConformanceProcessor = true)`.
+
Semantic rules live in the diagnostic listener layer:
- Stable OpenTelemetry keys are emitted; deprecated aliases such as `http.url`, `http.status_code`,
diff --git a/benchmarks/Qyl.AutoInstrumentation.Benchmarks/Program.cs b/benchmarks/Qyl.AutoInstrumentation.Benchmarks/Program.cs
new file mode 100644
index 0000000..eabe5b0
--- /dev/null
+++ b/benchmarks/Qyl.AutoInstrumentation.Benchmarks/Program.cs
@@ -0,0 +1,210 @@
+using System.Data;
+using System.Data.Common;
+using System.Collections;
+using System.Net;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Running;
+using Qyl.AutoInstrumentation;
+
+if (args.Contains("--smoke", StringComparer.Ordinal))
+{
+ await BenchmarkSmoke.RunAsync();
+ return;
+}
+
+BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
+
+internal static class BenchmarkSmoke
+{
+ public static async Task RunAsync()
+ {
+ using var httpClient = new HttpClientHotPathBenchmarks();
+ await httpClient.InterceptedGetAsync();
+
+ var dbCommand = new DbCommandHotPathBenchmarks();
+ dbCommand.InterceptedSqlClientCommand();
+
+ var efCore = new EntityFrameworkCoreHotPathBenchmarks();
+ efCore.InterceptedExecuteSqlRaw();
+ }
+}
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net10_0, launchCount: 1, warmupCount: 3, iterationCount: 5)]
+[SimpleJob(RuntimeMoniker.NativeAot10_0, launchCount: 1, warmupCount: 3, iterationCount: 5)]
+public class HttpClientHotPathBenchmarks : IDisposable
+{
+ private readonly HttpClient httpClient = new(new StaticHttpMessageHandler())
+ {
+ BaseAddress = new Uri("https://example.invalid"),
+ };
+
+ [Benchmark(Baseline = true)]
+ public async Task DirectGetAsync()
+ {
+ using var response = await httpClient.GetAsync("/", HttpCompletionOption.ResponseHeadersRead);
+ return (int)response.StatusCode;
+ }
+
+ [Benchmark]
+ public async Task InterceptedGetAsync()
+ {
+ using var response = await QylInterceptedHttpClient.GetAsync(httpClient, "/", HttpCompletionOption.ResponseHeadersRead);
+ return (int)response.StatusCode;
+ }
+
+ public void Dispose() => httpClient.Dispose();
+}
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net10_0, launchCount: 1, warmupCount: 3, iterationCount: 5)]
+[SimpleJob(RuntimeMoniker.NativeAot10_0, launchCount: 1, warmupCount: 3, iterationCount: 5)]
+public class DbCommandHotPathBenchmarks
+{
+ private readonly BenchmarkDbCommand command = new()
+ {
+ CommandText = "SELECT 1",
+ CommandType = CommandType.Text,
+ };
+
+ [Benchmark(Baseline = true)]
+ public int DirectSqlClientCommand() => command.CommandText!.Length;
+
+ [Benchmark]
+ public int InterceptedSqlClientCommand()
+ {
+ using var activity = QylInterceptedDbCommand.StartActivity(
+ command,
+ QylAutoInstrumentationIds.SqlClient,
+ "ExecuteScalar");
+
+ return activity is null ? 0 : 1;
+ }
+}
+
+[MemoryDiagnoser]
+[SimpleJob(RuntimeMoniker.Net10_0, launchCount: 1, warmupCount: 3, iterationCount: 5)]
+[SimpleJob(RuntimeMoniker.NativeAot10_0, launchCount: 1, warmupCount: 3, iterationCount: 5)]
+public class EntityFrameworkCoreHotPathBenchmarks
+{
+ private const string Statement = "INSERT INTO qyl_benchmark(value) VALUES (1)";
+
+ [Benchmark(Baseline = true)]
+ public int DirectExecuteSqlRaw() => Statement.Length;
+
+ [Benchmark]
+ public int InterceptedExecuteSqlRaw()
+ {
+ using var activity = QylInterceptedEntityFrameworkCore.StartActivity(Statement);
+
+ return activity is null ? 0 : 1;
+ }
+}
+
+internal sealed class StaticHttpMessageHandler : HttpMessageHandler
+{
+ protected override Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NoContent)
+ {
+ RequestMessage = request,
+ });
+ }
+}
+
+internal sealed class BenchmarkDbCommand : DbCommand
+{
+#pragma warning disable CS8765
+ public override string CommandText { get; set; } = string.Empty;
+#pragma warning restore CS8765
+
+ public override int CommandTimeout { get; set; }
+
+ public override CommandType CommandType { get; set; }
+
+ public override bool DesignTimeVisible { get; set; }
+
+ public override UpdateRowSource UpdatedRowSource { get; set; }
+
+ protected override DbConnection? DbConnection { get; set; }
+
+ protected override DbParameterCollection DbParameterCollection => EmptyDbParameterCollection.Instance;
+
+ protected override DbTransaction? DbTransaction { get; set; }
+
+ public override void Cancel()
+ {
+ }
+
+ public override int ExecuteNonQuery() => 0;
+
+ public override object ExecuteScalar() => 1;
+
+ public override void Prepare()
+ {
+ }
+
+ protected override DbParameter CreateDbParameter() => throw new NotSupportedException();
+
+ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw new NotSupportedException();
+}
+
+internal sealed class EmptyDbParameterCollection : DbParameterCollection
+{
+ public static readonly EmptyDbParameterCollection Instance = new();
+
+ private EmptyDbParameterCollection()
+ {
+ }
+
+ public override int Count => 0;
+
+ public override object SyncRoot => this;
+
+ public override int Add(object value) => throw new NotSupportedException();
+
+ public override void AddRange(Array values) => throw new NotSupportedException();
+
+ public override void Clear()
+ {
+ }
+
+ public override bool Contains(object value) => false;
+
+ public override bool Contains(string value) => false;
+
+ public override void CopyTo(Array array, int index)
+ {
+ }
+
+ public override IEnumerator GetEnumerator() => Array.Empty().GetEnumerator();
+
+ public override int IndexOf(object value) => -1;
+
+ public override int IndexOf(string parameterName) => -1;
+
+ public override void Insert(int index, object value) => throw new NotSupportedException();
+
+ public override void Remove(object value)
+ {
+ }
+
+ public override void RemoveAt(int index)
+ {
+ }
+
+ public override void RemoveAt(string parameterName)
+ {
+ }
+
+ protected override DbParameter GetParameter(int index) => throw new ArgumentOutOfRangeException(nameof(index));
+
+ protected override DbParameter GetParameter(string parameterName) => throw new ArgumentOutOfRangeException(nameof(parameterName));
+
+ protected override void SetParameter(int index, DbParameter value) => throw new NotSupportedException();
+
+ protected override void SetParameter(string parameterName, DbParameter value) => throw new NotSupportedException();
+}
diff --git a/benchmarks/Qyl.AutoInstrumentation.Benchmarks/Qyl.AutoInstrumentation.Benchmarks.csproj b/benchmarks/Qyl.AutoInstrumentation.Benchmarks/Qyl.AutoInstrumentation.Benchmarks.csproj
new file mode 100644
index 0000000..77f5bd6
--- /dev/null
+++ b/benchmarks/Qyl.AutoInstrumentation.Benchmarks/Qyl.AutoInstrumentation.Benchmarks.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net10.0
+ false
+
+
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+ false
+
+
+
+
+
+
+
+
diff --git a/docs/coverage-matrix.md b/docs/coverage-matrix.md
new file mode 100644
index 0000000..8601bcf
--- /dev/null
+++ b/docs/coverage-matrix.md
@@ -0,0 +1,96 @@
+# AOT Interceptor Coverage Matrix
+
+This matrix is generated from `docs/otel-dotnet-auto-60-contract-items.yaml`,
+`src/Qyl.AutoInstrumentation.SourceGenerators/InstrumentationContract.cs`, and
+`src/Qyl.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs`.
+
+It is the review artifact for the 60-item auto-instrumentation contract: upstream
+contract item on the left, qyl NativeAOT interceptor/runtime status on the right.
+
+## Counts
+
+| Count | Value |
+|---|---:|
+| Total contract items | 60 |
+| Source-generated signal bindings | 33 |
+| Unsupported NativeAOT parity/dynamic signals | 4 |
+| Runtime environment controls | 7 |
+| Runtime instrumentation options | 16 |
+| Missing bindings | 0 |
+
+## Status legend
+
+| Status | Meaning |
+|---|---|
+| `source_generated_signal` | The source generator has a source-visible call-site binding for this signal. |
+| `unsupported_nativeaot_parity_or_dynamic_signal` | The upstream contract item is retained for parity, but it is not reachable as a NativeAOT source-interceptor signal. |
+| `runtime_environment_control` | The runtime options model binds the global/signal environment control. |
+| `runtime_instrumentation_option` | The runtime options model binds the instrumentation option. |
+| `missing_*` | Fails the gate. |
+
+## Matrix
+
+| # | Contract item | Kind | Key | qyl status | Evidence |
+|---:|---|---|---|---|---|
+| 1 | `contract.item.01` | `signal_specific_instrumentation_promise` | `signals.traces.ADONET` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitDbCommandInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.DbCommand |
+| 2 | `contract.item.02` | `signal_specific_instrumentation_promise` | `signals.traces.ASPNET` | `unsupported_nativeaot_parity_or_dynamic_signal` | InstrumentationContract.UnsupportedNativeAotSignalKeys |
+| 3 | `contract.item.03` | `signal_specific_instrumentation_promise` | `signals.traces.ASPNETCORE` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitAspNetCoreEndpointMapInterceptor
QylAutoInstrumentationGenerator.EmitAspNetCoreRequestDelegateInterceptor
QylAutoInstrumentationGenerator.EmitAspNetCoreWebApplicationBuilderBuildInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.AspNetCoreEndpointMap
QylAutoInstrumentationGenerator.InterceptorKind.AspNetCoreRequestDelegate
QylAutoInstrumentationGenerator.InterceptorKind.AspNetCoreWebApplicationBuilderBuild |
+| 4 | `contract.item.04` | `signal_specific_instrumentation_promise` | `signals.traces.AZURE` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitAzureClientInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.AzureClient |
+| 5 | `contract.item.05` | `signal_specific_instrumentation_promise` | `signals.traces.ELASTICSEARCH` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitElasticInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.ElasticsearchClient |
+| 6 | `contract.item.06` | `signal_specific_instrumentation_promise` | `signals.traces.ELASTICTRANSPORT` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitElasticInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.ElasticTransport |
+| 7 | `contract.item.07` | `signal_specific_instrumentation_promise` | `signals.traces.ENTITYFRAMEWORKCORE` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitEntityFrameworkCoreDbContextInterceptor
QylAutoInstrumentationGenerator.EmitEntityFrameworkCoreQueryableInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.EntityFrameworkCoreDbContext
QylAutoInstrumentationGenerator.InterceptorKind.EntityFrameworkCoreQueryable |
+| 8 | `contract.item.08` | `signal_specific_instrumentation_promise` | `signals.traces.GRAPHQL` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitGraphQlInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.GraphQlDocumentExecuter |
+| 9 | `contract.item.09` | `signal_specific_instrumentation_promise` | `signals.traces.GRPCNETCLIENT` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitGrpcNetClientAsyncUnaryInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.GrpcNetClientAsyncUnaryCall |
+| 10 | `contract.item.10` | `signal_specific_instrumentation_promise` | `signals.traces.HTTPCLIENT` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitHttpClientInterceptor
QylAutoInstrumentationGenerator.EmitHttpWebRequestInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.HttpClient
QylAutoInstrumentationGenerator.InterceptorKind.HttpWebRequest |
+| 11 | `contract.item.11` | `signal_specific_instrumentation_promise` | `signals.traces.KAFKA` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitKafkaConsumerInterceptor
QylAutoInstrumentationGenerator.EmitKafkaProducerInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.KafkaConsumer
QylAutoInstrumentationGenerator.InterceptorKind.KafkaProducer |
+| 12 | `contract.item.12` | `signal_specific_instrumentation_promise` | `signals.traces.MASSTRANSIT` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitMassTransitInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.MassTransitMessageOperation |
+| 13 | `contract.item.13` | `signal_specific_instrumentation_promise` | `signals.traces.MONGODB` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitMongoDbInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.MongoDbCollection |
+| 14 | `contract.item.14` | `signal_specific_instrumentation_promise` | `signals.traces.MYSQLCONNECTOR` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitDbCommandInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.DbCommand |
+| 15 | `contract.item.15` | `signal_specific_instrumentation_promise` | `signals.traces.MYSQLDATA` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitDbCommandInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.DbCommand |
+| 16 | `contract.item.16` | `signal_specific_instrumentation_promise` | `signals.traces.NPGSQL` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitDbCommandInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.DbCommand |
+| 17 | `contract.item.17` | `signal_specific_instrumentation_promise` | `signals.traces.NSERVICEBUS` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitNServiceBusInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.NServiceBusMessageOperation |
+| 18 | `contract.item.18` | `signal_specific_instrumentation_promise` | `signals.traces.ORACLEMDA` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitDbCommandInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.DbCommand |
+| 19 | `contract.item.19` | `signal_specific_instrumentation_promise` | `signals.traces.RABBITMQ` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitRabbitMqInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.RabbitMqBasicPublish |
+| 20 | `contract.item.20` | `signal_specific_instrumentation_promise` | `signals.traces.QUARTZ` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitQuartzInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.QuartzJobExecute |
+| 21 | `contract.item.21` | `signal_specific_instrumentation_promise` | `signals.traces.SQLCLIENT` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitDbCommandInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.DbCommand |
+| 22 | `contract.item.22` | `signal_specific_instrumentation_promise` | `signals.traces.SQLITE` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitDbCommandInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.DbCommand |
+| 23 | `contract.item.23` | `signal_specific_instrumentation_promise` | `signals.traces.STACKEXCHANGEREDIS` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitStackExchangeRedisInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.StackExchangeRedisCommandAsync |
+| 24 | `contract.item.24` | `signal_specific_instrumentation_promise` | `signals.traces.WCFCLIENT` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitWcfClientInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.WcfClient |
+| 25 | `contract.item.25` | `signal_specific_instrumentation_promise` | `signals.traces.WCFCORE` | `unsupported_nativeaot_parity_or_dynamic_signal` | InstrumentationContract.UnsupportedNativeAotSignalKeys |
+| 26 | `contract.item.26` | `signal_specific_instrumentation_promise` | `signals.traces.WCFSERVICE` | `unsupported_nativeaot_parity_or_dynamic_signal` | InstrumentationContract.UnsupportedNativeAotSignalKeys |
+| 27 | `contract.item.27` | `signal_specific_instrumentation_promise` | `signals.metrics.ASPNET` | `unsupported_nativeaot_parity_or_dynamic_signal` | InstrumentationContract.UnsupportedNativeAotSignalKeys |
+| 28 | `contract.item.28` | `signal_specific_instrumentation_promise` | `signals.metrics.ASPNETCORE` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitMeterProviderBuilderAddMeterInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.MeterProviderBuilderAddMeter |
+| 29 | `contract.item.29` | `signal_specific_instrumentation_promise` | `signals.metrics.HTTPCLIENT` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitHttpClientInterceptor
QylAutoInstrumentationGenerator.EmitHttpWebRequestInterceptor
QylAutoInstrumentationGenerator.EmitMeterProviderBuilderAddMeterInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.HttpClient
QylAutoInstrumentationGenerator.InterceptorKind.HttpWebRequest
QylAutoInstrumentationGenerator.InterceptorKind.MeterProviderBuilderAddMeter |
+| 30 | `contract.item.30` | `signal_specific_instrumentation_promise` | `signals.metrics.NETRUNTIME` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitMeterProviderBuilderAddMeterInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.MeterProviderBuilderAddMeter |
+| 31 | `contract.item.31` | `signal_specific_instrumentation_promise` | `signals.metrics.NPGSQL` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitDbCommandInterceptor
QylAutoInstrumentationGenerator.EmitMeterProviderBuilderAddMeterInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.DbCommand
QylAutoInstrumentationGenerator.InterceptorKind.MeterProviderBuilderAddMeter |
+| 32 | `contract.item.32` | `signal_specific_instrumentation_promise` | `signals.metrics.NSERVICEBUS` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitMeterProviderBuilderAddMeterInterceptor
QylAutoInstrumentationGenerator.EmitNServiceBusInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.MeterProviderBuilderAddMeter
QylAutoInstrumentationGenerator.InterceptorKind.NServiceBusMessageOperation |
+| 33 | `contract.item.33` | `signal_specific_instrumentation_promise` | `signals.metrics.PROCESS` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitMeterProviderBuilderAddMeterInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.MeterProviderBuilderAddMeter |
+| 34 | `contract.item.34` | `signal_specific_instrumentation_promise` | `signals.metrics.SQLCLIENT` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitDbCommandInterceptor
QylAutoInstrumentationGenerator.EmitMeterProviderBuilderAddMeterInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.DbCommand
QylAutoInstrumentationGenerator.InterceptorKind.MeterProviderBuilderAddMeter |
+| 35 | `contract.item.35` | `signal_specific_instrumentation_promise` | `signals.logs.ILOGGER` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitLoggerExtensionInterceptor
QylAutoInstrumentationGenerator.EmitLoggerInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.ILoggerExtensionLog
QylAutoInstrumentationGenerator.InterceptorKind.ILoggerLog |
+| 36 | `contract.item.36` | `signal_specific_instrumentation_promise` | `signals.logs.LOG4NET` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitExternalLoggerInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.Log4NetLogger |
+| 37 | `contract.item.37` | `signal_specific_instrumentation_promise` | `signals.logs.NLOG` | `source_generated_signal` | InstrumentationContract.TryGetSourceGeneratedSignal
QylAutoInstrumentationGenerator.EmitExternalLoggerInterceptor
QylAutoInstrumentationGenerator.InterceptorKind.NLogLogger |
+| 38 | `contract.item.38` | `global_environment_control` | `global_environment_controls.OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED` | `runtime_environment_control` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 39 | `contract.item.39` | `global_environment_control` | `global_environment_controls.OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED` | `runtime_environment_control` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 40 | `contract.item.40` | `global_environment_control` | `global_environment_controls.OTEL_DOTNET_AUTO_TRACES_{0}_INSTRUMENTATION_ENABLED` | `runtime_environment_control` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 41 | `contract.item.41` | `global_environment_control` | `global_environment_controls.OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED` | `runtime_environment_control` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 42 | `contract.item.42` | `global_environment_control` | `global_environment_controls.OTEL_DOTNET_AUTO_METRICS_{0}_INSTRUMENTATION_ENABLED` | `runtime_environment_control` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 43 | `contract.item.43` | `global_environment_control` | `global_environment_controls.OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED` | `runtime_environment_control` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 44 | `contract.item.44` | `global_environment_control` | `global_environment_controls.OTEL_DOTNET_AUTO_LOGS_{0}_INSTRUMENTATION_ENABLED` | `runtime_environment_control` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 45 | `contract.item.45` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_ENTITYFRAMEWORKCORE_SET_DBSTATEMENT_FOR_TEXT` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 46 | `contract.item.46` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_GRAPHQL_SET_DOCUMENT` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 47 | `contract.item.47` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_ORACLEMDA_SET_DBSTATEMENT_FOR_TEXT` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 48 | `contract.item.48` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 49 | `contract.item.49` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 50 | `contract.item.50` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 51 | `contract.item.51` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 52 | `contract.item.52` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 53 | `contract.item.53` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_REQUEST_METADATA` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 54 | `contract.item.54` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_RESPONSE_METADATA` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 55 | `contract.item.55` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 56 | `contract.item.56` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 57 | `contract.item.57` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 58 | `contract.item.58` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 59 | `contract.item.59` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_ASPNET_DISABLE_URL_QUERY_REDACTION` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+| 60 | `contract.item.60` | `instrumentation_option` | `instrumentation_options.OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED` | `runtime_instrumentation_option` | QylAutoInstrumentationOptions
tools/verify-environment-options-behavior.py |
+
+// validated 2026-06-05 by tools/verify-contract-coverage-report.py
diff --git a/docs/rfc/0001-interceptor-substrate.md b/docs/rfc/0001-interceptor-substrate.md
new file mode 100644
index 0000000..30f0223
--- /dev/null
+++ b/docs/rfc/0001-interceptor-substrate.md
@@ -0,0 +1,140 @@
+# RFC 0001: AOT-native source interceptor substrate
+
+Status: draft for review
+
+## Summary
+
+This proposal defines qyl's AOT-native auto-instrumentation substrate: a Roslyn
+incremental source generator discovers source-visible call sites, obtains Roslyn
+`InterceptableLocation` data through `SemanticModel.GetInterceptableLocation`, and emits
+C# methods annotated with `[InterceptsLocation]`. The emitted methods call public,
+AOT-safe qyl runtime helpers and are compiled into the consumer application by the normal
+.NET compiler and NativeAOT toolchain.
+
+The substrate is intentionally complementary to CLR-profiler auto-instrumentation. It is
+not a CLR profiler, not runtime IL rewriting, not a startup hook, not an
+`AssemblyLoadContext` plugin, and not reflection-based dynamic patching.
+
+## Problem
+
+Existing .NET auto-instrumentation normally relies on runtime attach, profiler callbacks,
+startup hooks, dynamic assembly loading, or IL rewriting. Those mechanisms are useful for
+JIT applications, but they do not provide a clean NativeAOT story:
+
+- The NativeAOT executable has no JIT-time rewrite point.
+- Runtime-loaded instrumentation assemblies fight trimming and static analysis.
+- Reflection-heavy discovery either warns under trim/AOT analyzers or requires roots that
+ defeat the goal of predictable publication.
+- Silent build-asset gaps can produce a consumer that compiles but has no interceptors.
+
+For qyl, AOT compatibility is the product axis. If a feature cannot survive
+`dotnet publish -p:PublishAot=true` with trim/AOT warnings treated as release-gate
+failures, it is not part of this substrate.
+
+## Proposed substrate
+
+The substrate has three explicit layers:
+
+1. Discovery: an incremental generator inspects source-visible invocations and matches
+ supported methods without relying on runtime reflection.
+2. Encoding: the generator calls Roslyn's `GetInterceptableLocation` API and writes the
+ returned version/data into `[InterceptsLocation]` attributes in generated C#.
+3. Runtime: each generated interceptor calls qyl runtime helper APIs that use public
+ library surfaces, `DiagnosticListener`, `Activity`, and bounded OpenTelemetry
+ attributes.
+
+The generated code must be ordinary C# that NativeAOT can compile. It must not require
+profiler registration, ReJIT, runtime IL rewrite, dynamic `Assembly.Load`, or
+`Activator.CreateInstance`.
+
+## Build asset contract
+
+The package must carry all compiler-facing substrate assets:
+
+- `analyzers/dotnet/cs/Qyl.AutoInstrumentation.SourceGenerators.dll`
+- `build/Qyl.AutoInstrumentation.targets`
+- `build/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs`
+- `buildTransitive/Qyl.AutoInstrumentation.targets`
+- `buildTransitive/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs`
+
+`build/` and `buildTransitive/` intentionally contain the same core target content. The
+target enables the interceptors namespace and adds the local
+`InterceptsLocationAttribute` source. A guard property prevents duplicate imports when a
+direct package and a transitive package both try to bring in the same core assets.
+
+`PackageReference` is the zero-config consumer path. A consumer should be able to add the
+package and have the analyzer plus targets participate in compilation automatically.
+
+`ProjectReference` is a dogfooding path, not a magic NuGet replacement. A bare runtime
+`ProjectReference` cannot force the referenced project's analyzer/build assets into the
+consumer by MSBuild design. The supported project-reference proof path wires the runtime
+project, generator analyzer, and core target explicitly so local development exercises the
+same generated-interceptor substrate instead of silently compiling without interceptors.
+
+## Runtime rules
+
+Runtime helper APIs must keep the hot path compatible with NativeAOT and telemetry scale:
+
+- No profiler, startup hook, ReJIT, runtime IL rewrite, `AssemblyLoadContext`, dynamic
+ `Assembly.Load`, or reflection-based instrumentation dispatch.
+- No unbounded span names. Span and activity names must not include full URLs, request
+ paths, query strings, IDs, exception messages, or caller-supplied arbitrary text.
+- Stable OpenTelemetry attributes are emitted by default; deprecated aliases can be
+ consumed as inputs but must not be re-emitted as canonical output.
+- Sensitive raw values such as `url.full`, `url.path`, and `db.query.text` are gated off by
+ default.
+- Metric instruments and activity sources are process-level owners, not per-request or
+ per-interceptor allocations.
+- Conformance/self-telemetry processors stay opt-in when they add per-span work.
+
+## Verification contract
+
+This repo gates the substrate through executable checks rather than documentation claims:
+
+- Package layout verification confirms analyzer/build/buildTransitive assets exist and
+ forbids profiler/startup-hook/IL-rewrite/runtime-load tokens in those package assets.
+- ProjectReference behavior verification proves the bare runtime `ProjectReference`
+ limitation and the explicit dogfooding path.
+- Source-interceptor consumer verification proves generated interceptors execute under
+ managed and NativeAOT consumers.
+- The smoke test packs the current tree, creates PackageReference and ProjectReference
+ scratch consumers, runs JIT and NativeAOT binaries, and checks deterministic output.
+- The AOT warning gate fails on IL2xxx, IL3xxx, IL4xxx, or CA warnings in NativeAOT
+ publish logs for the supported smoke consumers.
+- Public API baseline verification prevents accidental package surface drift.
+
+These gates are part of the substrate definition. If a future change bypasses them, it is
+not a substrate improvement; it is an unverified instrumentation path.
+
+## Contribution shape
+
+The upstream contribution is not "rewrite the CLR profiler in qyl." The contribution is a
+separate AOT-native substrate that can coexist with profiler-based instrumentation:
+
+- A shared vocabulary for source-visible interceptor descriptors.
+- Golden generated-code fixtures for `[InterceptsLocation]` output.
+- AOT smoke consumers for library integrations that are reachable from source-level call
+ sites.
+- A clear split between compile-time interception and runtime diagnostic listener
+ extraction.
+- Explicit "not reachable by source interception" gaps where profiler/runtime approaches
+ remain the right tool.
+
+This lets upstream and downstream projects reason about AOT auto-instrumentation without
+pretending that profiler mechanics survive NativeAOT unchanged.
+
+## Current limitations
+
+- Only source-visible call sites can be intercepted. Calls hidden behind reflection,
+ generated binaries, dynamic dispatch without source, or external compiled assemblies are
+ outside this substrate.
+- Libraries that emit useful framework `ActivitySource`, `Meter`, or `DiagnosticListener`
+ data may be better consumed directly than wrapped through an interceptor.
+- Some third-party libraries may publish under NativeAOT only with their own warnings or
+ app-side constraints. Those warnings belong to that library boundary and must be called
+ out instead of hidden inside qyl.
+- Collector-backed OTLP transport fixtures, runtime XML-doc enforcement, generator-output
+ snapshots, source-generator XML-doc enforcement, the NativeAOT web API proof, and canonical
+ OTLP-shaped fixtures are covered by committed gates. Hot-path measurements live in the
+ BenchmarkDotNet project under `benchmarks/`.
+- The release tag remains the last step after the PR evidence is reviewed.
diff --git a/src/Qyl.AutoInstrumentation.DiagnosticListeners/PublicAPI.Shipped.txt b/src/Qyl.AutoInstrumentation.DiagnosticListeners/PublicAPI.Shipped.txt
new file mode 100644
index 0000000..7af6ed4
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation.DiagnosticListeners/PublicAPI.Shipped.txt
@@ -0,0 +1,19 @@
+#nullable enable
+Qyl.AutoInstrumentation.DiagnosticListeners.AspNetCore.AspNetCoreDiagnosticListener
+Qyl.AutoInstrumentation.DiagnosticListeners.AspNetCore.AspNetCoreDiagnosticListener.AspNetCoreDiagnosticListener() -> void
+Qyl.AutoInstrumentation.DiagnosticListeners.DiagnosticListenerSubscriber
+Qyl.AutoInstrumentation.DiagnosticListeners.DiagnosticListenerSubscriber.DiagnosticListenerSubscriber() -> void
+Qyl.AutoInstrumentation.DiagnosticListeners.DiagnosticListenerSubscriber.Dispose() -> void
+Qyl.AutoInstrumentation.DiagnosticListeners.DiagnosticListenerSubscriber.Subscribe() -> void
+Qyl.AutoInstrumentation.DiagnosticListeners.EntityFrameworkCore.EntityFrameworkCoreDiagnosticListener
+Qyl.AutoInstrumentation.DiagnosticListeners.EntityFrameworkCore.EntityFrameworkCoreDiagnosticListener.EntityFrameworkCoreDiagnosticListener() -> void
+Qyl.AutoInstrumentation.DiagnosticListeners.GrpcClient.GrpcClientDiagnosticListener
+Qyl.AutoInstrumentation.DiagnosticListeners.GrpcClient.GrpcClientDiagnosticListener.GrpcClientDiagnosticListener() -> void
+Qyl.AutoInstrumentation.DiagnosticListeners.HttpClient.HttpClientDiagnosticListener
+Qyl.AutoInstrumentation.DiagnosticListeners.HttpClient.HttpClientDiagnosticListener.HttpClientDiagnosticListener() -> void
+Qyl.AutoInstrumentation.DiagnosticListeners.SqlClient.SqlClientDiagnosticListener
+Qyl.AutoInstrumentation.DiagnosticListeners.SqlClient.SqlClientDiagnosticListener.SqlClientDiagnosticListener() -> void
+abstract Qyl.AutoInstrumentation.DiagnosticListeners.DiagnosticListenerSubscriber.InstrumentationId.get -> string!
+abstract Qyl.AutoInstrumentation.DiagnosticListeners.DiagnosticListenerSubscriber.ListenerName.get -> string!
+abstract Qyl.AutoInstrumentation.DiagnosticListeners.DiagnosticListenerSubscriber.OnEvent(string! name, object? payload) -> void
+abstract Qyl.AutoInstrumentation.DiagnosticListeners.DiagnosticListenerSubscriber.Signal.get -> Qyl.AutoInstrumentation.QylAutoInstrumentationSignal
diff --git a/src/Qyl.AutoInstrumentation.DiagnosticListeners/PublicAPI.Unshipped.txt b/src/Qyl.AutoInstrumentation.DiagnosticListeners/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000..7dc5c58
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation.DiagnosticListeners/PublicAPI.Unshipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Qyl.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Shipped.txt b/src/Qyl.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Shipped.txt
new file mode 100644
index 0000000..d31e406
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Shipped.txt
@@ -0,0 +1,5 @@
+#nullable enable
+Qyl.AutoInstrumentation.EntityFrameworkCore.EntityFrameworkCoreAutoInstrumentationBootstrap
+Qyl.AutoInstrumentation.EntityFrameworkCore.EntityFrameworkCoreDiagnosticListener
+Qyl.AutoInstrumentation.EntityFrameworkCore.EntityFrameworkCoreDiagnosticListener.EntityFrameworkCoreDiagnosticListener() -> void
+static Qyl.AutoInstrumentation.EntityFrameworkCore.EntityFrameworkCoreAutoInstrumentationBootstrap.Boot() -> void
diff --git a/src/Qyl.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Unshipped.txt b/src/Qyl.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000..7dc5c58
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Unshipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Qyl.AutoInstrumentation.Hosting/PublicAPI.Shipped.txt b/src/Qyl.AutoInstrumentation.Hosting/PublicAPI.Shipped.txt
new file mode 100644
index 0000000..6f8c923
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation.Hosting/PublicAPI.Shipped.txt
@@ -0,0 +1,10 @@
+#nullable enable
+Qyl.AutoInstrumentation.Hosting.QylAutoInstrumentationBootstrap
+Qyl.AutoInstrumentation.Hosting.QylAutoInstrumentationHostingOptions
+Qyl.AutoInstrumentation.Hosting.QylAutoInstrumentationHostingOptions.EnableConformanceProcessor.get -> bool
+Qyl.AutoInstrumentation.Hosting.QylAutoInstrumentationHostingOptions.EnableConformanceProcessor.set -> void
+Qyl.AutoInstrumentation.Hosting.QylAutoInstrumentationHostingOptions.QylAutoInstrumentationHostingOptions() -> void
+Qyl.AutoInstrumentation.Hosting.QylAutoInstrumentationServiceCollectionExtensions
+static Qyl.AutoInstrumentation.Hosting.QylAutoInstrumentationBootstrap.Boot() -> void
+static Qyl.AutoInstrumentation.Hosting.QylAutoInstrumentationServiceCollectionExtensions.AddQylAutoInstrumentation(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
+static Qyl.AutoInstrumentation.Hosting.QylAutoInstrumentationServiceCollectionExtensions.AddQylAutoInstrumentation(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
diff --git a/src/Qyl.AutoInstrumentation.Hosting/PublicAPI.Unshipped.txt b/src/Qyl.AutoInstrumentation.Hosting/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000..7dc5c58
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation.Hosting/PublicAPI.Unshipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Qyl.AutoInstrumentation.Hosting/QylAutoInstrumentationServiceCollectionExtensions.cs b/src/Qyl.AutoInstrumentation.Hosting/QylAutoInstrumentationServiceCollectionExtensions.cs
index 55859b3..f1d6021 100644
--- a/src/Qyl.AutoInstrumentation.Hosting/QylAutoInstrumentationServiceCollectionExtensions.cs
+++ b/src/Qyl.AutoInstrumentation.Hosting/QylAutoInstrumentationServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
+using Qyl.AutoInstrumentation.Internal;
namespace Qyl.AutoInstrumentation.Hosting;
@@ -21,4 +22,27 @@ public static IServiceCollection AddQylAutoInstrumentation(this IServiceCollecti
QylAutoInstrumentationBootstrap.Boot();
return services;
}
+
+ /// Idempotently activate qyl auto-instrumentation with explicit hosting options.
+ public static IServiceCollection AddQylAutoInstrumentation(
+ this IServiceCollection services,
+ Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(configure);
+
+ var options = new QylAutoInstrumentationHostingOptions();
+ configure(options);
+ if (options.EnableConformanceProcessor)
+ SemConvConformanceProcessor.Enable();
+
+ QylAutoInstrumentationBootstrap.Boot();
+ return services;
+ }
+}
+
+/// Options for explicit qyl hosting activation.
+public sealed class QylAutoInstrumentationHostingOptions
+{
+ /// Enable the development-only semconv conformance counter.
+ public bool EnableConformanceProcessor { get; set; }
}
diff --git a/src/Qyl.AutoInstrumentation.SourceGenerators/Qyl.AutoInstrumentation.SourceGenerators.csproj b/src/Qyl.AutoInstrumentation.SourceGenerators/Qyl.AutoInstrumentation.SourceGenerators.csproj
index 242d082..d6cf904 100644
--- a/src/Qyl.AutoInstrumentation.SourceGenerators/Qyl.AutoInstrumentation.SourceGenerators.csproj
+++ b/src/Qyl.AutoInstrumentation.SourceGenerators/Qyl.AutoInstrumentation.SourceGenerators.csproj
@@ -9,7 +9,10 @@
true
false
+ false
true
+ true
+ $(WarningsAsErrors);CS1591
diff --git a/src/Qyl.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs b/src/Qyl.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs
index d2de0da..a425fed 100644
--- a/src/Qyl.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs
+++ b/src/Qyl.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs
@@ -7,6 +7,15 @@
namespace Qyl.AutoInstrumentation.SourceGenerators;
+///
+/// Emits the qyl source-level auto-instrumentation interceptors used by NativeAOT consumers.
+///
+///
+/// The generator runs in the compiler, discovers source-visible invocation expressions, obtains
+/// Roslyn InterceptableLocation data, and emits ordinary C# interceptor methods. Runtime
+/// instrumentation stays in public qyl helper APIs; the generator never emits profiler, startup
+/// hook, reflection, or runtime IL-rewrite code.
+///
[Generator(LanguageNames.CSharp)]
public sealed class QylAutoInstrumentationGenerator : IIncrementalGenerator
{
@@ -15,6 +24,10 @@ public sealed class QylAutoInstrumentationGenerator : IIncrementalGenerator
SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions &
~SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);
+ ///
+ /// Registers the incremental syntax pipeline and post-initialization contract manifest output.
+ ///
+ /// Roslyn initialization context supplied by the compiler host.
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(static output =>
diff --git a/src/Qyl.AutoInstrumentation.SqlClient/PublicAPI.Shipped.txt b/src/Qyl.AutoInstrumentation.SqlClient/PublicAPI.Shipped.txt
new file mode 100644
index 0000000..8af6361
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation.SqlClient/PublicAPI.Shipped.txt
@@ -0,0 +1,5 @@
+#nullable enable
+Qyl.AutoInstrumentation.SqlClient.SqlClientAutoInstrumentationBootstrap
+Qyl.AutoInstrumentation.SqlClient.SqlClientDiagnosticListener
+Qyl.AutoInstrumentation.SqlClient.SqlClientDiagnosticListener.SqlClientDiagnosticListener() -> void
+static Qyl.AutoInstrumentation.SqlClient.SqlClientAutoInstrumentationBootstrap.Boot() -> void
diff --git a/src/Qyl.AutoInstrumentation.SqlClient/PublicAPI.Unshipped.txt b/src/Qyl.AutoInstrumentation.SqlClient/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000..7dc5c58
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation.SqlClient/PublicAPI.Unshipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Qyl.AutoInstrumentation/Internal/QylCapturedNameMap.cs b/src/Qyl.AutoInstrumentation/Internal/QylCapturedNameMap.cs
new file mode 100644
index 0000000..e19f513
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation/Internal/QylCapturedNameMap.cs
@@ -0,0 +1,71 @@
+using System.Collections.Frozen;
+using System.Globalization;
+
+namespace Qyl.AutoInstrumentation.Internal;
+
+internal sealed class QylCapturedNameMap
+{
+ internal static readonly QylCapturedNameMap Empty = new([], [], FrozenDictionary.Empty);
+
+ private readonly string[] _lookupNames;
+ private readonly string[] _tagNames;
+ private readonly FrozenDictionary _tagNameByLookupName;
+
+ private QylCapturedNameMap(
+ string[] lookupNames,
+ string[] tagNames,
+ FrozenDictionary tagNameByLookupName)
+ {
+ _lookupNames = lookupNames;
+ _tagNames = tagNames;
+ _tagNameByLookupName = tagNameByLookupName;
+ }
+
+ internal int Count => _lookupNames.Length;
+
+ internal string GetLookupName(int index) => _lookupNames[index];
+
+ internal string GetTagName(int index) => _tagNames[index];
+
+ internal bool TryGetTagName(string lookupName, out string tagName)
+ => _tagNameByLookupName.TryGetValue(lookupName, out tagName!);
+
+ internal static QylCapturedNameMap Create(string prefix, string[] configuredNames, bool normalizeLookupName = false)
+ {
+ if (configuredNames.Length is 0)
+ return Empty;
+
+ var entries = new Dictionary(configuredNames.Length, StringComparer.OrdinalIgnoreCase);
+ foreach (var configuredName in configuredNames)
+ {
+ var trimmedName = configuredName.Trim();
+ if (trimmedName.Length is 0)
+ continue;
+
+ var normalizedName = NormalizeName(trimmedName);
+ var lookupName = normalizeLookupName ? normalizedName : trimmedName;
+ entries[lookupName] = prefix + normalizedName;
+ }
+
+ if (entries.Count is 0)
+ return Empty;
+
+ var lookupNames = new string[entries.Count];
+ var tagNames = new string[entries.Count];
+ var index = 0;
+ foreach (var entry in entries)
+ {
+ lookupNames[index] = entry.Key;
+ tagNames[index] = entry.Value;
+ index++;
+ }
+
+ return new QylCapturedNameMap(
+ lookupNames,
+ tagNames,
+ entries.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase));
+ }
+
+ private static string NormalizeName(string name)
+ => name.Replace('_', '-').ToLower(CultureInfo.InvariantCulture);
+}
diff --git a/src/Qyl.AutoInstrumentation/Internal/QylSemConvRegistry.cs b/src/Qyl.AutoInstrumentation/Internal/QylSemConvRegistry.cs
index 54ff158..6b6d800 100644
--- a/src/Qyl.AutoInstrumentation/Internal/QylSemConvRegistry.cs
+++ b/src/Qyl.AutoInstrumentation/Internal/QylSemConvRegistry.cs
@@ -9,8 +9,8 @@ namespace Qyl.AutoInstrumentation.Internal;
/// The substrate-era code built this with Assembly.Load + Type.GetFields reflection
/// over the Qyl.OpenTelemetry.SemanticConventions packages at process startup. That path is
/// NOT AOT-safe (the trim/AOT analyzers reject Assembly.GetTypes()), so the build-time
-/// source generator now emits a FrozenSet<string> partial via
-/// . The fallback below keeps the file
+/// source generator now emits a FrozenSet<string> partial from the
+/// Qyl.AutoInstrumentation.SourceGenerators assembly. The fallback below keeps the file
/// compile-clean before the generator runs in a fresh checkout.
///
///
diff --git a/src/Qyl.AutoInstrumentation/Internal/SemConvConformanceProcessor.cs b/src/Qyl.AutoInstrumentation/Internal/SemConvConformanceProcessor.cs
index 4a3f929..deceacb 100644
--- a/src/Qyl.AutoInstrumentation/Internal/SemConvConformanceProcessor.cs
+++ b/src/Qyl.AutoInstrumentation/Internal/SemConvConformanceProcessor.cs
@@ -18,12 +18,20 @@ namespace Qyl.AutoInstrumentation.Internal;
///
internal static class SemConvConformanceProcessor
{
+ private static int _explicitlyEnabled;
+
+ internal static void Enable()
+ => Interlocked.Exchange(ref _explicitlyEnabled, 1);
+
///
/// Inspect a stopped and emit one qyl.semconv.attribute.checks
/// observation per attribute key.
///
public static void OnActivityStopped(Activity activity)
{
+ if (!IsEnabled())
+ return;
+
try
{
foreach (var tag in activity.TagObjects)
@@ -40,4 +48,8 @@ public static void OnActivityStopped(Activity activity)
new KeyValuePair(QylSemanticAttributes.ExceptionType, exception.GetType().Name));
}
}
+
+ private static bool IsEnabled()
+ => Volatile.Read(ref _explicitlyEnabled) is 1 ||
+ QylAutoInstrumentationOptions.Current.ConformanceProcessorEnabled;
}
diff --git a/src/Qyl.AutoInstrumentation/PublicAPI.Shipped.txt b/src/Qyl.AutoInstrumentation/PublicAPI.Shipped.txt
new file mode 100644
index 0000000..2b21729
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation/PublicAPI.Shipped.txt
@@ -0,0 +1,355 @@
+#nullable enable
+Qyl.AutoInstrumentation.QylActivityNames
+Qyl.AutoInstrumentation.QylActivitySource
+Qyl.AutoInstrumentation.QylAutoInstrumentationIds
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.AspNetCapturedRequestHeaders.get -> string![]!
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.AspNetCapturedResponseHeaders.get -> string![]!
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.AspNetCoreCapturedRequestHeaders.get -> string![]!
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.AspNetCoreCapturedResponseHeaders.get -> string![]!
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.AspNetCoreUrlQueryRedactionDisabled.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.AspNetUrlQueryRedactionDisabled.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.CaptureSensitiveValues.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.ConformanceProcessorEnabled.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.EntityFrameworkCoreSetDbStatementForText.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.GlobalEnabled.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.GraphQlSetDocument.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.GrpcNetClientCapturedRequestMetadata.get -> string![]!
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.GrpcNetClientCapturedResponseMetadata.get -> string![]!
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.HasAnyActivityInstrumentationEnabled() -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.HttpClientCapturedRequestHeaders.get -> string![]!
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.HttpClientCapturedResponseHeaders.get -> string![]!
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.HttpClientUrlQueryRedactionDisabled.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.IsInstrumentationEnabled(Qyl.AutoInstrumentation.QylAutoInstrumentationSignal signal, string! instrumentationId) -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.LogsEnabled.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.MetricsEnabled.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.OracleMdaSetDbStatementForText.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.SqlClientNetFxIlRewriteEnabled.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.SqlClientNetFxIlRewriteRequested.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.SqlClientSetDbStatementForText.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.TracesEnabled.get -> bool
+Qyl.AutoInstrumentation.QylAutoInstrumentationSignal
+Qyl.AutoInstrumentation.QylAutoInstrumentationSignal.Logs = 2 -> Qyl.AutoInstrumentation.QylAutoInstrumentationSignal
+Qyl.AutoInstrumentation.QylAutoInstrumentationSignal.Metrics = 1 -> Qyl.AutoInstrumentation.QylAutoInstrumentationSignal
+Qyl.AutoInstrumentation.QylAutoInstrumentationSignal.Traces = 0 -> Qyl.AutoInstrumentation.QylAutoInstrumentationSignal
+Qyl.AutoInstrumentation.QylDbClientMetrics
+Qyl.AutoInstrumentation.QylInstrumentation
+Qyl.AutoInstrumentation.QylInstrumentationDomains
+Qyl.AutoInstrumentation.QylInterceptedAspNetCore
+Qyl.AutoInstrumentation.QylInterceptedAzure
+Qyl.AutoInstrumentation.QylInterceptedDbCommand
+Qyl.AutoInstrumentation.QylInterceptedElastic
+Qyl.AutoInstrumentation.QylInterceptedEntityFrameworkCore
+Qyl.AutoInstrumentation.QylInterceptedExternalLogger
+Qyl.AutoInstrumentation.QylInterceptedGraphQl
+Qyl.AutoInstrumentation.QylInterceptedGrpcNetClient
+Qyl.AutoInstrumentation.QylInterceptedHttpClient
+Qyl.AutoInstrumentation.QylInterceptedHttpWebRequest
+Qyl.AutoInstrumentation.QylInterceptedKafka
+Qyl.AutoInstrumentation.QylInterceptedLogger
+Qyl.AutoInstrumentation.QylInterceptedMassTransit
+Qyl.AutoInstrumentation.QylInterceptedMongoDb
+Qyl.AutoInstrumentation.QylInterceptedNServiceBus
+Qyl.AutoInstrumentation.QylInterceptedQuartz
+Qyl.AutoInstrumentation.QylInterceptedRabbitMq
+Qyl.AutoInstrumentation.QylInterceptedRedis
+Qyl.AutoInstrumentation.QylInterceptedWcfClient
+Qyl.AutoInstrumentation.QylInterceptedWcfCore
+Qyl.AutoInstrumentation.QylMetricMeters
+Qyl.AutoInstrumentation.QylMetricNames
+Qyl.AutoInstrumentation.QylNServiceBusMetrics
+Qyl.AutoInstrumentation.QylSelfTelemetry
+Qyl.AutoInstrumentation.QylSemanticAttributes
+const Qyl.AutoInstrumentation.QylActivityNames.DbClientCommand = "DB client command" -> string!
+const Qyl.AutoInstrumentation.QylActivityNames.EntityFrameworkCoreOperation = "EF Core operation" -> string!
+const Qyl.AutoInstrumentation.QylActivityNames.HttpClientRequest = "HTTP client request" -> string!
+const Qyl.AutoInstrumentation.QylActivityNames.HttpServerRequest = "HTTP server request" -> string!
+const Qyl.AutoInstrumentation.QylActivitySource.Name = "Qyl.AutoInstrumentation" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.AdoNet = "ADONET" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.AspNet = "ASPNET" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.AspNetCore = "ASPNETCORE" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.Azure = "AZURE" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.ElasticTransport = "ELASTICTRANSPORT" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.Elasticsearch = "ELASTICSEARCH" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.EntityFrameworkCore = "ENTITYFRAMEWORKCORE" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.GraphQl = "GRAPHQL" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.GrpcNetClient = "GRPCNETCLIENT" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.HttpClient = "HTTPCLIENT" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.ILogger = "ILOGGER" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.Kafka = "KAFKA" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.Log4Net = "LOG4NET" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.MassTransit = "MASSTRANSIT" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.MongoDb = "MONGODB" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.MySqlConnector = "MYSQLCONNECTOR" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.MySqlData = "MYSQLDATA" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.NLog = "NLOG" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.NServiceBus = "NSERVICEBUS" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.NetRuntime = "NETRUNTIME" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.Npgsql = "NPGSQL" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.OracleMda = "ORACLEMDA" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.Process = "PROCESS" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.Quartz = "QUARTZ" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.RabbitMq = "RABBITMQ" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.SqlClient = "SQLCLIENT" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.Sqlite = "SQLITE" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.StackExchangeRedis = "STACKEXCHANGEREDIS" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.WcfClient = "WCFCLIENT" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.WcfCore = "WCFCORE" -> string!
+const Qyl.AutoInstrumentation.QylAutoInstrumentationIds.WcfService = "WCFSERVICE" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentation.Version = "0.3.0-pre.1" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.AspNetCoreServer = "aspnetcore.server" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.AzureSdk = "azure.sdk" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.DbClient = "db.client" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.DbEfCore = "db.efcore" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.DbElasticsearch = "db.elasticsearch" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.DbMongoDb = "db.mongodb" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.DbRedis = "db.redis" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.DbSqlClient = "db.sqlclient" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.ElasticTransport = "elastic.transport" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.GraphQl = "graphql" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.HttpClient = "http.client" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.HttpServer = "http.server" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.HttpWebRequest = "http.webrequest" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.JobQuartz = "job.quartz" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.LogILogger = "log.ilogger" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.LogLog4Net = "log.log4net" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.LogNLog = "log.nlog" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.MessagingKafka = "messaging.kafka" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.MessagingMassTransit = "messaging.masstransit" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.MessagingNServiceBus = "messaging.nservicebus" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.MessagingRabbitMq = "messaging.rabbitmq" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.RpcGrpc = "rpc.grpc" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.RpcWcfClient = "rpc.wcf.client" -> string!
+const Qyl.AutoInstrumentation.QylInstrumentationDomains.RpcWcfCore = "rpc.wcf.core" -> string!
+const Qyl.AutoInstrumentation.QylMetricMeters.AspNetCoreComponentsMeterName = "Microsoft.AspNetCore.Components" -> string!
+const Qyl.AutoInstrumentation.QylMetricMeters.DatabaseMeterName = "Qyl.AutoInstrumentation.Database" -> string!
+const Qyl.AutoInstrumentation.QylMetricMeters.HttpClientMeterName = "System.Net.Http" -> string!
+const Qyl.AutoInstrumentation.QylMetricMeters.NServiceBusMeterName = "NServiceBus.Core" -> string!
+const Qyl.AutoInstrumentation.QylMetricMeters.NetRuntimeMeterName = "OpenTelemetry.Instrumentation.Runtime" -> string!
+const Qyl.AutoInstrumentation.QylMetricMeters.NpgsqlMeterName = "Npgsql" -> string!
+const Qyl.AutoInstrumentation.QylMetricMeters.ProcessMeterName = "OpenTelemetry.Instrumentation.Process" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.AspNetCoreComponentsNavigation = "aspnetcore.components.navigation" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.DbClientOperationDuration = "db.client.operation.duration" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.HttpClientRequestDuration = "http.client.request.duration" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.NServiceBusMessagingOperationDuration = "nservicebus.messaging.operation.duration" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.ProcessCpuTime = "process.cpu.time" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.ProcessMemoryUsage = "process.memory.usage" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.ProcessMemoryVirtual = "process.memory.virtual" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.ProcessRuntimeDotnetGcCollectionsCount = "process.runtime.dotnet.gc.collections.count" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.ProcessRuntimeDotnetGcHeapSize = "process.runtime.dotnet.gc.heap.size" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.ProcessRuntimeDotnetGcObjectsSize = "process.runtime.dotnet.gc.objects.size" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.ProcessRuntimeDotnetThreadPoolQueueLength = "process.runtime.dotnet.thread_pool.queue.length" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.ProcessRuntimeDotnetThreadPoolThreadsCount = "process.runtime.dotnet.thread_pool.threads.count" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.QylSemConvAttributeChecks = "qyl.semconv.attribute.checks" -> string!
+const Qyl.AutoInstrumentation.QylMetricNames.QylSemConvProcessorFailures = "qyl.semconv.processor.failures" -> string!
+const Qyl.AutoInstrumentation.QylSelfTelemetry.MeterName = "Qyl.AutoInstrumentation" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.CpuMode = "cpu.mode" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.CpuModeSystem = "system" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.CpuModeUser = "user" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbNamespace = "db.namespace" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbOperationName = "db.operation.name" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbOperationNameGet = "GET" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbQuerySummary = "db.query.summary" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbQueryText = "db.query.text" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemElasticsearch = "elasticsearch" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemMicrosoftSqlServer = "microsoft.sql_server" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemMongodb = "mongodb" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemMysql = "mysql" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemName = "db.system.name" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemOracleDb = "oracle.db" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemOtherSql = "other_sql" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemPostgresql = "postgresql" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemRedis = "redis" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DbSystemSqlite = "sqlite" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DotnetGcHeapGeneration = "dotnet.gc.heap.generation" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DotnetGcHeapGenerationGen0 = "gen0" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DotnetGcHeapGenerationGen1 = "gen1" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.DotnetGcHeapGenerationGen2 = "gen2" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.ErrorType = "error.type" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.ExceptionType = "exception.type" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.GraphQlDocument = "graphql.document" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.GraphQlOperationName = "graphql.operation.name" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.GrpcRequestMetadataPrefix = "rpc.request.metadata." -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.GrpcResponseMetadataPrefix = "rpc.response.metadata." -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestHeaderPrefix = "http.request.header." -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethod = "http.request.method" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodConnect = "CONNECT" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodDelete = "DELETE" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodGet = "GET" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodHead = "HEAD" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodOptions = "OPTIONS" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodOriginal = "http.request.method_original" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodOther = "_OTHER" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodPatch = "PATCH" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodPost = "POST" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodPut = "PUT" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRequestMethodTrace = "TRACE" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpResponseHeaderPrefix = "http.response.header." -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpResponseStatusCode = "http.response.status_code" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.HttpRoute = "http.route" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogEventName = "otel.event.name" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogSeverity = "log.severity" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogSeverityCritical = "Critical" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogSeverityDebug = "Debug" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogSeverityError = "Error" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogSeverityInformation = "Information" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogSeverityNone = "None" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogSeverityOther = "Other" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogSeverityTrace = "Trace" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.LogSeverityWarning = "Warning" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingDestinationName = "messaging.destination.name" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingOperationName = "messaging.operation.name" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingOperationNamePublish = "publish" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingOperationNameSend = "send" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingOperationType = "messaging.operation.type" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingOperationTypeReceive = "receive" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingOperationTypeSend = "send" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingSystem = "messaging.system" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingSystemKafka = "kafka" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingSystemMassTransit = "masstransit" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingSystemNServiceBus = "nservicebus" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.MessagingSystemRabbitMq = "rabbitmq" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.QylConformanceVerdict = "qyl.conformance.verdict" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.QylInstrumentationDomain = "qyl.instrumentation.domain" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.RpcGrpcStatusCode = "rpc.grpc.status_code" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.RpcMethod = "rpc.method" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.RpcMethodExecute = "Execute" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.RpcService = "rpc.service" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.RpcSystem = "rpc.system.name" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.RpcSystemAzure = "azure" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.RpcSystemDotNetWcf = "dotnet_wcf" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.RpcSystemGrpc = "grpc" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.RpcSystemQuartz = "quartz" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.ServerAddress = "server.address" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.ServerPort = "server.port" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.UrlFull = "url.full" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.UrlPath = "url.path" -> string!
+const Qyl.AutoInstrumentation.QylSemanticAttributes.UrlQuery = "url.query" -> string!
+static Qyl.AutoInstrumentation.QylAutoInstrumentationOptions.Current.get -> Qyl.AutoInstrumentation.QylAutoInstrumentationOptions!
+static Qyl.AutoInstrumentation.QylDbClientMetrics.GetTimestamp() -> long
+static Qyl.AutoInstrumentation.QylDbClientMetrics.RecordDuration(long startTimestamp, string! instrumentationId) -> void
+static Qyl.AutoInstrumentation.QylInstrumentation.Activate() -> bool
+static Qyl.AutoInstrumentation.QylInterceptedAspNetCore.Build(Microsoft.AspNetCore.Builder.WebApplicationBuilder! builder) -> Microsoft.AspNetCore.Builder.WebApplication!
+static Qyl.AutoInstrumentation.QylInterceptedAspNetCore.InvokeAsync(Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate, Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedAspNetCore.MapDelete(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
+static Qyl.AutoInstrumentation.QylInterceptedAspNetCore.MapGet(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
+static Qyl.AutoInstrumentation.QylInterceptedAspNetCore.MapMethods(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, System.Collections.Generic.IEnumerable! httpMethods, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
+static Qyl.AutoInstrumentation.QylInterceptedAspNetCore.MapPatch(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
+static Qyl.AutoInstrumentation.QylInterceptedAspNetCore.MapPost(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
+static Qyl.AutoInstrumentation.QylInterceptedAspNetCore.MapPut(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
+static Qyl.AutoInstrumentation.QylInterceptedAzure.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedAzure.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedAzure.StartActivity(string! methodName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedDbCommand.ObserveAsync(System.Threading.Tasks.Task! task, System.Diagnostics.Activity? activity, long metricStart, string! instrumentationId) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedDbCommand.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedDbCommand.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedDbCommand.StartActivity(System.Data.Common.DbCommand! command, string! instrumentationId, string! operationName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedElastic.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedElastic.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedElastic.StartActivity(string! instrumentationId, string! methodName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedEntityFrameworkCore.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedEntityFrameworkCore.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedEntityFrameworkCore.StartActivity(string! operationName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedExternalLogger.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedExternalLogger.StartActivity(string! instrumentationId, string! domain, string! methodName, string? severityName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedGraphQl.ObserveAsync(System.Threading.Tasks.Task? task, System.Diagnostics.Activity? activity) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedGraphQl.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedGraphQl.RecordExecutionOptions(System.Diagnostics.Activity? activity, string? operationName, string? document) -> void
+static Qyl.AutoInstrumentation.QylInterceptedGraphQl.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedGraphQl.StartActivity() -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedGrpcNetClient.CaptureCompletedResponseHeaders(System.Threading.Tasks.Task? responseHeadersTask, System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedGrpcNetClient.Dispose(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedGrpcNetClient.ObserveResponseHeadersAsync(System.Threading.Tasks.Task! responseHeadersTask, System.Diagnostics.Activity? activity) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedGrpcNetClient.ObserveUnaryResponseAsync(System.Threading.Tasks.Task! responseTask, System.Threading.Tasks.Task! responseHeadersTask, System.Diagnostics.Activity? activity) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedGrpcNetClient.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedGrpcNetClient.RecordStreamingComplete(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedGrpcNetClient.StartActivity(string! clientTypeName, string! methodName, Grpc.Core.Metadata? requestMetadata) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.DeleteAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.DeleteAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.DeleteAsync(System.Net.Http.HttpClient! client, string? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.DeleteAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Net.Http.HttpCompletionOption completionOption) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetAsync(System.Net.Http.HttpClient! client, string? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Net.Http.HttpCompletionOption completionOption) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetByteArrayAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetByteArrayAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetByteArrayAsync(System.Net.Http.HttpClient! client, string? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetByteArrayAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetStreamAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetStreamAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetStreamAsync(System.Net.Http.HttpClient! client, string? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetStreamAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetStringAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetStringAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetStringAsync(System.Net.Http.HttpClient! client, string? requestUri) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.GetStringAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PatchAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Net.Http.HttpContent? content) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PatchAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Net.Http.HttpContent? content, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PatchAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Net.Http.HttpContent? content) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PatchAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Net.Http.HttpContent? content, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PostAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Net.Http.HttpContent? content) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PostAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Net.Http.HttpContent? content, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PostAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Net.Http.HttpContent? content) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PostAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Net.Http.HttpContent? content, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PutAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Net.Http.HttpContent? content) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PutAsync(System.Net.Http.HttpClient! client, System.Uri? requestUri, System.Net.Http.HttpContent? content, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PutAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Net.Http.HttpContent? content) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.PutAsync(System.Net.Http.HttpClient! client, string? requestUri, System.Net.Http.HttpContent? content, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.Send(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request) -> System.Net.Http.HttpResponseMessage!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.Send(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request, System.Net.Http.HttpCompletionOption completionOption) -> System.Net.Http.HttpResponseMessage!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.Send(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) -> System.Net.Http.HttpResponseMessage!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.Send(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Net.Http.HttpResponseMessage!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.SendAsync(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.SendAsync(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request, System.Net.Http.HttpCompletionOption completionOption) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.SendAsync(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request, System.Net.Http.HttpCompletionOption completionOption, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpClient.SendAsync(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedHttpWebRequest.GetStartTimeUtc() -> System.DateTime
+static Qyl.AutoInstrumentation.QylInterceptedHttpWebRequest.RecordException(System.Diagnostics.Activity? activity, System.DateTime startTimeUtc, string? method, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedHttpWebRequest.RecordResult(System.Diagnostics.Activity? activity, System.DateTime startTimeUtc, string? method, object? result) -> void
+static Qyl.AutoInstrumentation.QylInterceptedHttpWebRequest.StartActivity(System.Net.HttpWebRequest! request, string! methodName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedKafka.RecordConsumeSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedKafka.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedKafka.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedKafka.StartConsumerActivity() -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedKafka.StartProducerActivity() -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedLogger.Log(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func! formatter) -> void
+static Qyl.AutoInstrumentation.QylInterceptedLogger.LogExtension(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, System.Exception? exception, string? message, object?[]! args) -> void
+static Qyl.AutoInstrumentation.QylInterceptedMassTransit.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedMassTransit.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedMassTransit.StartActivity(string! operationName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedMongoDb.ObserveAsync(System.Threading.Tasks.Task? task, System.Diagnostics.Activity? activity) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedMongoDb.ObserveAsync(System.Threading.Tasks.Task? task, System.Diagnostics.Activity? activity) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedMongoDb.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedMongoDb.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedMongoDb.StartActivity(string! operationName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedNServiceBus.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedNServiceBus.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedNServiceBus.StartActivity(string! operationName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedQuartz.ObserveAsync(System.Threading.Tasks.Task? task, System.Diagnostics.Activity? activity) -> System.Threading.Tasks.Task!
+static Qyl.AutoInstrumentation.QylInterceptedQuartz.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedQuartz.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedQuartz.StartActivity() -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedRabbitMq.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedRabbitMq.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedRabbitMq.StartPublishActivity(string? exchange) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedRedis.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedRedis.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedRedis.StartCommandActivity(string! operationName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedWcfClient.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedWcfClient.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedWcfClient.StartActivity(string! clientType, string! methodName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylInterceptedWcfCore.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void
+static Qyl.AutoInstrumentation.QylInterceptedWcfCore.RecordSuccess(System.Diagnostics.Activity? activity) -> void
+static Qyl.AutoInstrumentation.QylInterceptedWcfCore.StartActivity(string! serviceName, string! contractName, string! operationName) -> System.Diagnostics.Activity?
+static Qyl.AutoInstrumentation.QylMetricMeters.GetEnabledMeterNames() -> string![]!
+static Qyl.AutoInstrumentation.QylNServiceBusMetrics.GetTimestamp() -> long
+static Qyl.AutoInstrumentation.QylNServiceBusMetrics.RecordDuration(long startTimestamp, string! operationName) -> void
+static readonly Qyl.AutoInstrumentation.QylActivitySource.Source -> System.Diagnostics.ActivitySource!
+static readonly Qyl.AutoInstrumentation.QylSelfTelemetry.AttributeChecks -> System.Diagnostics.Metrics.Counter!
+static readonly Qyl.AutoInstrumentation.QylSelfTelemetry.ConformanceProcessorFailures -> System.Diagnostics.Metrics.Counter!
+static readonly Qyl.AutoInstrumentation.QylSemanticAttributes.RpcGrpcStatusCodeOk -> int
diff --git a/src/Qyl.AutoInstrumentation/PublicAPI.Unshipped.txt b/src/Qyl.AutoInstrumentation/PublicAPI.Unshipped.txt
new file mode 100644
index 0000000..7dc5c58
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation/PublicAPI.Unshipped.txt
@@ -0,0 +1 @@
+#nullable enable
diff --git a/src/Qyl.AutoInstrumentation/Qyl.AutoInstrumentation.csproj b/src/Qyl.AutoInstrumentation/Qyl.AutoInstrumentation.csproj
index 373c71b..2984364 100644
--- a/src/Qyl.AutoInstrumentation/Qyl.AutoInstrumentation.csproj
+++ b/src/Qyl.AutoInstrumentation/Qyl.AutoInstrumentation.csproj
@@ -6,6 +6,8 @@
Qyl.AutoInstrumentation
Qyl.AutoInstrumentation
AOT-native zero-code .NET instrumentation core. Source-generated semconv registry + Activity/Meter primitives. NO IL rewriting, NO CLR profiler attach.
+ true
+ $(WarningsAsErrors);CS1591
$(InterceptorsNamespaces);Qyl.AutoInstrumentation.Generated
$(InterceptorsPreviewNamespaces);Qyl.AutoInstrumentation.Generated
@@ -15,6 +17,11 @@
+
+
+
+
+
@@ -23,6 +30,12 @@
+
+
diff --git a/src/Qyl.AutoInstrumentation/QylActivityNames.cs b/src/Qyl.AutoInstrumentation/QylActivityNames.cs
index 5faefb5..b92ccdb 100644
--- a/src/Qyl.AutoInstrumentation/QylActivityNames.cs
+++ b/src/Qyl.AutoInstrumentation/QylActivityNames.cs
@@ -1,9 +1,16 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Activity Names.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylActivityNames);
public static class QylActivityNames
{
+ /// Well-known HTTP Client Request value used by qyl auto-instrumentation.
public const string HttpClientRequest = "HTTP client request";
+ /// Well-known HTTP Server Request value used by qyl auto-instrumentation.
public const string HttpServerRequest = "HTTP server request";
+ /// Well-known database Client Command value used by qyl auto-instrumentation.
public const string DbClientCommand = "DB client command";
+ /// Well-known Entity Framework Core Operation value used by qyl auto-instrumentation.
public const string EntityFrameworkCoreOperation = "EF Core operation";
}
diff --git a/src/Qyl.AutoInstrumentation/QylActivitySource.cs b/src/Qyl.AutoInstrumentation/QylActivitySource.cs
index 0ee42e7..831e715 100644
--- a/src/Qyl.AutoInstrumentation/QylActivitySource.cs
+++ b/src/Qyl.AutoInstrumentation/QylActivitySource.cs
@@ -17,4 +17,12 @@ public static class QylActivitySource
public static readonly ActivitySource Source = new(
Name,
QylInstrumentation.Version);
+
+ internal static bool IsRecordingEnabled
+ => Source.HasListeners();
+
+ internal static Activity? StartActivity(string operationName, ActivityKind activityKind)
+ => Source.HasListeners()
+ ? Source.StartActivity(operationName, activityKind)
+ : null;
}
diff --git a/src/Qyl.AutoInstrumentation/QylAutoInstrumentationIds.cs b/src/Qyl.AutoInstrumentation/QylAutoInstrumentationIds.cs
index 87d5e04..0803fa6 100644
--- a/src/Qyl.AutoInstrumentation/QylAutoInstrumentationIds.cs
+++ b/src/Qyl.AutoInstrumentation/QylAutoInstrumentationIds.cs
@@ -1,43 +1,83 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Auto Instrumentation Signal.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylAutoInstrumentationSignal);
public enum QylAutoInstrumentationSignal
{
+ /// Represents the Traces qyl auto-instrumentation signal.
Traces,
+ /// Represents the Metrics qyl auto-instrumentation signal.
Metrics,
+ /// Represents the Logs qyl auto-instrumentation signal.
Logs,
}
+/// Defines the qyl auto-instrumentation surface for qyl Auto Instrumentation Ids.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylAutoInstrumentationIds);
public static class QylAutoInstrumentationIds
{
+ /// Well-known Ado Net value used by qyl auto-instrumentation.
public const string AdoNet = "ADONET";
+ /// Well-known ASP.NET value used by qyl auto-instrumentation.
public const string AspNet = "ASPNET";
+ /// Well-known ASP.NET Core value used by qyl auto-instrumentation.
public const string AspNetCore = "ASPNETCORE";
+ /// Well-known Azure value used by qyl auto-instrumentation.
public const string Azure = "AZURE";
+ /// Well-known Elasticsearch value used by qyl auto-instrumentation.
public const string Elasticsearch = "ELASTICSEARCH";
+ /// Well-known Elastic Transport value used by qyl auto-instrumentation.
public const string ElasticTransport = "ELASTICTRANSPORT";
+ /// Well-known Entity Framework Core value used by qyl auto-instrumentation.
public const string EntityFrameworkCore = "ENTITYFRAMEWORKCORE";
+ /// Well-known Graph Ql value used by qyl auto-instrumentation.
public const string GraphQl = "GRAPHQL";
+ /// Well-known gRPC Net Client value used by qyl auto-instrumentation.
public const string GrpcNetClient = "GRPCNETCLIENT";
+ /// Well-known HTTP Client value used by qyl auto-instrumentation.
public const string HttpClient = "HTTPCLIENT";
+ /// Well-known Kafka value used by qyl auto-instrumentation.
public const string Kafka = "KAFKA";
+ /// Well-known Mass Transit value used by qyl auto-instrumentation.
public const string MassTransit = "MASSTRANSIT";
+ /// Well-known Mongo Db value used by qyl auto-instrumentation.
public const string MongoDb = "MONGODB";
+ /// Well-known My Sql Connector value used by qyl auto-instrumentation.
public const string MySqlConnector = "MYSQLCONNECTOR";
+ /// Well-known My Sql Data value used by qyl auto-instrumentation.
public const string MySqlData = "MYSQLDATA";
+ /// Well-known Net Runtime value used by qyl auto-instrumentation.
public const string NetRuntime = "NETRUNTIME";
+ /// Well-known Npgsql value used by qyl auto-instrumentation.
public const string Npgsql = "NPGSQL";
+ /// Well-known N Service Bus value used by qyl auto-instrumentation.
public const string NServiceBus = "NSERVICEBUS";
+ /// Well-known Oracle Mda value used by qyl auto-instrumentation.
public const string OracleMda = "ORACLEMDA";
+ /// Well-known Process value used by qyl auto-instrumentation.
public const string Process = "PROCESS";
+ /// Well-known Quartz value used by qyl auto-instrumentation.
public const string Quartz = "QUARTZ";
+ /// Well-known Rabbit Mq value used by qyl auto-instrumentation.
public const string RabbitMq = "RABBITMQ";
+ /// Well-known Sql Client value used by qyl auto-instrumentation.
public const string SqlClient = "SQLCLIENT";
+ /// Well-known Sqlite value used by qyl auto-instrumentation.
public const string Sqlite = "SQLITE";
+ /// Well-known Stack Exchange Redis value used by qyl auto-instrumentation.
public const string StackExchangeRedis = "STACKEXCHANGEREDIS";
+ /// Well-known Wcf Client value used by qyl auto-instrumentation.
public const string WcfClient = "WCFCLIENT";
+ /// Well-known Wcf Core value used by qyl auto-instrumentation.
public const string WcfCore = "WCFCORE";
+ /// Well-known Wcf Service value used by qyl auto-instrumentation.
public const string WcfService = "WCFSERVICE";
+ /// Well-known I Logger value used by qyl auto-instrumentation.
public const string ILogger = "ILOGGER";
+ /// Well-known Log4 Net value used by qyl auto-instrumentation.
public const string Log4Net = "LOG4NET";
+ /// Well-known N Log value used by qyl auto-instrumentation.
public const string NLog = "NLOG";
}
diff --git a/src/Qyl.AutoInstrumentation/QylAutoInstrumentationOptions.cs b/src/Qyl.AutoInstrumentation/QylAutoInstrumentationOptions.cs
index 188391b..1802c9d 100644
--- a/src/Qyl.AutoInstrumentation/QylAutoInstrumentationOptions.cs
+++ b/src/Qyl.AutoInstrumentation/QylAutoInstrumentationOptions.cs
@@ -1,8 +1,12 @@
using System.Collections.ObjectModel;
using System.Globalization;
+using Qyl.AutoInstrumentation.Internal;
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Auto Instrumentation Options.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylAutoInstrumentationOptions);
public sealed class QylAutoInstrumentationOptions
{
private const string GlobalEnabledVariable = "OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED";
@@ -10,14 +14,12 @@ public sealed class QylAutoInstrumentationOptions
private const string MetricsEnabledVariable = "OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED";
private const string LogsEnabledVariable = "OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED";
private const string CaptureSensitiveValuesVariable = "QYL_AUTOINSTRUMENTATION_CAPTURE_SENSITIVE_VALUES";
+ private const string ConformanceEnabledVariable = "QYL_CONFORMANCE_ENABLED";
+ /// Well-known Current value used by qyl auto-instrumentation.
public static QylAutoInstrumentationOptions Current => CurrentHolder.Value;
- private readonly IReadOnlyDictionary _instrumentationEnabled;
-
- static QylAutoInstrumentationOptions()
- {
- }
+ private readonly IReadOnlyDictionary _instrumentationEnabled;
private QylAutoInstrumentationOptions(
bool globalEnabled,
@@ -25,7 +27,8 @@ private QylAutoInstrumentationOptions(
bool metricsEnabled,
bool logsEnabled,
bool captureSensitiveValues,
- IReadOnlyDictionary instrumentationEnabled,
+ bool conformanceProcessorEnabled,
+ IReadOnlyDictionary instrumentationEnabled,
bool entityFrameworkCoreSetDbStatementForText,
bool graphQlSetDocument,
bool oracleMdaSetDbStatementForText,
@@ -48,6 +51,7 @@ private QylAutoInstrumentationOptions(
MetricsEnabled = metricsEnabled;
LogsEnabled = logsEnabled;
CaptureSensitiveValues = captureSensitiveValues;
+ ConformanceProcessorEnabled = conformanceProcessorEnabled;
_instrumentationEnabled = instrumentationEnabled;
EntityFrameworkCoreSetDbStatementForText = entityFrameworkCoreSetDbStatementForText;
GraphQlSetDocument = graphQlSetDocument;
@@ -61,111 +65,114 @@ private QylAutoInstrumentationOptions(
GrpcNetClientCapturedResponseMetadata = grpcNetClientCapturedResponseMetadata;
HttpClientCapturedRequestHeaders = httpClientCapturedRequestHeaders;
HttpClientCapturedResponseHeaders = httpClientCapturedResponseHeaders;
+ AspNetCoreCapturedRequestHeaderMap = QylCapturedNameMap.Create(QylSemanticAttributes.HttpRequestHeaderPrefix, aspNetCoreCapturedRequestHeaders);
+ AspNetCoreCapturedResponseHeaderMap = QylCapturedNameMap.Create(QylSemanticAttributes.HttpResponseHeaderPrefix, aspNetCoreCapturedResponseHeaders);
+ GrpcNetClientCapturedRequestMetadataMap = QylCapturedNameMap.Create(QylSemanticAttributes.GrpcRequestMetadataPrefix, grpcNetClientCapturedRequestMetadata, normalizeLookupName: true);
+ GrpcNetClientCapturedResponseMetadataMap = QylCapturedNameMap.Create(QylSemanticAttributes.GrpcResponseMetadataPrefix, grpcNetClientCapturedResponseMetadata, normalizeLookupName: true);
+ HttpClientCapturedRequestHeaderMap = QylCapturedNameMap.Create(QylSemanticAttributes.HttpRequestHeaderPrefix, httpClientCapturedRequestHeaders);
+ HttpClientCapturedResponseHeaderMap = QylCapturedNameMap.Create(QylSemanticAttributes.HttpResponseHeaderPrefix, httpClientCapturedResponseHeaders);
AspNetCoreUrlQueryRedactionDisabled = aspNetCoreUrlQueryRedactionDisabled;
HttpClientUrlQueryRedactionDisabled = httpClientUrlQueryRedactionDisabled;
AspNetUrlQueryRedactionDisabled = aspNetUrlQueryRedactionDisabled;
SqlClientNetFxIlRewriteRequested = sqlClientNetFxIlRewriteRequested;
}
+ /// Gets the configured Global Enabled value for the current qyl auto-instrumentation runtime.
public bool GlobalEnabled { get; }
+ /// Gets the configured Traces Enabled value for the current qyl auto-instrumentation runtime.
public bool TracesEnabled { get; }
+ /// Gets the configured Metrics Enabled value for the current qyl auto-instrumentation runtime.
public bool MetricsEnabled { get; }
+ /// Gets the configured Logs Enabled value for the current qyl auto-instrumentation runtime.
public bool LogsEnabled { get; }
+ /// Gets the configured Capture Sensitive Values value for the current qyl auto-instrumentation runtime.
public bool CaptureSensitiveValues { get; }
+ /// Gets the configured Conformance Processor Enabled value for the current qyl auto-instrumentation runtime.
+ public bool ConformanceProcessorEnabled { get; }
+
+ /// Gets the configured Entity Framework Core Set database Statement For Text value for the current qyl auto-instrumentation runtime.
public bool EntityFrameworkCoreSetDbStatementForText { get; }
+ /// Gets the configured Graph Ql Set Document value for the current qyl auto-instrumentation runtime.
public bool GraphQlSetDocument { get; }
+ /// Gets the configured Oracle Mda Set database Statement For Text value for the current qyl auto-instrumentation runtime.
public bool OracleMdaSetDbStatementForText { get; }
+ /// Gets the configured Sql Client Set database Statement For Text value for the current qyl auto-instrumentation runtime.
public bool SqlClientSetDbStatementForText { get; }
+ /// Gets the configured ASP.NET Captured Request Headers value for the current qyl auto-instrumentation runtime.
public string[] AspNetCapturedRequestHeaders { get; }
+ /// Gets the configured ASP.NET Captured Response Headers value for the current qyl auto-instrumentation runtime.
public string[] AspNetCapturedResponseHeaders { get; }
+ /// Gets the configured ASP.NET Core Captured Request Headers value for the current qyl auto-instrumentation runtime.
public string[] AspNetCoreCapturedRequestHeaders { get; }
+ /// Gets the configured ASP.NET Core Captured Response Headers value for the current qyl auto-instrumentation runtime.
public string[] AspNetCoreCapturedResponseHeaders { get; }
+ /// Gets the configured gRPC Net Client Captured Request Metadata value for the current qyl auto-instrumentation runtime.
public string[] GrpcNetClientCapturedRequestMetadata { get; }
+ /// Gets the configured gRPC Net Client Captured Response Metadata value for the current qyl auto-instrumentation runtime.
public string[] GrpcNetClientCapturedResponseMetadata { get; }
+ /// Gets the configured HTTP Client Captured Request Headers value for the current qyl auto-instrumentation runtime.
public string[] HttpClientCapturedRequestHeaders { get; }
+ /// Gets the configured HTTP Client Captured Response Headers value for the current qyl auto-instrumentation runtime.
public string[] HttpClientCapturedResponseHeaders { get; }
+ internal QylCapturedNameMap AspNetCoreCapturedRequestHeaderMap { get; }
+
+ internal QylCapturedNameMap AspNetCoreCapturedResponseHeaderMap { get; }
+
+ internal QylCapturedNameMap GrpcNetClientCapturedRequestMetadataMap { get; }
+
+ internal QylCapturedNameMap GrpcNetClientCapturedResponseMetadataMap { get; }
+
+ internal QylCapturedNameMap HttpClientCapturedRequestHeaderMap { get; }
+
+ internal QylCapturedNameMap HttpClientCapturedResponseHeaderMap { get; }
+
+ /// Gets the configured ASP.NET Core Url Query Redaction Disabled value for the current qyl auto-instrumentation runtime.
public bool AspNetCoreUrlQueryRedactionDisabled { get; }
+ /// Gets the configured HTTP Client Url Query Redaction Disabled value for the current qyl auto-instrumentation runtime.
public bool HttpClientUrlQueryRedactionDisabled { get; }
+ /// Gets the configured ASP.NET Url Query Redaction Disabled value for the current qyl auto-instrumentation runtime.
public bool AspNetUrlQueryRedactionDisabled { get; }
+ /// Gets the configured Sql Client Net Fx Il Rewrite Requested value for the current qyl auto-instrumentation runtime.
public bool SqlClientNetFxIlRewriteRequested { get; }
+ /// Well-known Sql Client Net Fx Il Rewrite Enabled value used by qyl auto-instrumentation.
public bool SqlClientNetFxIlRewriteEnabled => false;
+ /// Runs the Is Instrumentation Enabled runtime helper used by source-generated qyl interceptors.
public bool IsInstrumentationEnabled(QylAutoInstrumentationSignal signal, string instrumentationId)
{
ArgumentNullException.ThrowIfNull(instrumentationId);
- return _instrumentationEnabled.TryGetValue(BuildKey(signal, instrumentationId), out var enabled)
+ return _instrumentationEnabled.TryGetValue(new InstrumentationLookupKey(signal, instrumentationId), out var enabled)
? enabled
: IsSignalEnabled(signal);
}
+ /// Runs the Has Any Activity Instrumentation Enabled runtime helper used by source-generated qyl interceptors.
public bool HasAnyActivityInstrumentationEnabled()
=> HasAnyInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, TraceInstrumentationIds) ||
HasAnyInstrumentationEnabled(QylAutoInstrumentationSignal.Logs, LogInstrumentationIds);
- private static class CurrentHolder
- {
- internal static readonly QylAutoInstrumentationOptions Value = Load();
- }
-
- private static QylAutoInstrumentationOptions Load()
- {
- var globalEnabled = ReadBoolean(GlobalEnabledVariable) ?? true;
- var tracesEnabled = ReadBoolean(TracesEnabledVariable) ?? globalEnabled;
- var metricsEnabled = ReadBoolean(MetricsEnabledVariable) ?? globalEnabled;
- var logsEnabled = ReadBoolean(LogsEnabledVariable) ?? globalEnabled;
- var instrumentationEnabled = new Dictionary(StringComparer.Ordinal);
-
- AddSignalInstrumentations(instrumentationEnabled, QylAutoInstrumentationSignal.Traces, tracesEnabled, TraceInstrumentationIds);
- AddSignalInstrumentations(instrumentationEnabled, QylAutoInstrumentationSignal.Metrics, metricsEnabled, MetricInstrumentationIds);
- AddSignalInstrumentations(instrumentationEnabled, QylAutoInstrumentationSignal.Logs, logsEnabled, LogInstrumentationIds);
-
- return new QylAutoInstrumentationOptions(
- globalEnabled,
- tracesEnabled,
- metricsEnabled,
- logsEnabled,
- ReadBoolean(CaptureSensitiveValuesVariable) ?? false,
- new ReadOnlyDictionary(instrumentationEnabled),
- ReadBoolean("OTEL_DOTNET_AUTO_ENTITYFRAMEWORKCORE_SET_DBSTATEMENT_FOR_TEXT") ?? false,
- ReadBoolean("OTEL_DOTNET_AUTO_GRAPHQL_SET_DOCUMENT") ?? false,
- ReadBoolean("OTEL_DOTNET_AUTO_ORACLEMDA_SET_DBSTATEMENT_FOR_TEXT") ?? false,
- ReadBoolean("OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT") ?? false,
- ReadList("OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS"),
- ReadList("OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS"),
- ReadList("OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS"),
- ReadList("OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS"),
- ReadList("OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_REQUEST_METADATA"),
- ReadList("OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_RESPONSE_METADATA"),
- ReadList("OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS"),
- ReadList("OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS"),
- ReadBoolean("OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION") ?? false,
- ReadBoolean("OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION") ?? false,
- ReadBoolean("OTEL_DOTNET_EXPERIMENTAL_ASPNET_DISABLE_URL_QUERY_REDACTION") ?? false,
- ReadBoolean("OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED") ?? false);
- }
-
private static readonly string[] TraceInstrumentationIds =
[
QylAutoInstrumentationIds.AdoNet,
@@ -215,16 +222,64 @@ private static QylAutoInstrumentationOptions Load()
QylAutoInstrumentationIds.NLog,
];
+ private static class CurrentHolder
+ {
+ internal static readonly QylAutoInstrumentationOptions Value = Load();
+ }
+
+ private static QylAutoInstrumentationOptions Load()
+ {
+ var globalEnabled = ReadBoolean(GlobalEnabledVariable) ?? true;
+ var tracesEnabled = ReadBoolean(TracesEnabledVariable) ?? globalEnabled;
+ var metricsEnabled = ReadBoolean(MetricsEnabledVariable) ?? globalEnabled;
+ var logsEnabled = ReadBoolean(LogsEnabledVariable) ?? globalEnabled;
+ var instrumentationEnabled = new Dictionary();
+
+ AddSignalInstrumentations(instrumentationEnabled, QylAutoInstrumentationSignal.Traces, tracesEnabled, TraceInstrumentationIds);
+ AddSignalInstrumentations(instrumentationEnabled, QylAutoInstrumentationSignal.Metrics, metricsEnabled, MetricInstrumentationIds);
+ AddSignalInstrumentations(instrumentationEnabled, QylAutoInstrumentationSignal.Logs, logsEnabled, LogInstrumentationIds);
+
+ return new QylAutoInstrumentationOptions(
+ globalEnabled,
+ tracesEnabled,
+ metricsEnabled,
+ logsEnabled,
+ ReadBoolean(CaptureSensitiveValuesVariable) ?? false,
+ ReadBoolean(ConformanceEnabledVariable) ?? false,
+ new ReadOnlyDictionary(instrumentationEnabled),
+ ReadBoolean("OTEL_DOTNET_AUTO_ENTITYFRAMEWORKCORE_SET_DBSTATEMENT_FOR_TEXT") ?? false,
+ ReadBoolean("OTEL_DOTNET_AUTO_GRAPHQL_SET_DOCUMENT") ?? false,
+ ReadBoolean("OTEL_DOTNET_AUTO_ORACLEMDA_SET_DBSTATEMENT_FOR_TEXT") ?? false,
+ ReadBoolean("OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT") ?? false,
+ ReadList("OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS"),
+ ReadList("OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS"),
+ ReadList("OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS"),
+ ReadList("OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS"),
+ ReadList("OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_REQUEST_METADATA"),
+ ReadList("OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_RESPONSE_METADATA"),
+ ReadList("OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS"),
+ ReadList("OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS"),
+ ReadBoolean("OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION") ?? false,
+ ReadBoolean("OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION") ?? false,
+ ReadBoolean("OTEL_DOTNET_EXPERIMENTAL_ASPNET_DISABLE_URL_QUERY_REDACTION") ?? false,
+ ReadBoolean("OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED") ?? false);
+ }
+
private static void AddSignalInstrumentations(
- Dictionary target,
+ Dictionary target,
QylAutoInstrumentationSignal signal,
bool signalDefault,
string[] instrumentationIds)
{
+ ArgumentNullException.ThrowIfNull(instrumentationIds);
+
foreach (var instrumentationId in instrumentationIds)
{
+ if (string.IsNullOrWhiteSpace(instrumentationId))
+ continue;
+
var variable = BuildSignalSpecificVariable(signal, instrumentationId);
- target[BuildKey(signal, instrumentationId)] = ReadBoolean(variable) ?? signalDefault;
+ target[new InstrumentationLookupKey(signal, instrumentationId)] = ReadBoolean(variable) ?? signalDefault;
}
}
@@ -257,9 +312,6 @@ private static string BuildSignalSpecificVariable(QylAutoInstrumentationSignal s
_ => throw new ArgumentOutOfRangeException(nameof(signal), signal, null),
};
- private static string BuildKey(QylAutoInstrumentationSignal signal, string instrumentationId)
- => signal.ToString() + ":" + instrumentationId;
-
private static bool? ReadBoolean(string variable)
{
var value = Environment.GetEnvironmentVariable(variable);
@@ -298,4 +350,31 @@ private static string[] ReadList(string variable)
.Distinct(StringComparer.Ordinal)
.ToArray();
}
+
+ private readonly struct InstrumentationLookupKey : IEquatable
+ {
+ private readonly QylAutoInstrumentationSignal signal;
+ private readonly string instrumentationId;
+
+ internal InstrumentationLookupKey(QylAutoInstrumentationSignal signal, string instrumentationId)
+ {
+ this.signal = signal;
+ this.instrumentationId = instrumentationId;
+ }
+
+ /// Runs the Equals runtime helper used by source-generated qyl interceptors.
+ public bool Equals(InstrumentationLookupKey other)
+ => signal == other.signal &&
+ string.Equals(instrumentationId, other.instrumentationId, StringComparison.Ordinal);
+
+ /// Runs the Equals runtime helper used by source-generated qyl interceptors.
+ public override bool Equals(object? obj)
+ => obj is InstrumentationLookupKey other && Equals(other);
+
+ /// Runs the Get Hash Code runtime helper used by source-generated qyl interceptors.
+ public override int GetHashCode()
+ => HashCode.Combine(
+ signal,
+ instrumentationId is null ? 0 : StringComparer.Ordinal.GetHashCode(instrumentationId));
+ }
}
diff --git a/src/Qyl.AutoInstrumentation/QylDbClientMetrics.cs b/src/Qyl.AutoInstrumentation/QylDbClientMetrics.cs
index ceb7e07..554acf9 100644
--- a/src/Qyl.AutoInstrumentation/QylDbClientMetrics.cs
+++ b/src/Qyl.AutoInstrumentation/QylDbClientMetrics.cs
@@ -2,19 +2,24 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl database Client Metrics.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylDbClientMetrics);
public static class QylDbClientMetrics
{
private static readonly Meter Meter = new(QylMetricMeters.DatabaseMeterName);
private static readonly Histogram OperationDuration = Meter.CreateHistogram(QylMetricNames.DbClientOperationDuration, "s");
+ /// Runs the Get Timestamp runtime helper used by source-generated qyl interceptors.
public static long GetTimestamp()
- => TimeProvider.System.GetTimestamp();
+ => OperationDuration.Enabled ? TimeProvider.System.GetTimestamp() : 0;
+ /// Runs the Record Duration runtime helper used by source-generated qyl interceptors.
public static void RecordDuration(long startTimestamp, string instrumentationId)
{
ArgumentNullException.ThrowIfNull(instrumentationId);
- if (!ShouldRecord(instrumentationId))
+ if (startTimestamp is 0 || !IsRecordingEnabled(instrumentationId))
return;
var elapsed = TimeProvider.System.GetElapsedTime(startTimestamp);
@@ -26,6 +31,9 @@ public static void RecordDuration(long startTimestamp, string instrumentationId)
}
}
+ internal static bool IsRecordingEnabled(string instrumentationId)
+ => OperationDuration.Enabled && ShouldRecord(instrumentationId);
+
private static bool ShouldRecord(string instrumentationId)
=> instrumentationId switch
{
diff --git a/src/Qyl.AutoInstrumentation/QylHttpClientMetrics.cs b/src/Qyl.AutoInstrumentation/QylHttpClientMetrics.cs
index f06f292..c568ac0 100644
--- a/src/Qyl.AutoInstrumentation/QylHttpClientMetrics.cs
+++ b/src/Qyl.AutoInstrumentation/QylHttpClientMetrics.cs
@@ -8,9 +8,16 @@ internal static class QylHttpClientMetrics
private static readonly Meter Meter = new(QylMetricMeters.HttpClientMeterName);
private static readonly Histogram RequestDuration = Meter.CreateHistogram(QylMetricNames.HttpClientRequestDuration, "s");
+ public static bool IsRecordingEnabled
+ => IsRecordingEnabledFor(QylAutoInstrumentationOptions.Current);
+
+ public static bool IsRecordingEnabledFor(QylAutoInstrumentationOptions options)
+ => RequestDuration.Enabled &&
+ options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.HttpClient);
+
public static void RecordRequestDuration(DateTime startTimeUtc, string? method, int? statusCode)
{
- if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.HttpClient))
+ if (!IsRecordingEnabled)
return;
var elapsed = TimeProvider.System.GetUtcNow().UtcDateTime - startTimeUtc;
diff --git a/src/Qyl.AutoInstrumentation/QylInstrumentation.cs b/src/Qyl.AutoInstrumentation/QylInstrumentation.cs
index 7573c0f..4af3205 100644
--- a/src/Qyl.AutoInstrumentation/QylInstrumentation.cs
+++ b/src/Qyl.AutoInstrumentation/QylInstrumentation.cs
@@ -11,13 +11,13 @@ namespace Qyl.AutoInstrumentation;
public static class QylInstrumentation
{
/// Library version, baked at build time by the root Directory.Build.props.
- public const string Version = "0.2.0-pre.1";
+ public const string Version = "0.3.0-pre.1";
private static int _activated;
///
- /// Activate qyl: subscribe an that runs the M3 semconv-
- /// conformance check on every emitted span from the qyl ActivitySource. Idempotent.
+ /// Activate qyl: subscribe an for qyl spans. The M3 semconv
+ /// conformance counter remains default-off and is checked only after explicit opt-in.
///
/// true on the first activation, false on subsequent calls.
public static bool Activate()
diff --git a/src/Qyl.AutoInstrumentation/QylInstrumentationDomains.cs b/src/Qyl.AutoInstrumentation/QylInstrumentationDomains.cs
index 2e3dba8..f8076a4 100644
--- a/src/Qyl.AutoInstrumentation/QylInstrumentationDomains.cs
+++ b/src/Qyl.AutoInstrumentation/QylInstrumentationDomains.cs
@@ -1,29 +1,56 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Instrumentation Domains.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInstrumentationDomains);
public static class QylInstrumentationDomains
{
+ /// Well-known ASP.NET Core Server value used by qyl auto-instrumentation.
public const string AspNetCoreServer = "aspnetcore.server";
+ /// Well-known Azure Sdk value used by qyl auto-instrumentation.
public const string AzureSdk = "azure.sdk";
+ /// Well-known database Client value used by qyl auto-instrumentation.
public const string DbClient = "db.client";
+ /// Well-known database Ef Core value used by qyl auto-instrumentation.
public const string DbEfCore = "db.efcore";
+ /// Well-known database Elasticsearch value used by qyl auto-instrumentation.
public const string DbElasticsearch = "db.elasticsearch";
+ /// Well-known database Mongo Db value used by qyl auto-instrumentation.
public const string DbMongoDb = "db.mongodb";
+ /// Well-known database Redis value used by qyl auto-instrumentation.
public const string DbRedis = "db.redis";
+ /// Well-known database Sql Client value used by qyl auto-instrumentation.
public const string DbSqlClient = "db.sqlclient";
+ /// Well-known Elastic Transport value used by qyl auto-instrumentation.
public const string ElasticTransport = "elastic.transport";
+ /// Well-known Graph Ql value used by qyl auto-instrumentation.
public const string GraphQl = "graphql";
+ /// Well-known HTTP Client value used by qyl auto-instrumentation.
public const string HttpClient = "http.client";
+ /// Well-known HTTP Server value used by qyl auto-instrumentation.
public const string HttpServer = "http.server";
+ /// Well-known HTTP Web Request value used by qyl auto-instrumentation.
public const string HttpWebRequest = "http.webrequest";
+ /// Well-known Job Quartz value used by qyl auto-instrumentation.
public const string JobQuartz = "job.quartz";
+ /// Well-known Log I Logger value used by qyl auto-instrumentation.
public const string LogILogger = "log.ilogger";
+ /// Well-known Log Log4 Net value used by qyl auto-instrumentation.
public const string LogLog4Net = "log.log4net";
+ /// Well-known Log N Log value used by qyl auto-instrumentation.
public const string LogNLog = "log.nlog";
+ /// Well-known Messaging Kafka value used by qyl auto-instrumentation.
public const string MessagingKafka = "messaging.kafka";
+ /// Well-known Messaging Mass Transit value used by qyl auto-instrumentation.
public const string MessagingMassTransit = "messaging.masstransit";
+ /// Well-known Messaging N Service Bus value used by qyl auto-instrumentation.
public const string MessagingNServiceBus = "messaging.nservicebus";
+ /// Well-known Messaging Rabbit Mq value used by qyl auto-instrumentation.
public const string MessagingRabbitMq = "messaging.rabbitmq";
+ /// Well-known Rpc Grpc value used by qyl auto-instrumentation.
public const string RpcGrpc = "rpc.grpc";
+ /// Well-known Rpc Wcf Client value used by qyl auto-instrumentation.
public const string RpcWcfClient = "rpc.wcf.client";
+ /// Well-known Rpc Wcf Core value used by qyl auto-instrumentation.
public const string RpcWcfCore = "rpc.wcf.core";
}
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedAspNetCore.cs b/src/Qyl.AutoInstrumentation/QylInterceptedAspNetCore.cs
index 84da2e5..5ef2d54 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedAspNetCore.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedAspNetCore.cs
@@ -2,12 +2,17 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
+using Qyl.AutoInstrumentation.Internal;
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted ASP.NET Core.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedAspNetCore);
public static class QylInterceptedAspNetCore
{
+ /// Runs the Build runtime helper used by source-generated qyl interceptors.
public static WebApplication Build(WebApplicationBuilder builder)
{
if (builder is null)
@@ -18,24 +23,31 @@ public static WebApplication Build(WebApplicationBuilder builder)
return app;
}
+ /// Runs the Map Get runtime helper used by source-generated qyl interceptors.
public static IEndpointConventionBuilder MapGet(IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)
=> endpoints.MapGet(pattern, Observe(requestDelegate));
+ /// Runs the Map Post runtime helper used by source-generated qyl interceptors.
public static IEndpointConventionBuilder MapPost(IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)
=> endpoints.MapPost(pattern, Observe(requestDelegate));
+ /// Runs the Map Put runtime helper used by source-generated qyl interceptors.
public static IEndpointConventionBuilder MapPut(IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)
=> endpoints.MapPut(pattern, Observe(requestDelegate));
+ /// Runs the Map Delete runtime helper used by source-generated qyl interceptors.
public static IEndpointConventionBuilder MapDelete(IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)
=> endpoints.MapDelete(pattern, Observe(requestDelegate));
+ /// Runs the Map Patch runtime helper used by source-generated qyl interceptors.
public static IEndpointConventionBuilder MapPatch(IEndpointRouteBuilder endpoints, string pattern, RequestDelegate requestDelegate)
=> endpoints.MapPatch(pattern, Observe(requestDelegate));
+ /// Runs the Map Methods runtime helper used by source-generated qyl interceptors.
public static IEndpointConventionBuilder MapMethods(IEndpointRouteBuilder endpoints, string pattern, IEnumerable httpMethods, RequestDelegate requestDelegate)
=> endpoints.MapMethods(pattern, httpMethods, Observe(requestDelegate));
+ /// Runs the Invoke Async runtime helper used by source-generated qyl interceptors.
public static Task InvokeAsync(RequestDelegate requestDelegate, HttpContext context)
{
if (requestDelegate is null)
@@ -65,7 +77,7 @@ public static Task InvokeAsync(RequestDelegate requestDelegate, HttpContext cont
var method = QylHttpMethod.Normalize(context.Request.Method);
var route = GetRoute(context);
- var activity = QylActivitySource.Source.StartActivity(QylActivityNames.HttpServerRequest, ActivityKind.Server);
+ var activity = QylActivitySource.StartActivity(QylActivityNames.HttpServerRequest, ActivityKind.Server);
if (activity is null)
return null;
@@ -86,7 +98,7 @@ public static Task InvokeAsync(RequestDelegate requestDelegate, HttpContext cont
if (route is not null)
activity.SetTag(QylSemanticAttributes.HttpRoute, route);
- SetConfiguredHeaders(activity, QylSemanticAttributes.HttpRequestHeaderPrefix, options.AspNetCoreCapturedRequestHeaders, context.Request.Headers);
+ SetConfiguredHeaders(activity, options.AspNetCoreCapturedRequestHeaderMap, context.Request.Headers);
return activity;
}
@@ -126,7 +138,7 @@ private static void RecordResponse(Activity? activity, HttpContext context)
return;
activity.SetTag(QylSemanticAttributes.HttpResponseStatusCode, context.Response.StatusCode);
- SetConfiguredHeaders(activity, QylSemanticAttributes.HttpResponseHeaderPrefix, QylAutoInstrumentationOptions.Current.AspNetCoreCapturedResponseHeaders, context.Response.Headers);
+ SetConfiguredHeaders(activity, QylAutoInstrumentationOptions.Current.AspNetCoreCapturedResponseHeaderMap, context.Response.Headers);
if (context.Response.StatusCode >= 500)
{
activity.SetTag(QylSemanticAttributes.ErrorType, context.Response.StatusCode.ToString(System.Globalization.CultureInfo.InvariantCulture));
@@ -137,21 +149,18 @@ private static void RecordResponse(Activity? activity, HttpContext context)
private static RequestDelegate Observe(RequestDelegate requestDelegate)
=> requestDelegate is null ? null! : context => InvokeAsync(requestDelegate, context);
- private static void SetConfiguredHeaders(Activity activity, string prefix, string[] configuredHeaders, IHeaderDictionary headers)
+ private static void SetConfiguredHeaders(Activity activity, QylCapturedNameMap configuredHeaders, IHeaderDictionary headers)
{
- if (configuredHeaders.Length is 0)
+ if (configuredHeaders.Count is 0)
return;
- foreach (var headerName in configuredHeaders)
+ for (var index = 0; index < configuredHeaders.Count; index++)
{
- if (headers.TryGetValue(headerName, out var values) && values.Count > 0)
- activity.SetTag(prefix + NormalizeHeaderName(headerName), values.ToArray());
+ if (headers.TryGetValue(configuredHeaders.GetLookupName(index), out var values) && values.Count > 0)
+ activity.SetTag(configuredHeaders.GetTagName(index), values.Count is 1 ? values[0] : values.ToArray());
}
}
- private static string NormalizeHeaderName(string headerName)
- => headerName.Trim().ToLowerInvariant().Replace('_', '-');
-
private static string TrimQueryPrefix(string? query)
=> string.IsNullOrEmpty(query) || query[0] is not '?'
? query ?? string.Empty
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedAzure.cs b/src/Qyl.AutoInstrumentation/QylInterceptedAzure.cs
index 74d0371..f2550d1 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedAzure.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedAzure.cs
@@ -2,15 +2,19 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Azure.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedAzure);
public static class QylInterceptedAzure
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string methodName)
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.Azure))
return null;
- var activity = QylActivitySource.Source.StartActivity("Azure SDK", ActivityKind.Client);
+ var activity = QylActivitySource.StartActivity("Azure SDK", ActivityKind.Client);
if (activity is null)
return null;
@@ -18,10 +22,12 @@ public static class QylInterceptedAzure
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedDbCommand.cs b/src/Qyl.AutoInstrumentation/QylInterceptedDbCommand.cs
index 0fdcd9d..d346745 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedDbCommand.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedDbCommand.cs
@@ -4,9 +4,13 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted database Command.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedDbCommand);
public static class QylInterceptedDbCommand
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(DbCommand command, string instrumentationId, string operationName)
{
ArgumentNullException.ThrowIfNull(command);
@@ -17,11 +21,11 @@ public static class QylInterceptedDbCommand
if (!options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, instrumentationId))
return null;
- var operation = NormalizeOperation(operationName, command);
- var activity = QylActivitySource.Source.StartActivity("DB client command", ActivityKind.Client);
+ var activity = QylActivitySource.StartActivity("DB client command", ActivityKind.Client);
if (activity is null)
return null;
+ var operation = NormalizeOperation(operationName, command);
activity.SetTag(QylSemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.DbClient);
activity.SetTag(QylSemanticAttributes.DbSystemName, GetDbSystemName(instrumentationId));
activity.SetTag(QylSemanticAttributes.DbOperationName, operation);
@@ -40,15 +44,25 @@ public static class QylInterceptedDbCommand
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
- public static async Task ObserveAsync(Task task, Activity? activity, long metricStart, string instrumentationId)
+ /// Observes an asynchronous database command and records qyl success, exception, and duration telemetry.
+ public static Task ObserveAsync(Task task, Activity? activity, long metricStart, string instrumentationId)
{
ArgumentNullException.ThrowIfNull(task);
ArgumentNullException.ThrowIfNull(instrumentationId);
+ if (activity is null && !QylDbClientMetrics.IsRecordingEnabled(instrumentationId))
+ return task;
+
+ return ObserveSlowAsync(task, activity, metricStart, instrumentationId);
+ }
+
+ private static async Task ObserveSlowAsync(Task task, Activity? activity, long metricStart, string instrumentationId)
+ {
try
{
var result = await task.ConfigureAwait(false);
@@ -68,6 +82,7 @@ public static async Task ObserveAsync(Task task, Activity? activity, lo
}
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedElastic.cs b/src/Qyl.AutoInstrumentation/QylInterceptedElastic.cs
index 2693274..e2b6f03 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedElastic.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedElastic.cs
@@ -2,9 +2,13 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Elastic.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedElastic);
public static class QylInterceptedElastic
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string instrumentationId, string methodName)
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, instrumentationId))
@@ -14,7 +18,7 @@ public static class QylInterceptedElastic
var activityName = string.Equals(instrumentationId, QylAutoInstrumentationIds.ElasticTransport, StringComparison.Ordinal)
? "Elastic transport request"
: "Elasticsearch request";
- var activity = QylActivitySource.Source.StartActivity(activityName, ActivityKind.Client);
+ var activity = QylActivitySource.StartActivity(activityName, ActivityKind.Client);
if (activity is null)
return null;
@@ -29,10 +33,12 @@ public static class QylInterceptedElastic
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedEntityFrameworkCore.cs b/src/Qyl.AutoInstrumentation/QylInterceptedEntityFrameworkCore.cs
index f34797e..a7a57ac 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedEntityFrameworkCore.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedEntityFrameworkCore.cs
@@ -2,9 +2,13 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Entity Framework Core.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedEntityFrameworkCore);
public static class QylInterceptedEntityFrameworkCore
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string operationName)
{
ArgumentNullException.ThrowIfNull(operationName);
@@ -12,7 +16,7 @@ public static class QylInterceptedEntityFrameworkCore
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.EntityFrameworkCore))
return null;
- var activity = QylActivitySource.Source.StartActivity("EF Core operation", ActivityKind.Client);
+ var activity = QylActivitySource.StartActivity("EF Core operation", ActivityKind.Client);
if (activity is null)
return null;
@@ -22,10 +26,12 @@ public static class QylInterceptedEntityFrameworkCore
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedExternalLogger.cs b/src/Qyl.AutoInstrumentation/QylInterceptedExternalLogger.cs
index addf7e1..97d31c2 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedExternalLogger.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedExternalLogger.cs
@@ -2,8 +2,12 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted External Logger.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedExternalLogger);
public static class QylInterceptedExternalLogger
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string instrumentationId, string domain, string methodName, string? severityName)
{
ArgumentNullException.ThrowIfNull(instrumentationId);
@@ -13,7 +17,7 @@ public static class QylInterceptedExternalLogger
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Logs, instrumentationId))
return null;
- var activity = QylActivitySource.Source.StartActivity(GetActivityName(instrumentationId), ActivityKind.Internal);
+ var activity = QylActivitySource.StartActivity(GetActivityName(instrumentationId), ActivityKind.Internal);
if (activity is null)
return null;
@@ -22,6 +26,7 @@ public static class QylInterceptedExternalLogger
return activity;
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedGraphQl.cs b/src/Qyl.AutoInstrumentation/QylInterceptedGraphQl.cs
index cb0414e..ebb69b9 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedGraphQl.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedGraphQl.cs
@@ -2,15 +2,19 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Graph Ql.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedGraphQl);
public static class QylInterceptedGraphQl
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity()
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.GraphQl))
return null;
- var activity = QylActivitySource.Source.StartActivity("GraphQL execute", ActivityKind.Internal);
+ var activity = QylActivitySource.StartActivity("GraphQL execute", ActivityKind.Internal);
if (activity is null)
return null;
@@ -19,6 +23,7 @@ public static class QylInterceptedGraphQl
return activity;
}
+ /// Runs the Record Execution Options runtime helper used by source-generated qyl interceptors.
public static void RecordExecutionOptions(Activity? activity, string? operationName, string? document)
{
if (activity is null)
@@ -34,10 +39,12 @@ public static void RecordExecutionOptions(Activity? activity, string? operationN
}
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Observes an asynchronous GraphQL operation and records qyl success or exception telemetry.
public static Task ObserveAsync(Task? task, Activity? activity)
{
if (activity is null || task is null)
@@ -68,6 +75,7 @@ private static async Task ObserveSlowAsync(Task task, Activity activity
}
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedGrpcNetClient.cs b/src/Qyl.AutoInstrumentation/QylInterceptedGrpcNetClient.cs
index b3ed5fd..a5d03eb 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedGrpcNetClient.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedGrpcNetClient.cs
@@ -1,11 +1,16 @@
using System.Diagnostics;
using Grpc.Core;
+using Qyl.AutoInstrumentation.Internal;
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted gRPC Net Client.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedGrpcNetClient);
public static class QylInterceptedGrpcNetClient
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string clientTypeName, string methodName, Metadata? requestMetadata)
{
ArgumentNullException.ThrowIfNull(clientTypeName);
@@ -15,7 +20,7 @@ public static class QylInterceptedGrpcNetClient
return null;
var service = GetServiceName(clientTypeName);
- var activity = QylActivitySource.Source.StartActivity("gRPC CLIENT", ActivityKind.Client);
+ var activity = QylActivitySource.StartActivity("gRPC CLIENT", ActivityKind.Client);
if (activity is null)
return null;
@@ -23,10 +28,11 @@ public static class QylInterceptedGrpcNetClient
activity.SetTag(QylSemanticAttributes.RpcSystem, QylSemanticAttributes.RpcSystemGrpc);
activity.SetTag(QylSemanticAttributes.RpcService, service);
activity.SetTag(QylSemanticAttributes.RpcMethod, methodName);
- SetConfiguredMetadata(activity, QylSemanticAttributes.GrpcRequestMetadataPrefix, QylAutoInstrumentationOptions.Current.GrpcNetClientCapturedRequestMetadata, requestMetadata);
+ SetConfiguredMetadata(activity, QylAutoInstrumentationOptions.Current.GrpcNetClientCapturedRequestMetadataMap, requestMetadata);
return activity;
}
+ /// Observes an asynchronous gRPC unary response and records qyl success, exception, and response metadata telemetry.
public static async Task ObserveUnaryResponseAsync(
Task responseTask,
Task responseHeadersTask,
@@ -53,6 +59,7 @@ public static async Task ObserveUnaryResponseAsync(
}
}
+ /// Runs the Observe Response Headers Async runtime helper used by source-generated qyl interceptors.
public static async Task ObserveResponseHeadersAsync(Task responseHeadersTask, Activity? activity)
{
var metadata = await responseHeadersTask.ConfigureAwait(false);
@@ -60,6 +67,7 @@ public static async Task ObserveResponseHeadersAsync(Task re
return metadata;
}
+ /// Runs the Capture Completed Response Headers runtime helper used by source-generated qyl interceptors.
public static void CaptureCompletedResponseHeaders(Task? responseHeadersTask, Activity? activity)
{
if (activity is null || responseHeadersTask is null || !responseHeadersTask.IsCompletedSuccessfully)
@@ -68,12 +76,14 @@ public static void CaptureCompletedResponseHeaders(Task? responseHeade
SetResponseMetadata(activity, responseHeadersTask.Result);
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
activity?.SetStatus(ActivityStatusCode.Error);
}
+ /// Runs the Record Streaming Complete runtime helper used by source-generated qyl interceptors.
public static void RecordStreamingComplete(Activity? activity)
{
if (activity is null)
@@ -83,25 +93,26 @@ public static void RecordStreamingComplete(Activity? activity)
activity.Dispose();
}
+ /// Runs the Dispose runtime helper used by source-generated qyl interceptors.
public static void Dispose(Activity? activity)
=> activity?.Dispose();
private static void SetResponseMetadata(Activity? activity, Metadata? metadata)
- => SetConfiguredMetadata(activity, QylSemanticAttributes.GrpcResponseMetadataPrefix, QylAutoInstrumentationOptions.Current.GrpcNetClientCapturedResponseMetadata, metadata);
+ => SetConfiguredMetadata(activity, QylAutoInstrumentationOptions.Current.GrpcNetClientCapturedResponseMetadataMap, metadata);
- private static void SetConfiguredMetadata(Activity? activity, string prefix, string[] configuredMetadata, Metadata? metadata)
+ private static void SetConfiguredMetadata(Activity? activity, QylCapturedNameMap configuredMetadata, Metadata? metadata)
{
- if (activity is null || metadata is null || configuredMetadata.Length is 0)
+ if (activity is null || metadata is null || configuredMetadata.Count is 0)
return;
- foreach (var metadataName in configuredMetadata)
+ for (var index = 0; index < configuredMetadata.Count; index++)
{
- var normalizedName = NormalizeMetadataName(metadataName);
+ var lookupName = configuredMetadata.GetLookupName(index);
List? values = null;
foreach (var entry in metadata)
{
if (entry.IsBinary ||
- !string.Equals(entry.Key, normalizedName, StringComparison.OrdinalIgnoreCase))
+ !string.Equals(entry.Key, lookupName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
@@ -110,13 +121,10 @@ private static void SetConfiguredMetadata(Activity? activity, string prefix, str
}
if (values is { Count: > 0 })
- activity.SetTag(prefix + normalizedName, values.ToArray());
+ activity.SetTag(configuredMetadata.GetTagName(index), values.Count is 1 ? values[0] : values.ToArray());
}
}
- private static string NormalizeMetadataName(string metadataName)
- => metadataName.Trim().ToLowerInvariant().Replace('_', '-');
-
private static string GetServiceName(string clientTypeName)
{
var lastDot = clientTypeName.LastIndexOf(".", StringComparison.Ordinal);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedHttpClient.cs b/src/Qyl.AutoInstrumentation/QylInterceptedHttpClient.cs
index 1a83e19..ca62eed 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedHttpClient.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedHttpClient.cs
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Net.Http;
+using Qyl.AutoInstrumentation.Internal;
namespace Qyl.AutoInstrumentation;
@@ -11,6 +12,7 @@ namespace Qyl.AutoInstrumentation;
public static class QylInterceptedHttpClient
{
+ /// Runs the Send runtime helper used by source-generated qyl interceptors.
public static HttpResponseMessage Send(HttpClient client, HttpRequestMessage request)
{
ThrowIfInvalidCallTarget(client, request);
@@ -35,6 +37,7 @@ public static HttpResponseMessage Send(HttpClient client, HttpRequestMessage req
}
}
+ /// Runs the Send runtime helper used by source-generated qyl interceptors.
public static HttpResponseMessage Send(HttpClient client, HttpRequestMessage request, CancellationToken cancellationToken)
{
ThrowIfInvalidCallTarget(client, request);
@@ -59,6 +62,7 @@ public static HttpResponseMessage Send(HttpClient client, HttpRequestMessage req
}
}
+ /// Runs the Send runtime helper used by source-generated qyl interceptors.
public static HttpResponseMessage Send(HttpClient client, HttpRequestMessage request, HttpCompletionOption completionOption)
{
ThrowIfInvalidCallTarget(client, request);
@@ -83,6 +87,7 @@ public static HttpResponseMessage Send(HttpClient client, HttpRequestMessage req
}
}
+ /// Runs the Send runtime helper used by source-generated qyl interceptors.
public static HttpResponseMessage Send(HttpClient client, HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
ThrowIfInvalidCallTarget(client, request);
@@ -107,6 +112,7 @@ public static HttpResponseMessage Send(HttpClient client, HttpRequestMessage req
}
}
+ /// Runs the Send Async runtime helper used by source-generated qyl interceptors.
public static Task SendAsync(HttpClient client, HttpRequestMessage request)
{
ThrowIfInvalidCallTarget(client, request);
@@ -115,6 +121,7 @@ public static Task SendAsync(HttpClient client, HttpRequest
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Send Async runtime helper used by source-generated qyl interceptors.
public static Task SendAsync(HttpClient client, HttpRequestMessage request, CancellationToken cancellationToken)
{
ThrowIfInvalidCallTarget(client, request);
@@ -123,6 +130,7 @@ public static Task SendAsync(HttpClient client, HttpRequest
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Send Async runtime helper used by source-generated qyl interceptors.
public static Task SendAsync(HttpClient client, HttpRequestMessage request, HttpCompletionOption completionOption)
{
ThrowIfInvalidCallTarget(client, request);
@@ -131,6 +139,7 @@ public static Task SendAsync(HttpClient client, HttpRequest
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Send Async runtime helper used by source-generated qyl interceptors.
public static Task SendAsync(HttpClient client, HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
ThrowIfInvalidCallTarget(client, request);
@@ -139,6 +148,7 @@ public static Task SendAsync(HttpClient client, HttpRequest
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Async runtime helper used by source-generated qyl interceptors.
public static Task GetAsync(HttpClient client, string? requestUri)
{
ThrowIfNullClient(client);
@@ -147,6 +157,7 @@ public static Task GetAsync(HttpClient client, string? requ
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Async runtime helper used by source-generated qyl interceptors.
public static Task GetAsync(HttpClient client, Uri? requestUri)
{
ThrowIfNullClient(client);
@@ -155,6 +166,7 @@ public static Task GetAsync(HttpClient client, Uri? request
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Async runtime helper used by source-generated qyl interceptors.
public static Task GetAsync(HttpClient client, string? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -163,6 +175,7 @@ public static Task GetAsync(HttpClient client, string? requ
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Async runtime helper used by source-generated qyl interceptors.
public static Task GetAsync(HttpClient client, Uri? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -171,6 +184,7 @@ public static Task GetAsync(HttpClient client, Uri? request
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Async runtime helper used by source-generated qyl interceptors.
public static Task GetAsync(HttpClient client, string? requestUri, HttpCompletionOption completionOption)
{
ThrowIfNullClient(client);
@@ -179,6 +193,7 @@ public static Task GetAsync(HttpClient client, string? requ
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Async runtime helper used by source-generated qyl interceptors.
public static Task GetAsync(HttpClient client, Uri? requestUri, HttpCompletionOption completionOption)
{
ThrowIfNullClient(client);
@@ -187,6 +202,7 @@ public static Task GetAsync(HttpClient client, Uri? request
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Async runtime helper used by source-generated qyl interceptors.
public static Task GetAsync(HttpClient client, string? requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -195,6 +211,7 @@ public static Task GetAsync(HttpClient client, string? requ
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Async runtime helper used by source-generated qyl interceptors.
public static Task GetAsync(HttpClient client, Uri? requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -203,6 +220,7 @@ public static Task GetAsync(HttpClient client, Uri? request
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Post Async runtime helper used by source-generated qyl interceptors.
public static Task PostAsync(HttpClient client, string? requestUri, HttpContent? content)
{
ThrowIfNullClient(client);
@@ -211,6 +229,7 @@ public static Task PostAsync(HttpClient client, string? req
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Post Async runtime helper used by source-generated qyl interceptors.
public static Task PostAsync(HttpClient client, Uri? requestUri, HttpContent? content)
{
ThrowIfNullClient(client);
@@ -219,6 +238,7 @@ public static Task PostAsync(HttpClient client, Uri? reques
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Post Async runtime helper used by source-generated qyl interceptors.
public static Task PostAsync(HttpClient client, string? requestUri, HttpContent? content, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -227,6 +247,7 @@ public static Task PostAsync(HttpClient client, string? req
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Post Async runtime helper used by source-generated qyl interceptors.
public static Task PostAsync(HttpClient client, Uri? requestUri, HttpContent? content, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -235,6 +256,7 @@ public static Task PostAsync(HttpClient client, Uri? reques
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Put Async runtime helper used by source-generated qyl interceptors.
public static Task PutAsync(HttpClient client, string? requestUri, HttpContent? content)
{
ThrowIfNullClient(client);
@@ -243,6 +265,7 @@ public static Task PutAsync(HttpClient client, string? requ
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Put Async runtime helper used by source-generated qyl interceptors.
public static Task PutAsync(HttpClient client, Uri? requestUri, HttpContent? content)
{
ThrowIfNullClient(client);
@@ -251,6 +274,7 @@ public static Task PutAsync(HttpClient client, Uri? request
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Put Async runtime helper used by source-generated qyl interceptors.
public static Task PutAsync(HttpClient client, string? requestUri, HttpContent? content, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -259,6 +283,7 @@ public static Task PutAsync(HttpClient client, string? requ
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Put Async runtime helper used by source-generated qyl interceptors.
public static Task PutAsync(HttpClient client, Uri? requestUri, HttpContent? content, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -267,6 +292,7 @@ public static Task PutAsync(HttpClient client, Uri? request
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Patch Async runtime helper used by source-generated qyl interceptors.
public static Task PatchAsync(HttpClient client, string? requestUri, HttpContent? content)
{
ThrowIfNullClient(client);
@@ -275,6 +301,7 @@ public static Task PatchAsync(HttpClient client, string? re
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Patch Async runtime helper used by source-generated qyl interceptors.
public static Task PatchAsync(HttpClient client, Uri? requestUri, HttpContent? content)
{
ThrowIfNullClient(client);
@@ -283,6 +310,7 @@ public static Task PatchAsync(HttpClient client, Uri? reque
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Patch Async runtime helper used by source-generated qyl interceptors.
public static Task PatchAsync(HttpClient client, string? requestUri, HttpContent? content, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -291,6 +319,7 @@ public static Task PatchAsync(HttpClient client, string? re
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Patch Async runtime helper used by source-generated qyl interceptors.
public static Task PatchAsync(HttpClient client, Uri? requestUri, HttpContent? content, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -299,6 +328,7 @@ public static Task PatchAsync(HttpClient client, Uri? reque
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Delete Async runtime helper used by source-generated qyl interceptors.
public static Task DeleteAsync(HttpClient client, string? requestUri)
{
ThrowIfNullClient(client);
@@ -307,6 +337,7 @@ public static Task DeleteAsync(HttpClient client, string? r
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Delete Async runtime helper used by source-generated qyl interceptors.
public static Task DeleteAsync(HttpClient client, Uri? requestUri)
{
ThrowIfNullClient(client);
@@ -315,6 +346,7 @@ public static Task DeleteAsync(HttpClient client, Uri? requ
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Delete Async runtime helper used by source-generated qyl interceptors.
public static Task DeleteAsync(HttpClient client, string? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -323,6 +355,7 @@ public static Task DeleteAsync(HttpClient client, string? r
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Delete Async runtime helper used by source-generated qyl interceptors.
public static Task DeleteAsync(HttpClient client, Uri? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -331,6 +364,7 @@ public static Task DeleteAsync(HttpClient client, Uri? requ
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get String Async runtime helper used by source-generated qyl interceptors.
public static Task GetStringAsync(HttpClient client, string? requestUri)
{
ThrowIfNullClient(client);
@@ -339,6 +373,7 @@ public static Task GetStringAsync(HttpClient client, string? requestUri)
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get String Async runtime helper used by source-generated qyl interceptors.
public static Task GetStringAsync(HttpClient client, Uri? requestUri)
{
ThrowIfNullClient(client);
@@ -347,6 +382,7 @@ public static Task GetStringAsync(HttpClient client, Uri? requestUri)
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get String Async runtime helper used by source-generated qyl interceptors.
public static Task GetStringAsync(HttpClient client, string? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -355,6 +391,7 @@ public static Task GetStringAsync(HttpClient client, string? requestUri,
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get String Async runtime helper used by source-generated qyl interceptors.
public static Task GetStringAsync(HttpClient client, Uri? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -363,6 +400,7 @@ public static Task GetStringAsync(HttpClient client, Uri? requestUri, Ca
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Byte Array Async runtime helper used by source-generated qyl interceptors.
public static Task GetByteArrayAsync(HttpClient client, string? requestUri)
{
ThrowIfNullClient(client);
@@ -371,6 +409,7 @@ public static Task GetByteArrayAsync(HttpClient client, string? requestU
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Byte Array Async runtime helper used by source-generated qyl interceptors.
public static Task GetByteArrayAsync(HttpClient client, Uri? requestUri)
{
ThrowIfNullClient(client);
@@ -379,6 +418,7 @@ public static Task GetByteArrayAsync(HttpClient client, Uri? requestUri)
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Byte Array Async runtime helper used by source-generated qyl interceptors.
public static Task GetByteArrayAsync(HttpClient client, string? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -387,6 +427,7 @@ public static Task GetByteArrayAsync(HttpClient client, string? requestU
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Byte Array Async runtime helper used by source-generated qyl interceptors.
public static Task GetByteArrayAsync(HttpClient client, Uri? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -395,6 +436,7 @@ public static Task GetByteArrayAsync(HttpClient client, Uri? requestUri,
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Stream Async runtime helper used by source-generated qyl interceptors.
public static Task GetStreamAsync(HttpClient client, string? requestUri)
{
ThrowIfNullClient(client);
@@ -403,6 +445,7 @@ public static Task GetStreamAsync(HttpClient client, string? requestUri)
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Stream Async runtime helper used by source-generated qyl interceptors.
public static Task GetStreamAsync(HttpClient client, Uri? requestUri)
{
ThrowIfNullClient(client);
@@ -411,6 +454,7 @@ public static Task GetStreamAsync(HttpClient client, Uri? requestUri)
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Stream Async runtime helper used by source-generated qyl interceptors.
public static Task GetStreamAsync(HttpClient client, string? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -419,6 +463,7 @@ public static Task GetStreamAsync(HttpClient client, string? requestUri,
catch (Exception exception) { RecordException(observation, exception); observation.Dispose(); throw; }
}
+ /// Runs the Get Stream Async runtime helper used by source-generated qyl interceptors.
public static Task GetStreamAsync(HttpClient client, Uri? requestUri, CancellationToken cancellationToken)
{
ThrowIfNullClient(client);
@@ -428,7 +473,17 @@ public static Task GetStreamAsync(HttpClient client, Uri? requestUri, Ca
}
private static Task ObserveResponseAsync(Task originalTask, HttpClientObservation observation)
- => !observation.IsEnabled ? originalTask : ObserveResponseSlowAsync(originalTask, observation);
+ {
+ if (!observation.IsEnabled)
+ return originalTask;
+
+ if (!originalTask.IsCompletedSuccessfully)
+ return ObserveResponseSlowAsync(originalTask, observation);
+
+ RecordResponse(observation, originalTask.Result);
+ observation.Dispose();
+ return originalTask;
+ }
private static async Task ObserveResponseSlowAsync(Task originalTask, HttpClientObservation observation)
{
@@ -450,7 +505,17 @@ private static async Task ObserveResponseSlowAsync(Task ObserveValueAsync(Task originalTask, HttpClientObservation observation)
- => !observation.IsEnabled ? originalTask : ObserveValueSlowAsync(originalTask, observation);
+ {
+ if (!observation.IsEnabled)
+ return originalTask;
+
+ if (!originalTask.IsCompletedSuccessfully)
+ return ObserveValueSlowAsync(originalTask, observation);
+
+ RecordSuccess(observation);
+ observation.Dispose();
+ return originalTask;
+ }
private static async Task ObserveValueSlowAsync(Task originalTask, HttpClientObservation observation)
{
@@ -475,18 +540,21 @@ private static HttpClientObservation StartHttpClientObservation(HttpRequestMessa
{
var observation = StartHttpClientObservation(request.Method.Method, request.RequestUri, null);
if (observation.Activity is not null)
- SetConfiguredHeaders(observation.Activity, QylSemanticAttributes.HttpRequestHeaderPrefix, QylAutoInstrumentationOptions.Current.HttpClientCapturedRequestHeaders, request.Headers, request.Content?.Headers);
+ SetConfiguredHeaders(observation.Activity, QylAutoInstrumentationOptions.Current.HttpClientCapturedRequestHeaderMap, request.Headers, request.Content?.Headers);
return observation;
}
private static HttpClientObservation StartHttpClientObservation(string method, string? requestUri)
{
+ if (!TryGetHttpClientObservationOptions(out var options, out var traceEnabled, out var metricsEnabled))
+ return default;
+
Uri? uri = null;
if (!string.IsNullOrWhiteSpace(requestUri))
Uri.TryCreate(requestUri, UriKind.RelativeOrAbsolute, out uri);
- return StartHttpClientObservation(method, uri, requestUri);
+ return StartHttpClientObservation(options, traceEnabled, metricsEnabled, method, uri, requestUri);
}
private static HttpClientObservation StartHttpClientObservation(string method, Uri? requestUri)
@@ -494,19 +562,39 @@ private static HttpClientObservation StartHttpClientObservation(string method, U
private static HttpClientObservation StartHttpClientObservation(string method, Uri? requestUri, string? rawRequestUri)
{
- var options = QylAutoInstrumentationOptions.Current;
- var traceEnabled = options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.HttpClient);
- var metricsEnabled = options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.HttpClient);
- if (!traceEnabled && !metricsEnabled)
+ if (!TryGetHttpClientObservationOptions(out var options, out var traceEnabled, out var metricsEnabled))
return default;
+ return StartHttpClientObservation(options, traceEnabled, metricsEnabled, method, requestUri, rawRequestUri);
+ }
+
+ private static bool TryGetHttpClientObservationOptions(
+ out QylAutoInstrumentationOptions options,
+ out bool traceEnabled,
+ out bool metricsEnabled)
+ {
+ options = QylAutoInstrumentationOptions.Current;
+ traceEnabled = QylActivitySource.IsRecordingEnabled &&
+ options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.HttpClient);
+ metricsEnabled = QylHttpClientMetrics.IsRecordingEnabledFor(options);
+ return traceEnabled || metricsEnabled;
+ }
+
+ private static HttpClientObservation StartHttpClientObservation(
+ QylAutoInstrumentationOptions options,
+ bool traceEnabled,
+ bool metricsEnabled,
+ string method,
+ Uri? requestUri,
+ string? rawRequestUri)
+ {
method = QylHttpMethod.Normalize(method);
var startTimeUtc = TimeProvider.System.GetUtcNow().UtcDateTime;
Activity? activity = null;
if (traceEnabled)
{
- activity = QylActivitySource.Source.StartActivity("HTTP client request", ActivityKind.Client);
+ activity = QylActivitySource.StartActivity("HTTP client request", ActivityKind.Client);
if (activity is not null)
{
activity.SetTag(QylSemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.HttpClient);
@@ -542,7 +630,7 @@ private static void RecordResponse(HttpClientObservation observation, HttpRespon
if (activity is not null)
{
activity.SetTag(QylSemanticAttributes.HttpResponseStatusCode, statusCode);
- SetConfiguredHeaders(activity, QylSemanticAttributes.HttpResponseHeaderPrefix, QylAutoInstrumentationOptions.Current.HttpClientCapturedResponseHeaders, response.Headers, response.Content?.Headers);
+ SetConfiguredHeaders(activity, QylAutoInstrumentationOptions.Current.HttpClientCapturedResponseHeaderMap, response.Headers, response.Content?.Headers);
if (statusCode >= 400)
{
@@ -602,26 +690,38 @@ private static string RedactQuery(string url)
: url[..queryStart] + "?Redacted" + url[fragmentStart..];
}
- private static void SetConfiguredHeaders(Activity activity, string prefix, IReadOnlyList configuredHeaders, params System.Net.Http.Headers.HttpHeaders?[] headerSources)
+ private static void SetConfiguredHeaders(Activity activity, QylCapturedNameMap configuredHeaders, params System.Net.Http.Headers.HttpHeaders?[] headerSources)
{
if (configuredHeaders.Count is 0)
return;
- foreach (var headerName in configuredHeaders)
+ for (var index = 0; index < configuredHeaders.Count; index++)
{
+ var lookupName = configuredHeaders.GetLookupName(index);
foreach (var source in headerSources)
{
- if (source is null || !source.TryGetValues(headerName, out var values))
+ if (source is null || !source.TryGetValues(lookupName, out var values))
continue;
- activity.SetTag(prefix + NormalizeHeaderName(headerName), values.ToArray());
+ activity.SetTag(configuredHeaders.GetTagName(index), ToTagValue(values));
break;
}
}
}
- private static string NormalizeHeaderName(string headerName)
- => headerName.Replace('_', '-').ToLower(CultureInfo.InvariantCulture);
+ private static object ToTagValue(IEnumerable values)
+ {
+ if (values is string[] array)
+ return array.Length is 1 ? array[0] : array;
+
+ if (values is IReadOnlyCollection { Count: 1 })
+ {
+ foreach (var value in values)
+ return value;
+ }
+
+ return values.ToArray();
+ }
private readonly record struct HttpClientObservation(
Activity? Activity,
@@ -629,8 +729,10 @@ private readonly record struct HttpClientObservation(
string? Method,
bool RecordMetrics)
{
+ /// Well-known Is Enabled value used by qyl auto-instrumentation.
public bool IsEnabled => Activity is not null || RecordMetrics;
+ /// Runs the Dispose runtime helper used by source-generated qyl interceptors.
public void Dispose()
=> Activity?.Dispose();
}
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedHttpWebRequest.cs b/src/Qyl.AutoInstrumentation/QylInterceptedHttpWebRequest.cs
index f7b896b..36344a8 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedHttpWebRequest.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedHttpWebRequest.cs
@@ -1,14 +1,20 @@
using System.Diagnostics;
using System.Net;
+using Qyl.AutoInstrumentation.Internal;
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted HTTP Web Request.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedHttpWebRequest);
public static class QylInterceptedHttpWebRequest
{
+ /// Runs the Get Start Time Utc runtime helper used by source-generated qyl interceptors.
public static DateTime GetStartTimeUtc()
- => TimeProvider.System.GetUtcNow().UtcDateTime;
+ => QylHttpClientMetrics.IsRecordingEnabled ? TimeProvider.System.GetUtcNow().UtcDateTime : default;
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(HttpWebRequest request, string methodName)
{
var options = QylAutoInstrumentationOptions.Current;
@@ -16,7 +22,7 @@ public static DateTime GetStartTimeUtc()
return null;
var method = QylHttpMethod.Normalize(request.Method);
- var activity = QylActivitySource.Source.StartActivity("HTTP client request", ActivityKind.Client);
+ var activity = QylActivitySource.StartActivity("HTTP client request", ActivityKind.Client);
if (activity is null)
return null;
@@ -41,10 +47,11 @@ public static DateTime GetStartTimeUtc()
}
}
- SetConfiguredHeaders(activity, QylSemanticAttributes.HttpRequestHeaderPrefix, options.HttpClientCapturedRequestHeaders, request.Headers);
+ SetConfiguredHeaders(activity, options.HttpClientCapturedRequestHeaderMap, request.Headers);
return activity;
}
+ /// Runs the Record Result runtime helper used by source-generated qyl interceptors.
public static void RecordResult(Activity? activity, DateTime startTimeUtc, string? method, object? result)
{
int? statusCode = null;
@@ -55,6 +62,7 @@ public static void RecordResult(Activity? activity, DateTime startTimeUtc, strin
RecordDuration(startTimeUtc, method, statusCode);
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, DateTime startTimeUtc, string? method, Exception exception)
{
int? statusCode = null;
@@ -72,7 +80,7 @@ private static int RecordResponse(Activity? activity, HttpWebResponse response,
if (activity is not null)
{
activity.SetTag(QylSemanticAttributes.HttpResponseStatusCode, statusCode);
- SetConfiguredHeaders(activity, QylSemanticAttributes.HttpResponseHeaderPrefix, QylAutoInstrumentationOptions.Current.HttpClientCapturedResponseHeaders, response.Headers);
+ SetConfiguredHeaders(activity, QylAutoInstrumentationOptions.Current.HttpClientCapturedResponseHeaderMap, response.Headers);
if (markErrorForStatus && statusCode >= 400)
{
activity.SetTag(QylSemanticAttributes.ErrorType, statusCode.ToString(System.Globalization.CultureInfo.InvariantCulture));
@@ -85,28 +93,28 @@ private static int RecordResponse(Activity? activity, HttpWebResponse response,
private static void RecordDuration(DateTime startTimeUtc, string? method, int? statusCode)
{
+ if (startTimeUtc == default)
+ return;
+
QylHttpClientMetrics.RecordRequestDuration(
startTimeUtc,
method,
statusCode);
}
- private static void SetConfiguredHeaders(Activity activity, string prefix, string[] configuredHeaders, WebHeaderCollection headers)
+ private static void SetConfiguredHeaders(Activity activity, QylCapturedNameMap configuredHeaders, WebHeaderCollection headers)
{
- if (configuredHeaders.Length is 0)
+ if (configuredHeaders.Count is 0)
return;
- foreach (var headerName in configuredHeaders)
+ for (var index = 0; index < configuredHeaders.Count; index++)
{
- var values = headers.GetValues(headerName);
+ var values = headers.GetValues(configuredHeaders.GetLookupName(index));
if (values is { Length: > 0 })
- activity.SetTag(prefix + NormalizeHeaderName(headerName), values);
+ activity.SetTag(configuredHeaders.GetTagName(index), values.Length is 1 ? values[0] : values);
}
}
- private static string NormalizeHeaderName(string headerName)
- => headerName.Trim().ToLowerInvariant().Replace('_', '-');
-
private static string RedactQuery(string url)
{
var queryStart = url.IndexOf('?', StringComparison.Ordinal);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedKafka.cs b/src/Qyl.AutoInstrumentation/QylInterceptedKafka.cs
index 385cab9..0289b2d 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedKafka.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedKafka.cs
@@ -2,29 +2,37 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Kafka.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedKafka);
public static class QylInterceptedKafka
{
+ /// Runs the Start Producer Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartProducerActivity()
=> StartActivity(
ActivityKind.Producer,
QylSemanticAttributes.MessagingOperationTypeSend,
QylSemanticAttributes.MessagingOperationNamePublish);
+ /// Runs the Start Consumer Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartConsumerActivity()
=> StartActivity(
ActivityKind.Consumer,
QylSemanticAttributes.MessagingOperationTypeReceive,
QylSemanticAttributes.MessagingOperationTypeReceive);
+ /// Runs the Record Consume Success runtime helper used by source-generated qyl interceptors.
public static void RecordConsumeSuccess(Activity? activity)
{
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
@@ -36,7 +44,7 @@ public static void RecordException(Activity? activity, Exception exception)
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.Kafka))
return null;
- var activity = QylActivitySource.Source.StartActivity("Kafka message", activityKind);
+ var activity = QylActivitySource.StartActivity("Kafka message", activityKind);
if (activity is null)
return null;
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedLogger.cs b/src/Qyl.AutoInstrumentation/QylInterceptedLogger.cs
index 4170b35..a7f2a69 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedLogger.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedLogger.cs
@@ -3,9 +3,13 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Logger.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedLogger);
public static class QylInterceptedLogger
{
+ /// Runs the generic Microsoft.Extensions.Logging log helper used by source-generated qyl interceptors.
public static void Log(
ILogger logger,
LogLevel logLevel,
@@ -33,6 +37,7 @@ public static void Log(
}
}
+ /// Runs the Log Extension runtime helper used by source-generated qyl interceptors.
public static void LogExtension(
ILogger logger,
LogLevel logLevel,
@@ -66,7 +71,7 @@ public static void LogExtension(
if (severity is null || !logger.IsEnabled(logLevel))
return null;
- var activity = QylActivitySource.Source.StartActivity("ILogger log", ActivityKind.Internal);
+ var activity = QylActivitySource.StartActivity("ILogger log", ActivityKind.Internal);
if (activity is null)
return null;
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedMassTransit.cs b/src/Qyl.AutoInstrumentation/QylInterceptedMassTransit.cs
index e372ab6..cf35156 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedMassTransit.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedMassTransit.cs
@@ -2,9 +2,13 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Mass Transit.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedMassTransit);
public static class QylInterceptedMassTransit
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string operationName)
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.MassTransit))
@@ -14,7 +18,7 @@ public static class QylInterceptedMassTransit
? QylSemanticAttributes.MessagingOperationNameSend
: QylSemanticAttributes.MessagingOperationNamePublish;
- var activity = QylActivitySource.Source.StartActivity("MassTransit message", ActivityKind.Producer);
+ var activity = QylActivitySource.StartActivity("MassTransit message", ActivityKind.Producer);
if (activity is null)
return null;
@@ -25,10 +29,12 @@ public static class QylInterceptedMassTransit
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedMongoDb.cs b/src/Qyl.AutoInstrumentation/QylInterceptedMongoDb.cs
index 1ad9a53..71bf1a4 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedMongoDb.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedMongoDb.cs
@@ -2,9 +2,13 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Mongo Db.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedMongoDb);
public static class QylInterceptedMongoDb
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string operationName)
{
ArgumentNullException.ThrowIfNull(operationName);
@@ -13,7 +17,7 @@ public static class QylInterceptedMongoDb
return null;
var operation = NormalizeOperation(operationName);
- var activity = QylActivitySource.Source.StartActivity("MongoDB command", ActivityKind.Client);
+ var activity = QylActivitySource.StartActivity("MongoDB command", ActivityKind.Client);
if (activity is null)
return null;
@@ -24,10 +28,12 @@ public static class QylInterceptedMongoDb
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Observe Async runtime helper used by source-generated qyl interceptors.
public static Task ObserveAsync(Task? task, Activity? activity)
{
if (activity is null || task is null)
@@ -39,6 +45,7 @@ public static Task ObserveAsync(Task? task, Activity? activity)
return ObserveSlowAsync(task, activity);
}
+ /// Observes an asynchronous MongoDB command and records qyl success or exception telemetry.
public static Task ObserveAsync(Task? task, Activity? activity)
{
if (activity is null || task is null)
@@ -87,6 +94,7 @@ private static async Task ObserveSlowAsync(Task task, Activity activity
}
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedNServiceBus.cs b/src/Qyl.AutoInstrumentation/QylInterceptedNServiceBus.cs
index e65b70c..59c8400 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedNServiceBus.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedNServiceBus.cs
@@ -2,9 +2,13 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted N Service Bus.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedNServiceBus);
public static class QylInterceptedNServiceBus
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string operationName)
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.NServiceBus))
@@ -14,7 +18,7 @@ public static class QylInterceptedNServiceBus
? QylSemanticAttributes.MessagingOperationNameSend
: QylSemanticAttributes.MessagingOperationNamePublish;
- var activity = QylActivitySource.Source.StartActivity("NServiceBus message", ActivityKind.Producer);
+ var activity = QylActivitySource.StartActivity("NServiceBus message", ActivityKind.Producer);
if (activity is null)
return null;
@@ -25,10 +29,12 @@ public static class QylInterceptedNServiceBus
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedQuartz.cs b/src/Qyl.AutoInstrumentation/QylInterceptedQuartz.cs
index fb0362b..6f2f719 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedQuartz.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedQuartz.cs
@@ -2,15 +2,19 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Quartz.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedQuartz);
public static class QylInterceptedQuartz
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity()
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.Quartz))
return null;
- var activity = QylActivitySource.Source.StartActivity("Quartz execute", ActivityKind.Internal);
+ var activity = QylActivitySource.StartActivity("Quartz execute", ActivityKind.Internal);
if (activity is null)
return null;
@@ -18,10 +22,12 @@ public static class QylInterceptedQuartz
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Observe Async runtime helper used by source-generated qyl interceptors.
public static Task ObserveAsync(Task? task, Activity? activity)
{
if (activity is null || task is null)
@@ -51,6 +57,7 @@ private static async Task ObserveSlowAsync(Task task, Activity activity)
}
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedRabbitMq.cs b/src/Qyl.AutoInstrumentation/QylInterceptedRabbitMq.cs
index 5ddf369..da138b4 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedRabbitMq.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedRabbitMq.cs
@@ -2,15 +2,19 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Rabbit Mq.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedRabbitMq);
public static class QylInterceptedRabbitMq
{
+ /// Runs the Start Publish Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartPublishActivity(string? exchange)
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.RabbitMq))
return null;
- var activity = QylActivitySource.Source.StartActivity("RabbitMQ publish", ActivityKind.Producer);
+ var activity = QylActivitySource.StartActivity("RabbitMQ publish", ActivityKind.Producer);
if (activity is null)
return null;
@@ -22,10 +26,12 @@ public static class QylInterceptedRabbitMq
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedRedis.cs b/src/Qyl.AutoInstrumentation/QylInterceptedRedis.cs
index d13997c..e6abde6 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedRedis.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedRedis.cs
@@ -2,15 +2,19 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Redis.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedRedis);
public static class QylInterceptedRedis
{
+ /// Runs the Start Command Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartCommandActivity(string operationName)
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.StackExchangeRedis))
return null;
- var activity = QylActivitySource.Source.StartActivity("Redis command", ActivityKind.Client);
+ var activity = QylActivitySource.StartActivity("Redis command", ActivityKind.Client);
if (activity is null)
return null;
@@ -22,10 +26,12 @@ public static class QylInterceptedRedis
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedWcfClient.cs b/src/Qyl.AutoInstrumentation/QylInterceptedWcfClient.cs
index 34ac524..2fdc1d4 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedWcfClient.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedWcfClient.cs
@@ -2,15 +2,19 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Wcf Client.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedWcfClient);
public static class QylInterceptedWcfClient
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string clientType, string methodName)
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.WcfClient))
return null;
- var activity = QylActivitySource.Source.StartActivity("WCF CLIENT", ActivityKind.Client);
+ var activity = QylActivitySource.StartActivity("WCF CLIENT", ActivityKind.Client);
if (activity is null)
return null;
@@ -21,10 +25,12 @@ public static class QylInterceptedWcfClient
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylInterceptedWcfCore.cs b/src/Qyl.AutoInstrumentation/QylInterceptedWcfCore.cs
index 86c959b..8cd7f12 100644
--- a/src/Qyl.AutoInstrumentation/QylInterceptedWcfCore.cs
+++ b/src/Qyl.AutoInstrumentation/QylInterceptedWcfCore.cs
@@ -2,15 +2,19 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Intercepted Wcf Core.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylInterceptedWcfCore);
public static class QylInterceptedWcfCore
{
+ /// Runs the Start Activity runtime helper used by source-generated qyl interceptors.
public static Activity? StartActivity(string serviceName, string contractName, string operationName)
{
if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.WcfCore))
return null;
- var activity = QylActivitySource.Source.StartActivity("CoreWCF SERVER", ActivityKind.Server);
+ var activity = QylActivitySource.StartActivity("CoreWCF SERVER", ActivityKind.Server);
if (activity is null)
return null;
@@ -21,10 +25,12 @@ public static class QylInterceptedWcfCore
return activity;
}
+ /// Runs the Record Success runtime helper used by source-generated qyl interceptors.
public static void RecordSuccess(Activity? activity)
{
}
+ /// Runs the Record Exception runtime helper used by source-generated qyl interceptors.
public static void RecordException(Activity? activity, Exception exception)
{
activity?.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name);
diff --git a/src/Qyl.AutoInstrumentation/QylMetricMeters.cs b/src/Qyl.AutoInstrumentation/QylMetricMeters.cs
index 31907d0..edc9f14 100644
--- a/src/Qyl.AutoInstrumentation/QylMetricMeters.cs
+++ b/src/Qyl.AutoInstrumentation/QylMetricMeters.cs
@@ -7,14 +7,22 @@ namespace Qyl.AutoInstrumentation;
///
public static class QylMetricMeters
{
+ /// Well-known ASP.NET Core Components Meter Name value used by qyl auto-instrumentation.
public const string AspNetCoreComponentsMeterName = "Microsoft.AspNetCore.Components";
+ /// Well-known HTTP Client Meter Name value used by qyl auto-instrumentation.
public const string HttpClientMeterName = "System.Net.Http";
+ /// Well-known Database Meter Name value used by qyl auto-instrumentation.
public const string DatabaseMeterName = "Qyl.AutoInstrumentation.Database";
+ /// Well-known Npgsql Meter Name value used by qyl auto-instrumentation.
public const string NpgsqlMeterName = "Npgsql";
+ /// Well-known N Service Bus Meter Name value used by qyl auto-instrumentation.
public const string NServiceBusMeterName = "NServiceBus.Core";
+ /// Well-known Net Runtime Meter Name value used by qyl auto-instrumentation.
public const string NetRuntimeMeterName = "OpenTelemetry.Instrumentation.Runtime";
+ /// Well-known Process Meter Name value used by qyl auto-instrumentation.
public const string ProcessMeterName = "OpenTelemetry.Instrumentation.Process";
+ /// Runs the Get Enabled Meter Names runtime helper used by source-generated qyl interceptors.
public static string[] GetEnabledMeterNames()
{
var options = QylAutoInstrumentationOptions.Current;
diff --git a/src/Qyl.AutoInstrumentation/QylMetricNames.cs b/src/Qyl.AutoInstrumentation/QylMetricNames.cs
index 340ac2e..8d86333 100644
--- a/src/Qyl.AutoInstrumentation/QylMetricNames.cs
+++ b/src/Qyl.AutoInstrumentation/QylMetricNames.cs
@@ -1,23 +1,40 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl Metric Names.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylMetricNames);
public static class QylMetricNames
{
+ /// Well-known ASP.NET Core Components Navigation value used by qyl auto-instrumentation.
public const string AspNetCoreComponentsNavigation = "aspnetcore.components.navigation";
+ /// Well-known database Client Operation Duration value used by qyl auto-instrumentation.
public const string DbClientOperationDuration = "db.client.operation.duration";
+ /// Well-known HTTP Client Request Duration value used by qyl auto-instrumentation.
public const string HttpClientRequestDuration = "http.client.request.duration";
+ /// Well-known N Service Bus Messaging Operation Duration value used by qyl auto-instrumentation.
public const string NServiceBusMessagingOperationDuration = "nservicebus.messaging.operation.duration";
+ /// Well-known Process Cpu Time value used by qyl auto-instrumentation.
public const string ProcessCpuTime = "process.cpu.time";
+ /// Well-known Process Memory Usage value used by qyl auto-instrumentation.
public const string ProcessMemoryUsage = "process.memory.usage";
+ /// Well-known Process Memory Virtual value used by qyl auto-instrumentation.
public const string ProcessMemoryVirtual = "process.memory.virtual";
+ /// Well-known Process Runtime Dotnet Gc Collections Count value used by qyl auto-instrumentation.
public const string ProcessRuntimeDotnetGcCollectionsCount = "process.runtime.dotnet.gc.collections.count";
+ /// Well-known Process Runtime Dotnet Gc Heap Size value used by qyl auto-instrumentation.
public const string ProcessRuntimeDotnetGcHeapSize = "process.runtime.dotnet.gc.heap.size";
+ /// Well-known Process Runtime Dotnet Gc Objects Size value used by qyl auto-instrumentation.
public const string ProcessRuntimeDotnetGcObjectsSize = "process.runtime.dotnet.gc.objects.size";
+ /// Well-known Process Runtime Dotnet Thread Pool Queue Length value used by qyl auto-instrumentation.
public const string ProcessRuntimeDotnetThreadPoolQueueLength = "process.runtime.dotnet.thread_pool.queue.length";
+ /// Well-known Process Runtime Dotnet Thread Pool Threads Count value used by qyl auto-instrumentation.
public const string ProcessRuntimeDotnetThreadPoolThreadsCount = "process.runtime.dotnet.thread_pool.threads.count";
+ /// Well-known qyl Sem Conv Attribute Checks value used by qyl auto-instrumentation.
public const string QylSemConvAttributeChecks = "qyl.semconv.attribute.checks";
+ /// Well-known qyl Sem Conv Processor Failures value used by qyl auto-instrumentation.
public const string QylSemConvProcessorFailures = "qyl.semconv.processor.failures";
}
diff --git a/src/Qyl.AutoInstrumentation/QylNServiceBusMetrics.cs b/src/Qyl.AutoInstrumentation/QylNServiceBusMetrics.cs
index febea41..5f9038a 100644
--- a/src/Qyl.AutoInstrumentation/QylNServiceBusMetrics.cs
+++ b/src/Qyl.AutoInstrumentation/QylNServiceBusMetrics.cs
@@ -2,17 +2,22 @@
namespace Qyl.AutoInstrumentation;
+/// Defines the qyl auto-instrumentation surface for qyl N Service Bus Metrics.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylNServiceBusMetrics);
public static class QylNServiceBusMetrics
{
private static readonly Meter Meter = new(QylMetricMeters.NServiceBusMeterName);
private static readonly Histogram OperationDuration = Meter.CreateHistogram(QylMetricNames.NServiceBusMessagingOperationDuration, "s");
+ /// Runs the Get Timestamp runtime helper used by source-generated qyl interceptors.
public static long GetTimestamp()
- => TimeProvider.System.GetTimestamp();
+ => IsRecordingEnabled ? TimeProvider.System.GetTimestamp() : 0;
+ /// Runs the Record Duration runtime helper used by source-generated qyl interceptors.
public static void RecordDuration(long startTimestamp, string operationName)
{
- if (!QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.NServiceBus))
+ if (startTimestamp is 0 || !IsRecordingEnabled)
return;
var elapsed = TimeProvider.System.GetElapsedTime(startTimestamp);
@@ -26,6 +31,10 @@ public static void RecordDuration(long startTimestamp, string operationName)
}
}
+ internal static bool IsRecordingEnabled
+ => OperationDuration.Enabled &&
+ QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.NServiceBus);
+
private static string NormalizeOperation(string operationName)
=> string.Equals(operationName, "Send", StringComparison.Ordinal)
? QylSemanticAttributes.MessagingOperationNameSend
diff --git a/src/Qyl.AutoInstrumentation/QylSemanticAttributes.cs b/src/Qyl.AutoInstrumentation/QylSemanticAttributes.cs
index 599be5a..2dff08c 100644
--- a/src/Qyl.AutoInstrumentation/QylSemanticAttributes.cs
+++ b/src/Qyl.AutoInstrumentation/QylSemanticAttributes.cs
@@ -13,104 +13,190 @@ namespace Qyl.AutoInstrumentation;
using ServerAttributes = Qyl.OpenTelemetry.SemanticConventions.Attributes.Server.ServerAttributes;
using UrlAttributes = Qyl.OpenTelemetry.SemanticConventions.Attributes.Url.UrlAttributes;
+/// Defines the qyl auto-instrumentation surface for qyl Semantic Attributes.
+/// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery.
+/// var apiType = typeof(QylSemanticAttributes);
public static class QylSemanticAttributes
{
+ /// Well-known qyl Instrumentation Domain value used by qyl auto-instrumentation.
public const string QylInstrumentationDomain = "qyl.instrumentation.domain";
+ /// Well-known qyl Conformance Verdict value used by qyl auto-instrumentation.
public const string QylConformanceVerdict = "qyl.conformance.verdict";
+ /// Well-known HTTP Request Method value used by qyl auto-instrumentation.
public const string HttpRequestMethod = HttpAttributes.RequestMethod;
+ /// Well-known HTTP Request Method Original value used by qyl auto-instrumentation.
public const string HttpRequestMethodOriginal = HttpAttributes.RequestMethodOriginal;
+ /// Well-known HTTP Request Method Other value used by qyl auto-instrumentation.
public const string HttpRequestMethodOther = HttpAttributes.RequestMethodValues.Other;
+ /// Well-known HTTP Request Method Connect value used by qyl auto-instrumentation.
public const string HttpRequestMethodConnect = HttpAttributes.RequestMethodValues.Connect;
+ /// Well-known HTTP Request Method Delete value used by qyl auto-instrumentation.
public const string HttpRequestMethodDelete = HttpAttributes.RequestMethodValues.Delete;
+ /// Well-known HTTP Request Method Get value used by qyl auto-instrumentation.
public const string HttpRequestMethodGet = HttpAttributes.RequestMethodValues.Get;
+ /// Well-known HTTP Request Method Head value used by qyl auto-instrumentation.
public const string HttpRequestMethodHead = HttpAttributes.RequestMethodValues.Head;
+ /// Well-known HTTP Request Method Options value used by qyl auto-instrumentation.
public const string HttpRequestMethodOptions = HttpAttributes.RequestMethodValues.Options;
+ /// Well-known HTTP Request Method Patch value used by qyl auto-instrumentation.
public const string HttpRequestMethodPatch = HttpAttributes.RequestMethodValues.Patch;
+ /// Well-known HTTP Request Method Post value used by qyl auto-instrumentation.
public const string HttpRequestMethodPost = HttpAttributes.RequestMethodValues.Post;
+ /// Well-known HTTP Request Method Put value used by qyl auto-instrumentation.
public const string HttpRequestMethodPut = HttpAttributes.RequestMethodValues.Put;
+ /// Well-known HTTP Request Method Trace value used by qyl auto-instrumentation.
public const string HttpRequestMethodTrace = HttpAttributes.RequestMethodValues.Trace;
+ /// Well-known HTTP Response Status Code value used by qyl auto-instrumentation.
public const string HttpResponseStatusCode = HttpAttributes.ResponseStatusCode;
+ /// Well-known HTTP Request Header Prefix value used by qyl auto-instrumentation.
public const string HttpRequestHeaderPrefix = HttpAttributes.RequestHeader + ".";
+ /// Well-known HTTP Response Header Prefix value used by qyl auto-instrumentation.
public const string HttpResponseHeaderPrefix = HttpAttributes.ResponseHeader + ".";
+ /// Well-known HTTP Route value used by qyl auto-instrumentation.
public const string HttpRoute = HttpAttributes.Route;
+ /// Well-known Url Path value used by qyl auto-instrumentation.
public const string UrlPath = UrlAttributes.Path;
+ /// Well-known Url Query value used by qyl auto-instrumentation.
public const string UrlQuery = UrlAttributes.Query;
+ /// Well-known Url Full value used by qyl auto-instrumentation.
public const string UrlFull = UrlAttributes.Full;
+ /// Well-known Dotnet Gc Heap Generation value used by qyl auto-instrumentation.
public const string DotnetGcHeapGeneration = DotnetAttributes.GcHeapGeneration;
+ /// Well-known Dotnet Gc Heap Generation Gen0 value used by qyl auto-instrumentation.
public const string DotnetGcHeapGenerationGen0 = DotnetAttributes.GcHeapGenerationValues.Gen0;
+ /// Well-known Dotnet Gc Heap Generation Gen1 value used by qyl auto-instrumentation.
public const string DotnetGcHeapGenerationGen1 = DotnetAttributes.GcHeapGenerationValues.Gen1;
+ /// Well-known Dotnet Gc Heap Generation Gen2 value used by qyl auto-instrumentation.
public const string DotnetGcHeapGenerationGen2 = DotnetAttributes.GcHeapGenerationValues.Gen2;
+ /// Well-known Cpu Mode value used by qyl auto-instrumentation.
public const string CpuMode = CpuAttributes.Mode;
+ /// Well-known Cpu Mode System value used by qyl auto-instrumentation.
public const string CpuModeSystem = CpuAttributes.ModeValues.System;
+ /// Well-known Cpu Mode User value used by qyl auto-instrumentation.
public const string CpuModeUser = CpuAttributes.ModeValues.User;
+ /// Well-known database System Name value used by qyl auto-instrumentation.
public const string DbSystemName = DbAttributes.SystemName;
+ /// Well-known database Namespace value used by qyl auto-instrumentation.
public const string DbNamespace = DbAttributes.Namespace;
+ /// Well-known database Operation Name value used by qyl auto-instrumentation.
public const string DbOperationName = DbAttributes.OperationName;
+ /// Well-known database Operation Name Get value used by qyl auto-instrumentation.
public const string DbOperationNameGet = "GET";
+ /// Well-known database Query Summary value used by qyl auto-instrumentation.
public const string DbQuerySummary = DbAttributes.QuerySummary;
+ /// Well-known database Query Text value used by qyl auto-instrumentation.
public const string DbQueryText = DbAttributes.QueryText;
+ /// Well-known database System Elasticsearch value used by qyl auto-instrumentation.
public const string DbSystemElasticsearch = DbAttributes.SystemNameValues.Elasticsearch;
+ /// Well-known database System Microsoft Sql Server value used by qyl auto-instrumentation.
public const string DbSystemMicrosoftSqlServer = DbAttributes.SystemNameValues.MicrosoftSqlServer;
+ /// Well-known database System Mongodb value used by qyl auto-instrumentation.
public const string DbSystemMongodb = DbAttributes.SystemNameValues.Mongodb;
+ /// Well-known database System Mysql value used by qyl auto-instrumentation.
public const string DbSystemMysql = DbAttributes.SystemNameValues.Mysql;
+ /// Well-known database System Oracle Db value used by qyl auto-instrumentation.
public const string DbSystemOracleDb = DbAttributes.SystemNameValues.OracleDb;
+ /// Well-known database System Other Sql value used by qyl auto-instrumentation.
public const string DbSystemOtherSql = DbAttributes.SystemNameValues.OtherSql;
+ /// Well-known database System Postgresql value used by qyl auto-instrumentation.
public const string DbSystemPostgresql = DbAttributes.SystemNameValues.Postgresql;
+ /// Well-known database System Redis value used by qyl auto-instrumentation.
public const string DbSystemRedis = DbAttributes.SystemNameValues.Redis;
+ /// Well-known database System Sqlite value used by qyl auto-instrumentation.
public const string DbSystemSqlite = DbAttributes.SystemNameValues.Sqlite;
+ /// Well-known Rpc System value used by qyl auto-instrumentation.
public const string RpcSystem = RpcAttributes.SystemName;
+ /// Well-known Rpc System Grpc value used by qyl auto-instrumentation.
public const string RpcSystemGrpc = RpcAttributes.SystemNameValues.Grpc;
#pragma warning disable CS0618 // DotnetWcf exists only on the deprecated value set in the current semconv package.
+ /// Well-known Rpc System Dot Net Wcf value used by qyl auto-instrumentation.
public const string RpcSystemDotNetWcf = RpcAttributes.SystemValues.DotnetWcf;
#pragma warning restore CS0618
+ /// Well-known Rpc System Azure value used by qyl auto-instrumentation.
public const string RpcSystemAzure = "azure";
+ /// Well-known Rpc System Quartz value used by qyl auto-instrumentation.
public const string RpcSystemQuartz = "quartz";
#pragma warning disable CS0618 // Qyl still mirrors the current OTEL .NET auto gRPC status attribute contract.
+ /// Well-known Rpc Service value used by qyl auto-instrumentation.
public const string RpcService = RpcAttributes.Service;
+ /// Well-known Rpc Method value used by qyl auto-instrumentation.
public const string RpcMethod = RpcAttributes.Method;
+ /// Well-known Rpc Method Execute value used by qyl auto-instrumentation.
public const string RpcMethodExecute = "Execute";
+ /// Well-known Rpc gRPC Status Code value used by qyl auto-instrumentation.
public const string RpcGrpcStatusCode = RpcAttributes.GrpcStatusCode;
+ /// Well-known Rpc gRPC Status Code Ok value used by qyl auto-instrumentation.
public static readonly int RpcGrpcStatusCodeOk = GetRpcGrpcStatusCodeOk();
#pragma warning restore CS0618
+ /// Well-known gRPC Request Metadata Prefix value used by qyl auto-instrumentation.
public const string GrpcRequestMetadataPrefix = RpcAttributes.RequestMetadata + ".";
+ /// Well-known gRPC Response Metadata Prefix value used by qyl auto-instrumentation.
public const string GrpcResponseMetadataPrefix = RpcAttributes.ResponseMetadata + ".";
+ /// Well-known Messaging System value used by qyl auto-instrumentation.
public const string MessagingSystem = MessagingAttributes.System;
+ /// Well-known Messaging Operation Name value used by qyl auto-instrumentation.
public const string MessagingOperationName = MessagingAttributes.OperationName;
+ /// Well-known Messaging Operation Name Publish value used by qyl auto-instrumentation.
public const string MessagingOperationNamePublish = "publish";
+ /// Well-known Messaging Operation Name Send value used by qyl auto-instrumentation.
public const string MessagingOperationNameSend = MessagingAttributes.OperationTypeValues.Send;
+ /// Well-known Messaging Operation Type value used by qyl auto-instrumentation.
public const string MessagingOperationType = MessagingAttributes.OperationType;
+ /// Well-known Messaging Operation Type Receive value used by qyl auto-instrumentation.
public const string MessagingOperationTypeReceive = MessagingAttributes.OperationTypeValues.Receive;
+ /// Well-known Messaging Operation Type Send value used by qyl auto-instrumentation.
public const string MessagingOperationTypeSend = MessagingAttributes.OperationTypeValues.Send;
+ /// Well-known Messaging Destination Name value used by qyl auto-instrumentation.
public const string MessagingDestinationName = MessagingAttributes.DestinationName;
+ /// Well-known Messaging System Kafka value used by qyl auto-instrumentation.
public const string MessagingSystemKafka = MessagingAttributes.SystemValues.Kafka;
+ /// Well-known Messaging System Rabbit Mq value used by qyl auto-instrumentation.
public const string MessagingSystemRabbitMq = MessagingAttributes.SystemValues.Rabbitmq;
+ /// Well-known Messaging System Mass Transit value used by qyl auto-instrumentation.
public const string MessagingSystemMassTransit = "masstransit";
+ /// Well-known Messaging System N Service Bus value used by qyl auto-instrumentation.
public const string MessagingSystemNServiceBus = "nservicebus";
+ /// Well-known Log Severity value used by qyl auto-instrumentation.
public const string LogSeverity = "log.severity";
+ /// Well-known Log Severity Trace value used by qyl auto-instrumentation.
public const string LogSeverityTrace = "Trace";
+ /// Well-known Log Severity Debug value used by qyl auto-instrumentation.
public const string LogSeverityDebug = "Debug";
+ /// Well-known Log Severity Information value used by qyl auto-instrumentation.
public const string LogSeverityInformation = "Information";
+ /// Well-known Log Severity Warning value used by qyl auto-instrumentation.
public const string LogSeverityWarning = "Warning";
+ /// Well-known Log Severity Error value used by qyl auto-instrumentation.
public const string LogSeverityError = "Error";
+ /// Well-known Log Severity Critical value used by qyl auto-instrumentation.
public const string LogSeverityCritical = "Critical";
+ /// Well-known Log Severity None value used by qyl auto-instrumentation.
public const string LogSeverityNone = "None";
+ /// Well-known Log Severity Other value used by qyl auto-instrumentation.
public const string LogSeverityOther = "Other";
+ /// Well-known Log Event Name value used by qyl auto-instrumentation.
public const string LogEventName = OtelAttributes.EventName;
+ /// Well-known Graph Ql Operation Name value used by qyl auto-instrumentation.
public const string GraphQlOperationName = GraphqlAttributes.OperationName;
+ /// Well-known Graph Ql Document value used by qyl auto-instrumentation.
public const string GraphQlDocument = GraphqlAttributes.Document;
+ /// Well-known Server Address value used by qyl auto-instrumentation.
public const string ServerAddress = ServerAttributes.Address;
+ /// Well-known Server Port value used by qyl auto-instrumentation.
public const string ServerPort = ServerAttributes.Port;
+ /// Well-known Error Type value used by qyl auto-instrumentation.
public const string ErrorType = ErrorAttributes.Type;
+ /// Well-known Exception Type value used by qyl auto-instrumentation.
public const string ExceptionType = ExceptionAttributes.Type;
private static int GetRpcGrpcStatusCodeOk()
diff --git a/src/Qyl.AutoInstrumentation/build/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs b/src/Qyl.AutoInstrumentation/build/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs
new file mode 100644
index 0000000..f50be4a
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation/build/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs
@@ -0,0 +1,16 @@
+//
+namespace System.Runtime.CompilerServices;
+
+[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)]
+internal sealed class InterceptsLocationAttribute : global::System.Attribute
+{
+ public InterceptsLocationAttribute(int version, string data)
+ {
+ Version = version;
+ Data = data;
+ }
+
+ public int Version { get; }
+
+ public string Data { get; }
+}
diff --git a/src/Qyl.AutoInstrumentation/build/Qyl.AutoInstrumentation.targets b/src/Qyl.AutoInstrumentation/build/Qyl.AutoInstrumentation.targets
new file mode 100644
index 0000000..082909c
--- /dev/null
+++ b/src/Qyl.AutoInstrumentation/build/Qyl.AutoInstrumentation.targets
@@ -0,0 +1,17 @@
+
+
+ <_QylAutoInstrumentationCoreBuildAssetsAlreadyImported>$(QylAutoInstrumentationCoreBuildAssetsImported)
+ true
+
+
+
+ $(InterceptorsNamespaces);Qyl.AutoInstrumentation.Generated
+ $(InterceptorsPreviewNamespaces);Qyl.AutoInstrumentation.Generated
+
+
+
+
+
+
diff --git a/src/Qyl.AutoInstrumentation/buildTransitive/Qyl.AutoInstrumentation.targets b/src/Qyl.AutoInstrumentation/buildTransitive/Qyl.AutoInstrumentation.targets
index 272d1d4..082909c 100644
--- a/src/Qyl.AutoInstrumentation/buildTransitive/Qyl.AutoInstrumentation.targets
+++ b/src/Qyl.AutoInstrumentation/buildTransitive/Qyl.AutoInstrumentation.targets
@@ -1,10 +1,15 @@
+ <_QylAutoInstrumentationCoreBuildAssetsAlreadyImported>$(QylAutoInstrumentationCoreBuildAssetsImported)
+ true
+
+
+
$(InterceptorsNamespaces);Qyl.AutoInstrumentation.Generated
$(InterceptorsPreviewNamespaces);Qyl.AutoInstrumentation.Generated
-
+
diff --git a/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/Fixture/Program.cs b/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/Fixture/Program.cs
new file mode 100644
index 0000000..5e773cd
--- /dev/null
+++ b/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/Fixture/Program.cs
@@ -0,0 +1,30 @@
+using Microsoft.Extensions.Logging;
+
+ILogger logger = new SnapshotLogger();
+
+logger.Log(
+ LogLevel.Warning,
+ new EventId(42, "snapshot-log"),
+ "snapshot-state",
+ exception: null,
+ static (state, exception) => exception is null ? state : state + ":" + exception.GetType().Name);
+
+return 0;
+
+internal sealed class SnapshotLogger : ILogger
+{
+ public IDisposable? BeginScope(TState state)
+ where TState : notnull
+ => null;
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(
+ LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception? exception,
+ Func formatter)
+ {
+ }
+}
diff --git a/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/Fixture/Qyl.AutoInstrumentation.SourceGenerators.Snapshots.Fixture.csproj b/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/Fixture/Qyl.AutoInstrumentation.SourceGenerators.Snapshots.Fixture.csproj
new file mode 100644
index 0000000..700d0d6
--- /dev/null
+++ b/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/Fixture/Qyl.AutoInstrumentation.SourceGenerators.Snapshots.Fixture.csproj
@@ -0,0 +1,22 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ true
+ $(BaseIntermediateOutputPath)generated
+ $(MSBuildProjectDirectory)=/_qyl_generator_snapshot
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.Interceptors.g.verified.cs b/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.Interceptors.g.verified.cs
new file mode 100644
index 0000000..ad956fd
--- /dev/null
+++ b/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.Interceptors.g.verified.cs
@@ -0,0 +1,20 @@
+//
+#nullable enable
+#pragma warning disable
+namespace Qyl.AutoInstrumentation.Generated
+{
+ internal static class QylGeneratedInterceptors
+ {
+ // Intercepted call at /_qyl_generator_snapshot/Program.cs(5,8)
+ [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "OrHTaGeKBKDZi8Kz3ulm2VQAAABQcm9ncmFtLmNz")]
+ public static void ILogger_Log_0(
+ this global::Microsoft.Extensions.Logging.ILogger logger,
+ global::Microsoft.Extensions.Logging.LogLevel logLevel,
+ global::Microsoft.Extensions.Logging.EventId eventId,
+ TState state,
+ global::System.Exception? exception,
+ global::System.Func formatter)
+ => global::Qyl.AutoInstrumentation.QylInterceptedLogger.Log(logger, logLevel, eventId, state, exception, formatter);
+
+ }
+}
diff --git a/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylGeneratedInstrumentationContract.g.verified.cs b/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylGeneratedInstrumentationContract.g.verified.cs
new file mode 100644
index 0000000..aad906c
--- /dev/null
+++ b/tests/Qyl.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylGeneratedInstrumentationContract.g.verified.cs
@@ -0,0 +1,200 @@
+//
+namespace Qyl.AutoInstrumentation.Generated;
+
+internal static class QylGeneratedInstrumentationContract
+{
+ public const int SignalSpecificInstrumentationPromiseCount = 37;
+ public const int GlobalEnvironmentControlCount = 7;
+ public const int InstrumentationOptionCount = 16;
+ public const int TotalCount = 60;
+ public const int TracesSignalSpecificPromiseCount = 26;
+ public const int MetricsSignalSpecificPromiseCount = 8;
+ public const int LogsSignalSpecificPromiseCount = 3;
+ public const int UniqueInstrumentationIdCount = 31;
+ public const int UnsupportedNativeAotSignalPromiseCount = 4;
+ public const int SourceGeneratedSignalPromiseCount = 33;
+ public const string AspNetCoreComponentsMeterName = "Microsoft.AspNetCore.Components";
+ public const string AspNetCoreComponentsNavigationMetricName = "aspnetcore.components.navigation";
+
+ public static string[] ItemIds => new[]
+ {
+ "signals.traces.ADONET",
+ "signals.traces.ASPNET",
+ "signals.traces.ASPNETCORE",
+ "signals.traces.AZURE",
+ "signals.traces.ELASTICSEARCH",
+ "signals.traces.ELASTICTRANSPORT",
+ "signals.traces.ENTITYFRAMEWORKCORE",
+ "signals.traces.GRAPHQL",
+ "signals.traces.GRPCNETCLIENT",
+ "signals.traces.HTTPCLIENT",
+ "signals.traces.KAFKA",
+ "signals.traces.MASSTRANSIT",
+ "signals.traces.MONGODB",
+ "signals.traces.MYSQLCONNECTOR",
+ "signals.traces.MYSQLDATA",
+ "signals.traces.NPGSQL",
+ "signals.traces.NSERVICEBUS",
+ "signals.traces.ORACLEMDA",
+ "signals.traces.RABBITMQ",
+ "signals.traces.QUARTZ",
+ "signals.traces.SQLCLIENT",
+ "signals.traces.SQLITE",
+ "signals.traces.STACKEXCHANGEREDIS",
+ "signals.traces.WCFCLIENT",
+ "signals.traces.WCFCORE",
+ "signals.traces.WCFSERVICE",
+ "signals.metrics.ASPNET",
+ "signals.metrics.ASPNETCORE",
+ "signals.metrics.HTTPCLIENT",
+ "signals.metrics.NETRUNTIME",
+ "signals.metrics.NPGSQL",
+ "signals.metrics.NSERVICEBUS",
+ "signals.metrics.PROCESS",
+ "signals.metrics.SQLCLIENT",
+ "signals.logs.ILOGGER",
+ "signals.logs.LOG4NET",
+ "signals.logs.NLOG",
+ "global_environment_controls.OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED",
+ "global_environment_controls.OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED",
+ "global_environment_controls.OTEL_DOTNET_AUTO_TRACES_{0}_INSTRUMENTATION_ENABLED",
+ "global_environment_controls.OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED",
+ "global_environment_controls.OTEL_DOTNET_AUTO_METRICS_{0}_INSTRUMENTATION_ENABLED",
+ "global_environment_controls.OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED",
+ "global_environment_controls.OTEL_DOTNET_AUTO_LOGS_{0}_INSTRUMENTATION_ENABLED",
+ "instrumentation_options.OTEL_DOTNET_AUTO_ENTITYFRAMEWORKCORE_SET_DBSTATEMENT_FOR_TEXT",
+ "instrumentation_options.OTEL_DOTNET_AUTO_GRAPHQL_SET_DOCUMENT",
+ "instrumentation_options.OTEL_DOTNET_AUTO_ORACLEMDA_SET_DBSTATEMENT_FOR_TEXT",
+ "instrumentation_options.OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT",
+ "instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS",
+ "instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS",
+ "instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS",
+ "instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS",
+ "instrumentation_options.OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_REQUEST_METADATA",
+ "instrumentation_options.OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_RESPONSE_METADATA",
+ "instrumentation_options.OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS",
+ "instrumentation_options.OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS",
+ "instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION",
+ "instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION",
+ "instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_ASPNET_DISABLE_URL_QUERY_REDACTION",
+ "instrumentation_options.OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED",
+ };
+
+ public static string[] SignalKeys => new[]
+ {
+ "signals.traces.ADONET",
+ "signals.traces.ASPNET",
+ "signals.traces.ASPNETCORE",
+ "signals.traces.AZURE",
+ "signals.traces.ELASTICSEARCH",
+ "signals.traces.ELASTICTRANSPORT",
+ "signals.traces.ENTITYFRAMEWORKCORE",
+ "signals.traces.GRAPHQL",
+ "signals.traces.GRPCNETCLIENT",
+ "signals.traces.HTTPCLIENT",
+ "signals.traces.KAFKA",
+ "signals.traces.MASSTRANSIT",
+ "signals.traces.MONGODB",
+ "signals.traces.MYSQLCONNECTOR",
+ "signals.traces.MYSQLDATA",
+ "signals.traces.NPGSQL",
+ "signals.traces.NSERVICEBUS",
+ "signals.traces.ORACLEMDA",
+ "signals.traces.RABBITMQ",
+ "signals.traces.QUARTZ",
+ "signals.traces.SQLCLIENT",
+ "signals.traces.SQLITE",
+ "signals.traces.STACKEXCHANGEREDIS",
+ "signals.traces.WCFCLIENT",
+ "signals.traces.WCFCORE",
+ "signals.traces.WCFSERVICE",
+ "signals.metrics.ASPNET",
+ "signals.metrics.ASPNETCORE",
+ "signals.metrics.HTTPCLIENT",
+ "signals.metrics.NETRUNTIME",
+ "signals.metrics.NPGSQL",
+ "signals.metrics.NSERVICEBUS",
+ "signals.metrics.PROCESS",
+ "signals.metrics.SQLCLIENT",
+ "signals.logs.ILOGGER",
+ "signals.logs.LOG4NET",
+ "signals.logs.NLOG",
+ };
+
+ public static string[] SourceGeneratedSignalKeys => new[]
+ {
+ "signals.traces.ADONET",
+ "signals.traces.ASPNETCORE",
+ "signals.traces.AZURE",
+ "signals.traces.ELASTICSEARCH",
+ "signals.traces.ELASTICTRANSPORT",
+ "signals.traces.ENTITYFRAMEWORKCORE",
+ "signals.traces.GRAPHQL",
+ "signals.traces.GRPCNETCLIENT",
+ "signals.traces.HTTPCLIENT",
+ "signals.traces.KAFKA",
+ "signals.traces.MASSTRANSIT",
+ "signals.traces.MONGODB",
+ "signals.traces.MYSQLCONNECTOR",
+ "signals.traces.MYSQLDATA",
+ "signals.traces.NPGSQL",
+ "signals.traces.NSERVICEBUS",
+ "signals.traces.ORACLEMDA",
+ "signals.traces.RABBITMQ",
+ "signals.traces.QUARTZ",
+ "signals.traces.SQLCLIENT",
+ "signals.traces.SQLITE",
+ "signals.traces.STACKEXCHANGEREDIS",
+ "signals.traces.WCFCLIENT",
+ "signals.metrics.ASPNETCORE",
+ "signals.metrics.HTTPCLIENT",
+ "signals.metrics.NETRUNTIME",
+ "signals.metrics.NPGSQL",
+ "signals.metrics.NSERVICEBUS",
+ "signals.metrics.PROCESS",
+ "signals.metrics.SQLCLIENT",
+ "signals.logs.ILOGGER",
+ "signals.logs.LOG4NET",
+ "signals.logs.NLOG",
+ };
+
+ public static string[] UnsupportedNativeAotSignalKeys => new[]
+ {
+ "signals.traces.ASPNET",
+ "signals.traces.WCFCORE",
+ "signals.traces.WCFSERVICE",
+ "signals.metrics.ASPNET",
+ };
+
+ public static string[] GlobalEnvironmentControls => new[]
+ {
+ "OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED",
+ "OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED",
+ "OTEL_DOTNET_AUTO_TRACES_{0}_INSTRUMENTATION_ENABLED",
+ "OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED",
+ "OTEL_DOTNET_AUTO_METRICS_{0}_INSTRUMENTATION_ENABLED",
+ "OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED",
+ "OTEL_DOTNET_AUTO_LOGS_{0}_INSTRUMENTATION_ENABLED",
+ };
+
+ public static string[] InstrumentationOptions => new[]
+ {
+ "OTEL_DOTNET_AUTO_ENTITYFRAMEWORKCORE_SET_DBSTATEMENT_FOR_TEXT",
+ "OTEL_DOTNET_AUTO_GRAPHQL_SET_DOCUMENT",
+ "OTEL_DOTNET_AUTO_ORACLEMDA_SET_DBSTATEMENT_FOR_TEXT",
+ "OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT",
+ "OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS",
+ "OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS",
+ "OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS",
+ "OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS",
+ "OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_REQUEST_METADATA",
+ "OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_RESPONSE_METADATA",
+ "OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS",
+ "OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS",
+ "OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION",
+ "OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION",
+ "OTEL_DOTNET_EXPERIMENTAL_ASPNET_DISABLE_URL_QUERY_REDACTION",
+ "OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED",
+ };
+
+}
diff --git a/tools/Qyl.AutoInstrumentation.OtlpCollectorFixtures/golden/httpclient-traces.collector.json b/tools/Qyl.AutoInstrumentation.OtlpCollectorFixtures/golden/httpclient-traces.collector.json
new file mode 100644
index 0000000..17945b2
--- /dev/null
+++ b/tools/Qyl.AutoInstrumentation.OtlpCollectorFixtures/golden/httpclient-traces.collector.json
@@ -0,0 +1,20 @@
+{
+ "forbiddenStrings": [],
+ "matchedStrings": [
+ "GET",
+ "HTTP client request",
+ "Qyl.AutoInstrumentation",
+ "downstream.example",
+ "http.client",
+ "http.request.method",
+ "http.response.status_code",
+ "qyl.instrumentation.domain",
+ "server.address"
+ ],
+ "request": {
+ "contentType": "application/x-protobuf",
+ "method": "POST",
+ "path": "/v1/traces"
+ },
+ "wireFormat": "otlp-http-protobuf"
+}
diff --git a/tools/Qyl.AutoInstrumentation.OtlpGoldenFixtures/golden/webapi-aot-traces.otlp.json b/tools/Qyl.AutoInstrumentation.OtlpGoldenFixtures/golden/webapi-aot-traces.otlp.json
new file mode 100644
index 0000000..60fc2ec
--- /dev/null
+++ b/tools/Qyl.AutoInstrumentation.OtlpGoldenFixtures/golden/webapi-aot-traces.otlp.json
@@ -0,0 +1,246 @@
+{
+ "resourceSpans": [
+ {
+ "resource": {
+ "attributes": [
+ {
+ "key": "service.name",
+ "value": {
+ "stringValue": "qyl-webapi-aot-demo"
+ }
+ },
+ {
+ "key": "telemetry.sdk.language",
+ "value": {
+ "stringValue": "dotnet"
+ }
+ }
+ ]
+ },
+ "scopeSpans": [
+ {
+ "scope": {
+ "name": "Qyl.AutoInstrumentation",
+ "version": "0.3.0-pre.1"
+ },
+ "spans": [
+ {
+ "attributes": [
+ {
+ "key": "qyl.fixture.signal",
+ "value": {
+ "stringValue": "aspnetcore.server"
+ }
+ },
+ {
+ "key": "http.request.method",
+ "value": {
+ "stringValue": "GET"
+ }
+ },
+ {
+ "key": "http.response.status_code",
+ "value": {
+ "intValue": 204
+ }
+ },
+ {
+ "key": "http.route",
+ "value": {
+ "stringValue": "/probe/{id:int}"
+ }
+ },
+ {
+ "key": "qyl.instrumentation.domain",
+ "value": {
+ "stringValue": "http.server"
+ }
+ }
+ ],
+ "endTimeUnixNano": "0",
+ "kind": "SPAN_KIND_SERVER",
+ "name": "HTTP server request",
+ "parentSpanId": "",
+ "spanId": "0000000000000001",
+ "startTimeUnixNano": "0",
+ "status": {
+ "code": "STATUS_CODE_UNSET"
+ },
+ "traceId": "00000000000000000000000000000001"
+ },
+ {
+ "attributes": [
+ {
+ "key": "qyl.fixture.signal",
+ "value": {
+ "stringValue": "efcore.sqlite"
+ }
+ },
+ {
+ "key": "db.operation.name",
+ "value": {
+ "stringValue": "INSERT"
+ }
+ },
+ {
+ "key": "db.query.summary",
+ "value": {
+ "stringValue": "ExecuteSqlRaw INSERT"
+ }
+ },
+ {
+ "key": "qyl.instrumentation.domain",
+ "value": {
+ "stringValue": "db.efcore"
+ }
+ }
+ ],
+ "endTimeUnixNano": "0",
+ "kind": "SPAN_KIND_CLIENT",
+ "name": "EF Core operation",
+ "parentSpanId": "",
+ "spanId": "0000000000000002",
+ "startTimeUnixNano": "0",
+ "status": {
+ "code": "STATUS_CODE_UNSET"
+ },
+ "traceId": "00000000000000000000000000000002"
+ },
+ {
+ "attributes": [
+ {
+ "key": "qyl.fixture.signal",
+ "value": {
+ "stringValue": "httpclient.downstream"
+ }
+ },
+ {
+ "key": "http.request.method",
+ "value": {
+ "stringValue": "GET"
+ }
+ },
+ {
+ "key": "http.response.status_code",
+ "value": {
+ "intValue": 204
+ }
+ },
+ {
+ "key": "qyl.instrumentation.domain",
+ "value": {
+ "stringValue": "http.client"
+ }
+ }
+ ],
+ "endTimeUnixNano": "0",
+ "kind": "SPAN_KIND_CLIENT",
+ "name": "HTTP client request",
+ "parentSpanId": "",
+ "spanId": "0000000000000003",
+ "startTimeUnixNano": "0",
+ "status": {
+ "code": "STATUS_CODE_UNSET"
+ },
+ "traceId": "00000000000000000000000000000003"
+ },
+ {
+ "attributes": [
+ {
+ "key": "qyl.fixture.signal",
+ "value": {
+ "stringValue": "httpclient.self"
+ }
+ },
+ {
+ "key": "http.request.method",
+ "value": {
+ "stringValue": "GET"
+ }
+ },
+ {
+ "key": "http.response.status_code",
+ "value": {
+ "intValue": 204
+ }
+ },
+ {
+ "key": "qyl.instrumentation.domain",
+ "value": {
+ "stringValue": "http.client"
+ }
+ },
+ {
+ "key": "server.address",
+ "value": {
+ "stringValue": "127.0.0.1"
+ }
+ },
+ {
+ "key": "server.port",
+ "value": {
+ "stringValue": ""
+ }
+ }
+ ],
+ "endTimeUnixNano": "0",
+ "kind": "SPAN_KIND_CLIENT",
+ "name": "HTTP client request",
+ "parentSpanId": "",
+ "spanId": "0000000000000004",
+ "startTimeUnixNano": "0",
+ "status": {
+ "code": "STATUS_CODE_UNSET"
+ },
+ "traceId": "00000000000000000000000000000004"
+ },
+ {
+ "attributes": [
+ {
+ "key": "qyl.fixture.signal",
+ "value": {
+ "stringValue": "sqlclient.command"
+ }
+ },
+ {
+ "key": "db.operation.name",
+ "value": {
+ "stringValue": "SELECT"
+ }
+ },
+ {
+ "key": "db.query.summary",
+ "value": {
+ "stringValue": "Text SELECT"
+ }
+ },
+ {
+ "key": "error.type",
+ "value": {
+ "stringValue": "System.InvalidOperationException"
+ }
+ },
+ {
+ "key": "qyl.instrumentation.domain",
+ "value": {
+ "stringValue": "db.sqlclient"
+ }
+ }
+ ],
+ "endTimeUnixNano": "0",
+ "kind": "SPAN_KIND_CLIENT",
+ "name": "DB client command",
+ "parentSpanId": "",
+ "spanId": "0000000000000005",
+ "startTimeUnixNano": "0",
+ "status": {
+ "code": "STATUS_CODE_ERROR"
+ },
+ "traceId": "00000000000000000000000000000005"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/tools/Qyl.AutoInstrumentation.SmokeTest/golden/stdout.txt b/tools/Qyl.AutoInstrumentation.SmokeTest/golden/stdout.txt
new file mode 100644
index 0000000..2966b27
--- /dev/null
+++ b/tools/Qyl.AutoInstrumentation.SmokeTest/golden/stdout.txt
@@ -0,0 +1,6 @@
+http.status=204
+logger.calls=1
+logger.last=Warning:5:smoke-log
+activity=HTTP client request|Client|http.client|GET|204|
+activity=ILogger log|Internal|log.ilogger|||Warning
+activity.count=2
diff --git a/tools/Qyl.AutoInstrumentation.WebApiAotDemo/golden/report.json b/tools/Qyl.AutoInstrumentation.WebApiAotDemo/golden/report.json
new file mode 100644
index 0000000..acf3aa9
--- /dev/null
+++ b/tools/Qyl.AutoInstrumentation.WebApiAotDemo/golden/report.json
@@ -0,0 +1,175 @@
+{
+ "Activities": [
+ {
+ "Kind": "Client",
+ "Name": "DB client command",
+ "Signal": "activity",
+ "Status": "Unset",
+ "Tags": {
+ "db.operation.name": "CREATE",
+ "db.query.summary": "CREATE",
+ "qyl.instrumentation.domain": "db.client"
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "DB client command",
+ "Signal": "activity",
+ "Status": "Error",
+ "Tags": {
+ "db.operation.name": "SELECT",
+ "db.query.summary": "SELECT",
+ "error.type": "InvalidOperationException",
+ "qyl.instrumentation.domain": "db.client"
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "DB client command",
+ "Signal": "activity",
+ "Status": "Error",
+ "Tags": {
+ "db.operation.name": "SELECT",
+ "db.query.summary": "Text SELECT",
+ "error.type": "System.InvalidOperationException",
+ "qyl.instrumentation.domain": "db.sqlclient"
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "EF Core operation",
+ "Signal": "activity",
+ "Status": "Unset",
+ "Tags": {
+ "db.operation.name": "INSERT",
+ "db.query.summary": "ExecuteSqlRaw INSERT",
+ "qyl.instrumentation.domain": "db.efcore"
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "HTTP client request",
+ "Signal": "activity",
+ "Status": "Unset",
+ "Tags": {
+ "http.request.method": "GET",
+ "http.response.status_code": "204",
+ "qyl.instrumentation.domain": "http.client"
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "HTTP client request",
+ "Signal": "activity",
+ "Status": "Unset",
+ "Tags": {
+ "http.request.method": "GET",
+ "http.response.status_code": "204",
+ "qyl.instrumentation.domain": "http.client",
+ "server.address": "127.0.0.1",
+ "server.port": ""
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "HTTP client request",
+ "Signal": "activity",
+ "Status": "Unset",
+ "Tags": {
+ "http.request.method": "GET",
+ "http.response.status_code": "204",
+ "qyl.instrumentation.domain": "http.client",
+ "server.address": "127.0.0.1",
+ "server.port": ""
+ }
+ },
+ {
+ "Kind": "Server",
+ "Name": "HTTP server request",
+ "Signal": "activity",
+ "Status": "Unset",
+ "Tags": {
+ "http.request.method": "GET",
+ "http.response.status_code": "204",
+ "http.route": "/probe/{id:int}",
+ "qyl.instrumentation.domain": "aspnetcore.server"
+ }
+ },
+ {
+ "Kind": "Server",
+ "Name": "HTTP server request",
+ "Signal": "activity",
+ "Status": "Unset",
+ "Tags": {
+ "http.request.method": "GET",
+ "http.response.status_code": "204",
+ "http.route": "/probe/{id:int}",
+ "qyl.instrumentation.domain": "http.server"
+ }
+ }
+ ],
+ "Failures": [],
+ "Pass": true,
+ "RuntimeMode": "nativeaot",
+ "Signals": [
+ {
+ "Kind": "Server",
+ "Name": "HTTP server request",
+ "Signal": "aspnetcore.server",
+ "Status": "Unset",
+ "Tags": {
+ "http.request.method": "GET",
+ "http.response.status_code": "204",
+ "http.route": "/probe/{id:int}",
+ "qyl.instrumentation.domain": "http.server"
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "EF Core operation",
+ "Signal": "efcore.sqlite",
+ "Status": "Unset",
+ "Tags": {
+ "db.operation.name": "INSERT",
+ "db.query.summary": "ExecuteSqlRaw INSERT",
+ "qyl.instrumentation.domain": "db.efcore"
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "HTTP client request",
+ "Signal": "httpclient.downstream",
+ "Status": "Unset",
+ "Tags": {
+ "http.request.method": "GET",
+ "http.response.status_code": "204",
+ "qyl.instrumentation.domain": "http.client"
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "HTTP client request",
+ "Signal": "httpclient.self",
+ "Status": "Unset",
+ "Tags": {
+ "http.request.method": "GET",
+ "http.response.status_code": "204",
+ "qyl.instrumentation.domain": "http.client",
+ "server.address": "127.0.0.1",
+ "server.port": ""
+ }
+ },
+ {
+ "Kind": "Client",
+ "Name": "DB client command",
+ "Signal": "sqlclient.command",
+ "Status": "Error",
+ "Tags": {
+ "db.operation.name": "SELECT",
+ "db.query.summary": "Text SELECT",
+ "error.type": "System.InvalidOperationException",
+ "qyl.instrumentation.domain": "db.sqlclient"
+ }
+ }
+ ]
+}
diff --git a/tools/smoketest.sh b/tools/smoketest.sh
new file mode 100755
index 0000000..2d56a02
--- /dev/null
+++ b/tools/smoketest.sh
@@ -0,0 +1,250 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+WORK="${TMPDIR:-/tmp}/qyl-smoke"
+FEED="$WORK/feed"
+PACKAGES="$WORK/packages"
+NUGET_ORG="https://api.nuget.org/v3/index.json"
+VERSION="$(sed -n 's:.*\(.*\).*:\1:p' "$ROOT/Directory.Build.props" | head -n 1)"
+GOLDEN="$ROOT/tools/Qyl.AutoInstrumentation.SmokeTest/golden/stdout.txt"
+GENERATOR_DLL="$ROOT/src/Qyl.AutoInstrumentation.SourceGenerators/bin/Release/netstandard2.0/Qyl.AutoInstrumentation.SourceGenerators.dll"
+
+case "$(uname -s)-$(uname -m)" in
+ Darwin-arm64|Darwin-aarch64) RID="osx-arm64" ;;
+ Darwin-*) RID="osx-x64" ;;
+ Linux-arm64|Linux-aarch64) RID="linux-arm64" ;;
+ Linux-*) RID="linux-x64" ;;
+ *) echo "unsupported smoke platform: $(uname -s) $(uname -m)" >&2; exit 2 ;;
+esac
+
+export DOTNET_CLI_TELEMETRY_OPTOUT=1
+export DOTNET_NOLOGO=1
+
+if [[ -z "$VERSION" ]]; then
+ echo "Directory.Build.props does not contain a package " >&2
+ exit 2
+fi
+
+rm -rf "$WORK"
+mkdir -p "$FEED" "$PACKAGES"
+
+dotnet build "$ROOT/src/Qyl.AutoInstrumentation.SourceGenerators/Qyl.AutoInstrumentation.SourceGenerators.csproj" -c Release -v quiet
+dotnet pack "$ROOT/src/Qyl.AutoInstrumentation/Qyl.AutoInstrumentation.csproj" -c Release -o "$FEED" -v quiet
+dotnet pack "$ROOT/src/Qyl.AutoInstrumentation.DiagnosticListeners/Qyl.AutoInstrumentation.DiagnosticListeners.csproj" -c Release -o "$FEED" -v quiet
+dotnet pack "$ROOT/src/Qyl.AutoInstrumentation.Hosting/Qyl.AutoInstrumentation.Hosting.csproj" -c Release -o "$FEED" -v quiet
+
+write_program() {
+ local dir="$1"
+ cat > "$dir/Program.cs" <<'EOF'
+using System.Diagnostics;
+using System.Net;
+using Microsoft.Extensions.Logging;
+using Qyl.AutoInstrumentation;
+
+var captured = new List();
+using var listener = new ActivityListener
+{
+ ShouldListenTo = static source => source.Name == QylActivitySource.Name,
+ Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded,
+ ActivityStopped = activity => captured.Add(activity),
+};
+
+ActivitySource.AddActivityListener(listener);
+
+using var http = new HttpClient(new StubHandler())
+{
+ BaseAddress = new Uri("https://qyl-smoke.invalid"),
+};
+
+var response = await http.GetAsync("/probe?secret=redacted");
+Console.WriteLine("http.status=" + ((int)response.StatusCode).ToString(System.Globalization.CultureInfo.InvariantCulture));
+
+var concreteLogger = new CapturingLogger();
+ILogger logger = concreteLogger;
+logger.Log(
+ LogLevel.Warning,
+ new EventId(5, "smoke"),
+ "smoke-log",
+ exception: null,
+ static (state, exception) => exception is null ? state : state + ":" + exception.GetType().Name);
+
+Console.WriteLine("logger.calls=" + concreteLogger.Calls.ToString(System.Globalization.CultureInfo.InvariantCulture));
+Console.WriteLine("logger.last=" + concreteLogger.Last);
+
+foreach (var activity in captured.OrderBy(static activity => activity.DisplayName, StringComparer.Ordinal))
+{
+ var tags = activity.TagObjects.ToDictionary(
+ static tag => tag.Key,
+ static tag => Convert.ToString(tag.Value, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty,
+ StringComparer.Ordinal);
+
+ tags.TryGetValue(QylSemanticAttributes.QylInstrumentationDomain, out var domain);
+ tags.TryGetValue(QylSemanticAttributes.HttpRequestMethod, out var method);
+ tags.TryGetValue(QylSemanticAttributes.HttpResponseStatusCode, out var statusCode);
+ tags.TryGetValue(QylSemanticAttributes.LogSeverity, out var severity);
+
+ Console.WriteLine("activity=" + activity.DisplayName + "|" + activity.Kind + "|" + domain + "|" + method + "|" + statusCode + "|" + severity);
+}
+
+Console.WriteLine("activity.count=" + captured.Count.ToString(System.Globalization.CultureInfo.InvariantCulture));
+
+return captured.Count == 2 ? 0 : 3;
+
+internal sealed class StubHandler : HttpMessageHandler
+{
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NoContent)
+ {
+ RequestMessage = request,
+ });
+}
+
+internal sealed class CapturingLogger : ILogger
+{
+ public int Calls { get; private set; }
+
+ public string Last { get; private set; } = string.Empty;
+
+ public IDisposable? BeginScope(TState state) where TState : notnull => null;
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(
+ LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception? exception,
+ Func formatter)
+ {
+ Calls++;
+ Last = logLevel + ":" + eventId.Id.ToString(System.Globalization.CultureInfo.InvariantCulture) + ":" + formatter(state, exception);
+ }
+}
+EOF
+}
+
+write_package_consumer() {
+ local dir="$1"
+ mkdir -p "$dir"
+ cat > "$dir/Consumer.csproj" <
+
+ Exe
+ net10.0
+ enable
+ enable
+ $FEED;$NUGET_ORG
+ $PACKAGES/pkg
+ true
+ true
+ Generated
+
+
+
+
+
+
+
+
+
+EOF
+ write_program "$dir"
+}
+
+write_projectreference_consumer() {
+ local dir="$1"
+ mkdir -p "$dir"
+ cat > "$dir/Consumer.csproj" <
+
+ Exe
+ net10.0
+ enable
+ enable
+ $NUGET_ORG
+ $PACKAGES/projref
+ true
+ true
+ Generated
+
+
+
+
+
+
+
+
+
+
+
+
+EOF
+ write_program "$dir"
+}
+
+assert_generated_interceptor() {
+ local dir="$1"
+ local count
+ count="$(find "$dir/Generated" -name 'QylAutoInstrumentation.Interceptors.g.cs' | wc -l | tr -d '[:space:]')"
+ if [[ "$count" != "1" ]]; then
+ echo "expected one generated interceptor source in $dir, found $count" >&2
+ find "$dir/Generated" -type f -print >&2 || true
+ exit 4
+ fi
+}
+
+assert_no_aot_warnings() {
+ local log="$1"
+ local consumer="$2"
+ local matches
+
+ matches="$(grep -Eo '\\b(IL2[0-9]{3}|IL3[0-9]{3}|IL4[0-9]{3}|CA[0-9]{4})\\b' "$log" | sort -u || true)"
+ if [[ -n "$matches" ]]; then
+ echo "AOT warning gate failed for $consumer; found analyzer warnings:" >&2
+ echo "$matches" >&2
+ echo "--- publish log ---" >&2
+ cat "$log" >&2
+ exit 5
+ fi
+
+ echo "aot-warning-gate-ok consumer=$consumer warnings=0"
+}
+
+run_consumer() {
+ local name="$1"
+ local dir="$2"
+ local managed_out="$WORK/$name.managed.stdout"
+ local native_out="$WORK/$name.nativeaot.stdout"
+ local publish_log="$WORK/$name.nativeaot.publish.log"
+
+ dotnet build "$dir/Consumer.csproj" -c Release -v quiet
+ assert_generated_interceptor "$dir"
+ dotnet "$dir/bin/Release/net10.0/Consumer.dll" > "$managed_out"
+ diff -u "$GOLDEN" "$managed_out"
+
+ dotnet publish "$dir/Consumer.csproj" \
+ -c Release \
+ -r "$RID" \
+ -p:PublishAot=true \
+ -p:SelfContained=true \
+ -p:InvariantGlobalization=true \
+ -p:TreatWarningsAsErrors=true \
+ -v quiet 2>&1 | tee "$publish_log"
+ assert_no_aot_warnings "$publish_log" "$name"
+
+ "$dir/bin/Release/net10.0/$RID/publish/Consumer" > "$native_out"
+ diff -u "$GOLDEN" "$native_out"
+}
+
+write_package_consumer "$WORK/pkg-consumer"
+write_projectreference_consumer "$WORK/projref-consumer"
+
+run_consumer "package-reference" "$WORK/pkg-consumer"
+run_consumer "project-reference" "$WORK/projref-consumer"
+
+echo "smoketest-ok rid=$RID"
diff --git a/tools/verify-aot-autoinstrumentation-goal.py b/tools/verify-aot-autoinstrumentation-goal.py
index b3a631c..e8b7f6f 100644
--- a/tools/verify-aot-autoinstrumentation-goal.py
+++ b/tools/verify-aot-autoinstrumentation-goal.py
@@ -14,8 +14,17 @@
("contract coverage report", [sys.executable, "tools/verify-contract-coverage-report.py"]),
("release build", ["dotnet", "build", "Qyl.AutoInstrumentation.slnx", "-c", "Release"]),
("package layout", [sys.executable, "tools/verify-package-layout.py"]),
+ ("projectreference behavior", [sys.executable, "tools/verify-projectreference-behavior.py"]),
+ ("public api baseline", [sys.executable, "tools/verify-public-api-baseline.py"]),
+ ("xml doc enforcement", [sys.executable, "tools/verify-xml-doc-enforcement.py"]),
("environment options behavior", [sys.executable, "tools/verify-environment-options-behavior.py"]),
+ ("conformance opt-in", [sys.executable, "tools/verify-conformance-opt-in.py"]),
+ ("generator snapshots", [sys.executable, "tools/verify-generator-snapshots.py"]),
("source interceptor consumer", [sys.executable, "tools/verify-source-interceptor-consumer.py"]),
+ ("smoketest", ["bash", "tools/smoketest.sh"]),
+ ("webapi aot demo", [sys.executable, "tools/verify-webapi-aot-demo.py"]),
+ ("otlp golden fixtures", [sys.executable, "tools/verify-otlp-golden-fixtures.py"]),
+ ("otlp collector fixtures", [sys.executable, "tools/verify-otlp-collector-fixtures.py"]),
("consumer behavior", [sys.executable, "tools/verify-consumer-behavior.py"]),
("nativeaot consumer golden", [sys.executable, "tools/verify-nativeaot-consumer-golden.py"]),
("diff whitespace", ["git", "diff", "--check"]),
diff --git a/tools/verify-conformance-opt-in.py b/tools/verify-conformance-opt-in.py
new file mode 100644
index 0000000..0594b1d
--- /dev/null
+++ b/tools/verify-conformance-opt-in.py
@@ -0,0 +1,214 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import os
+import subprocess
+import tempfile
+from pathlib import Path
+
+try:
+ import fcntl
+except ImportError:
+ fcntl = None
+
+
+ROOT = Path(__file__).resolve().parents[1]
+PACK_LOCK_PATH = Path(tempfile.gettempdir()) / "qyl-dotnet-autoinstrumentation-pack.lock"
+PROPS_PATH = ROOT / "Directory.Build.props"
+CORE_PROJECT = ROOT / "src" / "Qyl.AutoInstrumentation" / "Qyl.AutoInstrumentation.csproj"
+DIAGNOSTIC_LISTENERS_PROJECT = ROOT / "src" / "Qyl.AutoInstrumentation.DiagnosticListeners" / "Qyl.AutoInstrumentation.DiagnosticListeners.csproj"
+HOSTING_PROJECT = ROOT / "src" / "Qyl.AutoInstrumentation.Hosting" / "Qyl.AutoInstrumentation.Hosting.csproj"
+TARGET_FRAMEWORK = "net10.0"
+NUGET_ORG = "https://api.nuget.org/v3/index.json"
+
+
+PROGRAM = r'''
+using System.Diagnostics;
+using System.Diagnostics.Metrics;
+using Microsoft.Extensions.DependencyInjection;
+using Qyl.AutoInstrumentation;
+using Qyl.AutoInstrumentation.Hosting;
+
+var mode = args.Length is 0 ? "direct" : args[0];
+var checks = 0L;
+
+using var meterListener = new MeterListener();
+meterListener.InstrumentPublished = static (instrument, listener) =>
+{
+ if (instrument.Meter.Name == QylSelfTelemetry.MeterName &&
+ instrument.Name == QylMetricNames.QylSemConvAttributeChecks)
+ {
+ listener.EnableMeasurementEvents(instrument);
+ }
+};
+meterListener.SetMeasurementEventCallback(
+ (instrument, measurement, tags, state) => checks += measurement);
+meterListener.Start();
+
+if (mode == "hosting")
+{
+ new ServiceCollection().AddQylAutoInstrumentation(static options => options.EnableConformanceProcessor = true);
+}
+else
+{
+ QylInstrumentation.Activate();
+}
+
+using (var activity = QylActivitySource.Source.StartActivity("conformance probe"))
+{
+ activity?.SetTag(QylSemanticAttributes.QylInstrumentationDomain, "probe");
+}
+
+Console.WriteLine("mode=" + mode);
+Console.WriteLine("checks=" + checks.ToString(System.Globalization.CultureInfo.InvariantCulture));
+'''
+
+
+def fail(message: str) -> None:
+ raise SystemExit(message)
+
+
+def clean_env() -> dict[str, str]:
+ env = dict(os.environ)
+ for key in list(env):
+ if key.startswith("OTEL_") or key.startswith("QYL_"):
+ del env[key]
+
+ env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
+ env["DOTNET_NOLOGO"] = "1"
+ return env
+
+
+def read_version() -> str:
+ text = PROPS_PATH.read_text(encoding="utf-8")
+ prefix = ""
+ suffix = ""
+ start = text.find(prefix)
+ if start < 0:
+ fail("Directory.Build.props is missing ")
+
+ end = text.find(suffix, start)
+ if end < 0:
+ fail("Directory.Build.props has unterminated ")
+
+ return text[start + len(prefix):end].strip()
+
+
+def run_checked(command: list[str], cwd: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
+ completed = subprocess.run(
+ command,
+ cwd=cwd,
+ env=env,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=False,
+ )
+ if completed.returncode != 0:
+ fail(
+ "command failed: "
+ + " ".join(command)
+ + f"\nexit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}"
+ )
+
+ return completed
+
+
+def pack_runtime(feed: Path, env: dict[str, str]) -> None:
+ feed.mkdir(parents=True)
+ with PACK_LOCK_PATH.open("w", encoding="utf-8") as lock:
+ if fcntl is not None:
+ fcntl.flock(lock, fcntl.LOCK_EX)
+ try:
+ for project in [CORE_PROJECT, DIAGNOSTIC_LISTENERS_PROJECT, HOSTING_PROJECT]:
+ run_checked(
+ ["dotnet", "pack", str(project), "-c", "Release", "-o", str(feed), "-v", "quiet"],
+ ROOT,
+ env,
+ )
+ finally:
+ if fcntl is not None:
+ fcntl.flock(lock, fcntl.LOCK_UN)
+
+
+def write_project(directory: Path, feed: Path, packages: Path, version: str) -> Path:
+ directory.mkdir(parents=True)
+ project_path = directory / "Consumer.csproj"
+ project_path.write_text(
+ f'''
+
+ Exe
+ {TARGET_FRAMEWORK}
+ enable
+ enable
+ {feed};{NUGET_ORG}
+ {packages}
+ true
+
+
+
+
+
+
+
+''',
+ encoding="utf-8",
+ )
+ (directory / "Program.cs").write_text(PROGRAM, encoding="utf-8")
+ return project_path
+
+
+def run_scenario(assembly: Path, base_env: dict[str, str], mode: str, overrides: dict[str, str]) -> str:
+ env = dict(base_env)
+ env.update(overrides)
+ completed = subprocess.run(
+ ["dotnet", str(assembly), mode],
+ cwd=assembly.parent,
+ env=env,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=False,
+ )
+ if completed.returncode != 0:
+ fail(f"{mode} scenario failed\nexit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}")
+ if completed.stderr:
+ fail(f"{mode} scenario wrote stderr:\n{completed.stderr}")
+
+ return completed.stdout
+
+
+def assert_output(name: str, actual: str, expected: str) -> None:
+ if actual != expected:
+ fail(f"{name} mismatch\nEXPECTED\n{expected}\nACTUAL\n{actual}")
+
+
+def main() -> None:
+ env = clean_env()
+ version = read_version()
+ with tempfile.TemporaryDirectory(prefix="qyl-conformance-opt-in-") as temp:
+ root = Path(temp)
+ feed = root / "feed"
+ packages = root / "packages"
+ pack_runtime(feed, env)
+ project = write_project(root / "consumer", feed, packages, version)
+ run_checked(["dotnet", "build", str(project), "-c", "Release", "-v", "quiet"], project.parent, env)
+ assembly = project.parent / "bin" / "Release" / TARGET_FRAMEWORK / "Consumer.dll"
+
+ assert_output("default off", run_scenario(assembly, env, "direct", {}), "mode=direct\nchecks=0\n")
+ assert_output(
+ "environment opt-in",
+ run_scenario(assembly, env, "direct", {"QYL_CONFORMANCE_ENABLED": "true"}),
+ "mode=direct\nchecks=1\n",
+ )
+ assert_output(
+ "hosting opt-in",
+ run_scenario(assembly, env, "hosting", {}),
+ "mode=hosting\nchecks=1\n",
+ )
+
+ print("conformance-opt-in-ok")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/verify-contract-coverage-report.py b/tools/verify-contract-coverage-report.py
index 749afc1..ed59290 100644
--- a/tools/verify-contract-coverage-report.py
+++ b/tools/verify-contract-coverage-report.py
@@ -12,6 +12,7 @@
ROOT = Path(__file__).resolve().parents[1]
VERIFIER_PATH = ROOT / "tools" / "verify-contract-invariants.py"
+COVERAGE_MATRIX_PATH = ROOT / "docs" / "coverage-matrix.md"
def fail(message: str) -> None:
@@ -202,14 +203,102 @@ def build_report(verifier: ModuleType) -> dict[str, Any]:
}
+def render_markdown(report: dict[str, Any]) -> str:
+ counts = report["counts"]
+ lines = [
+ "# AOT Interceptor Coverage Matrix",
+ "",
+ "This matrix is generated from `docs/otel-dotnet-auto-60-contract-items.yaml`,",
+ "`src/Qyl.AutoInstrumentation.SourceGenerators/InstrumentationContract.cs`, and",
+ "`src/Qyl.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs`.",
+ "",
+ "It is the review artifact for the 60-item auto-instrumentation contract: upstream",
+ "contract item on the left, qyl NativeAOT interceptor/runtime status on the right.",
+ "",
+ "## Counts",
+ "",
+ "| Count | Value |",
+ "|---|---:|",
+ f"| Total contract items | {counts['total']} |",
+ f"| Source-generated signal bindings | {counts['source_generated_signals']} |",
+ f"| Unsupported NativeAOT parity/dynamic signals | {counts['unsupported_signals']} |",
+ f"| Runtime environment controls | {counts['environment_controls']} |",
+ f"| Runtime instrumentation options | {counts['instrumentation_options']} |",
+ f"| Missing bindings | {counts['missing']} |",
+ "",
+ "## Status legend",
+ "",
+ "| Status | Meaning |",
+ "|---|---|",
+ "| `source_generated_signal` | The source generator has a source-visible call-site binding for this signal. |",
+ "| `unsupported_nativeaot_parity_or_dynamic_signal` | The upstream contract item is retained for parity, but it is not reachable as a NativeAOT source-interceptor signal. |",
+ "| `runtime_environment_control` | The runtime options model binds the global/signal environment control. |",
+ "| `runtime_instrumentation_option` | The runtime options model binds the instrumentation option. |",
+ "| `missing_*` | Fails the gate. |",
+ "",
+ "## Matrix",
+ "",
+ "| # | Contract item | Kind | Key | qyl status | Evidence |",
+ "|---:|---|---|---|---|---|",
+ ]
+
+ for record in report["items"]:
+ evidence = "
".join(record["evidence"]) if record["evidence"] else "-"
+ lines.append(
+ "| "
+ + str(record["index"])
+ + " | `"
+ + record["contract_item_id"]
+ + "` | `"
+ + record["kind"]
+ + "` | `"
+ + record["key"]
+ + "` | `"
+ + record["status"]
+ + "` | "
+ + evidence
+ + " |"
+ )
+
+ lines.extend(
+ [
+ "",
+ "// validated 2026-06-05 by tools/verify-contract-coverage-report.py",
+ "",
+ ]
+ )
+ return "\n".join(lines)
+
+
+def verify_markdown(report: dict[str, Any]) -> None:
+ expected = render_markdown(report)
+ if not COVERAGE_MATRIX_PATH.exists():
+ fail(f"coverage matrix missing: {COVERAGE_MATRIX_PATH}")
+
+ actual = COVERAGE_MATRIX_PATH.read_text(encoding="utf-8")
+ if actual != expected:
+ received = COVERAGE_MATRIX_PATH.with_suffix(".received.md")
+ received.write_text(expected, encoding="utf-8")
+ fail(
+ "coverage matrix is stale\n"
+ f"expected={COVERAGE_MATRIX_PATH}\n"
+ f"received={received}"
+ )
+
+
def main() -> None:
parser = argparse.ArgumentParser(description="Verify and optionally emit the 60-item qyl contract coverage report.")
parser.add_argument("--json", type=Path, help="Write the full machine-readable report to this path.")
+ parser.add_argument("--markdown", type=Path, help="Write the generated coverage matrix markdown to this path.")
args = parser.parse_args()
report = build_report(load_verifier())
if args.json is not None:
args.json.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
+ if args.markdown is not None:
+ args.markdown.write_text(render_markdown(report), encoding="utf-8")
+ else:
+ verify_markdown(report)
counts = report["counts"]
print(
diff --git a/tools/verify-environment-options-behavior.py b/tools/verify-environment-options-behavior.py
index 86d246e..ace26e1 100644
--- a/tools/verify-environment-options-behavior.py
+++ b/tools/verify-environment-options-behavior.py
@@ -29,6 +29,7 @@
Console.WriteLine("traces=" + options.TracesEnabled);
Console.WriteLine("metrics=" + options.MetricsEnabled);
Console.WriteLine("logs=" + options.LogsEnabled);
+Console.WriteLine("conformance=" + options.ConformanceProcessorEnabled);
Console.WriteLine("trace.http=" + options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.HttpClient));
Console.WriteLine("trace.sql=" + options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.SqlClient));
Console.WriteLine("metric.http=" + options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.HttpClient));
@@ -59,6 +60,7 @@
traces=True
metrics=True
logs=True
+conformance=False
trace.http=True
trace.sql=True
metric.http=True
@@ -88,6 +90,7 @@
traces=False
metrics=False
logs=False
+conformance=False
trace.http=True
trace.sql=False
metric.http=False
@@ -117,6 +120,7 @@
traces=True
metrics=True
logs=True
+conformance=False
trace.http=True
trace.sql=False
metric.http=True
@@ -146,6 +150,7 @@
traces=True
metrics=True
logs=True
+conformance=True
trace.http=True
trace.sql=True
metric.http=True
@@ -353,6 +358,7 @@ def main() -> None:
"OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION": "true",
"OTEL_DOTNET_EXPERIMENTAL_ASPNET_DISABLE_URL_QUERY_REDACTION": "true",
"OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED": "true",
+ "QYL_CONFORMANCE_ENABLED": "true",
},
),
OPTIONS_EXPECTED,
diff --git a/tools/verify-generator-snapshots.py b/tools/verify-generator-snapshots.py
new file mode 100644
index 0000000..996202c
--- /dev/null
+++ b/tools/verify-generator-snapshots.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import filecmp
+import shutil
+import subprocess
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+FIXTURE_PROJECT = (
+ ROOT
+ / "tests"
+ / "Qyl.AutoInstrumentation.SourceGenerators.Snapshots"
+ / "Fixture"
+ / "Qyl.AutoInstrumentation.SourceGenerators.Snapshots.Fixture.csproj"
+)
+FIXTURE_DIR = FIXTURE_PROJECT.parent
+GENERATED_ROOT = FIXTURE_DIR / "obj" / "generated"
+VERIFIED_ROOT = FIXTURE_DIR.parent / "verified"
+
+EXPECTED_FILES = {
+ "QylAutoInstrumentation.Interceptors.g.verified.cs": "QylAutoInstrumentation.Interceptors.g.cs",
+ "QylGeneratedInstrumentationContract.g.verified.cs": "QylGeneratedInstrumentationContract.g.cs",
+}
+
+REQUIRED_INTERCEPTOR_TOKENS = [
+ "// ",
+ "#nullable enable",
+ "namespace Qyl.AutoInstrumentation.Generated",
+ "internal static class QylGeneratedInterceptors",
+ "// Intercepted call at /_qyl_generator_snapshot/Program.cs",
+ "[global::System.Runtime.CompilerServices.InterceptsLocationAttribute(",
+ "ILogger_Log_",
+ "global::Microsoft.Extensions.Logging.ILogger logger",
+ "global::Qyl.AutoInstrumentation.QylInterceptedLogger.Log(",
+]
+
+FORBIDDEN_INTERCEPTOR_TOKENS = [
+ "System.Reflection",
+ "Assembly.Load",
+ "Activator.CreateInstance",
+ "QylActivitySource",
+ ".SetTag(",
+]
+
+
+def fail(message: str) -> None:
+ raise SystemExit(message)
+
+
+def run_build() -> None:
+ completed = subprocess.run(
+ ["dotnet", "build", str(FIXTURE_PROJECT), "-c", "Release", "-v", "quiet"],
+ cwd=ROOT,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=False,
+ )
+ if completed.returncode != 0:
+ fail(
+ "generator snapshot fixture build failed\n"
+ f"exit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}"
+ )
+
+
+def generated_files_by_name() -> dict[str, Path]:
+ if not GENERATED_ROOT.exists():
+ fail(f"missing generated output directory: {GENERATED_ROOT}")
+
+ expected = set(EXPECTED_FILES.values())
+ files = {
+ path.name: path
+ for path in GENERATED_ROOT.rglob("*.g.cs")
+ if path.name in expected
+ }
+ return files
+
+
+def verify_interceptor_snapshot(text: str) -> None:
+ for token in REQUIRED_INTERCEPTOR_TOKENS:
+ if token not in text:
+ fail(f"interceptor snapshot missing token: {token}")
+
+ for token in FORBIDDEN_INTERCEPTOR_TOKENS:
+ if token in text:
+ fail(f"interceptor snapshot must not inline runtime telemetry behavior: {token}")
+
+
+def compare_snapshots(files: dict[str, Path]) -> None:
+ expected_generated = set(EXPECTED_FILES.values())
+ actual_generated = set(files)
+ missing = expected_generated - actual_generated
+ unexpected = actual_generated - expected_generated
+ if missing:
+ fail(f"missing generated snapshot files: {sorted(missing)}")
+ if unexpected:
+ fail(f"unexpected generated snapshot files: {sorted(unexpected)}")
+
+ for verified_name, generated_name in EXPECTED_FILES.items():
+ verified = VERIFIED_ROOT / verified_name
+ generated = files[generated_name]
+ if not verified.exists():
+ fail(f"missing verified snapshot: {verified}")
+ if not filecmp.cmp(verified, generated, shallow=False):
+ received = GENERATED_ROOT / verified_name.replace(".verified.cs", ".received.cs")
+ shutil.copyfile(generated, received)
+ fail(
+ "generator snapshot mismatch\n"
+ f"verified={verified}\n"
+ f"received={received}"
+ )
+
+ verify_interceptor_snapshot((VERIFIED_ROOT / "QylAutoInstrumentation.Interceptors.g.verified.cs").read_text(encoding="utf-8"))
+
+
+def main() -> None:
+ run_build()
+ compare_snapshots(generated_files_by_name())
+ print("generator-snapshots-ok")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/verify-otlp-collector-fixtures.py b/tools/verify-otlp-collector-fixtures.py
new file mode 100755
index 0000000..1963dca
--- /dev/null
+++ b/tools/verify-otlp-collector-fixtures.py
@@ -0,0 +1,406 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import json
+import shutil
+import subprocess
+import sys
+import textwrap
+import threading
+import time
+from dataclasses import dataclass
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from pathlib import Path
+from typing import Any
+
+
+ROOT = Path(__file__).resolve().parents[1]
+WORK = Path("/tmp/qyl-otlp-collector-fixtures")
+FEED = WORK / "feed"
+APP = WORK / "collector-consumer"
+GOLDEN = ROOT / "tools/Qyl.AutoInstrumentation.OtlpCollectorFixtures/golden/httpclient-traces.collector.json"
+EXPORTER_VERSION = "1.15.3"
+
+REQUIRED_STRINGS = (
+ "Qyl.AutoInstrumentation",
+ "HTTP client request",
+ "qyl.instrumentation.domain",
+ "http.client",
+ "http.request.method",
+ "GET",
+ "http.response.status_code",
+ "server.address",
+ "downstream.example",
+)
+
+FORBIDDEN_STRINGS = (
+ "url.full",
+ "url.path",
+ "access_token",
+ "super-secret",
+ "/probe",
+ "?access_token",
+)
+
+
+@dataclass(frozen=True)
+class CapturedRequest:
+ method: str
+ path: str
+ content_type: str
+ body: bytes
+
+
+class CollectorServer(ThreadingHTTPServer):
+ requests: list[CapturedRequest]
+
+ def __init__(self) -> None:
+ super().__init__(("127.0.0.1", 0), CollectorHandler)
+ self.requests = []
+
+
+class CollectorHandler(BaseHTTPRequestHandler):
+ server: CollectorServer
+
+ def do_POST(self) -> None:
+ length = int(self.headers.get("Content-Length", "0"))
+ body = self.rfile.read(length)
+ content_type = self.headers.get("Content-Type", "")
+ self.server.requests.append(
+ CapturedRequest(
+ method="POST",
+ path=self.path,
+ content_type=normalize_content_type(content_type),
+ body=body,
+ )
+ )
+ self.send_response(200)
+ self.send_header("Content-Type", "application/x-protobuf")
+ self.end_headers()
+
+ def log_message(self, _format: str, *_args: Any) -> None:
+ return
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--update-golden", action="store_true")
+ args = parser.parse_args()
+
+ clean_workdir()
+ version = pack_local_packages()
+
+ server = CollectorServer()
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ endpoint = f"http://127.0.0.1:{server.server_port}/v1/traces"
+
+ try:
+ write_consumer(version)
+ run(["dotnet", "run", "-c", "Release", "--", endpoint], cwd=APP)
+ report = build_report(server.requests)
+ finally:
+ server.shutdown()
+ server.server_close()
+ thread.join(timeout=5)
+
+ if args.update_golden:
+ GOLDEN.parent.mkdir(parents=True, exist_ok=True)
+ GOLDEN.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
+ print(f"updated {GOLDEN.relative_to(ROOT)}")
+ return
+
+ expected = json.loads(GOLDEN.read_text())
+ if report != expected:
+ print("OTLP collector fixture mismatch", file=sys.stderr)
+ print("expected:", json.dumps(expected, indent=2, sort_keys=True), file=sys.stderr)
+ print("actual:", json.dumps(report, indent=2, sort_keys=True), file=sys.stderr)
+ raise SystemExit(1)
+
+ print("otlp-collector-fixtures-ok")
+
+
+def clean_workdir() -> None:
+ if WORK.exists():
+ shutil.rmtree(WORK)
+ FEED.mkdir(parents=True)
+ APP.mkdir(parents=True)
+
+
+def pack_local_packages() -> str:
+ version = f"{read_package_version()}.otlpcollector.{time.time_ns()}"
+ projects = (
+ ROOT / "src/Qyl.AutoInstrumentation.SourceGenerators/Qyl.AutoInstrumentation.SourceGenerators.csproj",
+ ROOT / "src/Qyl.AutoInstrumentation/Qyl.AutoInstrumentation.csproj",
+ )
+ for project in projects:
+ if project.exists():
+ run(
+ [
+ "dotnet",
+ "pack",
+ str(project),
+ "-c",
+ "Release",
+ "-o",
+ str(FEED),
+ f"-p:Version={version}",
+ f"-p:PackageVersion={version}",
+ ],
+ cwd=ROOT,
+ )
+
+ package_prefix = "Qyl.AutoInstrumentation."
+ packages = sorted(FEED.glob(f"{package_prefix}*.nupkg"))
+ runtime_packages = [package for package in packages if ".SourceGenerator." not in package.name]
+ if not runtime_packages:
+ raise SystemExit("Qyl.AutoInstrumentation package was not produced")
+
+ package = runtime_packages[-1]
+ name = package.name
+ if not name.startswith(package_prefix) or not name.endswith(".nupkg"):
+ raise SystemExit(f"cannot infer package version from {name}")
+
+ return name[len(package_prefix) : -len(".nupkg")]
+
+
+def read_package_version() -> str:
+ text = (ROOT / "Directory.Build.props").read_text(encoding="utf-8")
+ prefix = ""
+ suffix = ""
+ start = text.find(prefix)
+ if start < 0:
+ raise SystemExit("Directory.Build.props is missing ")
+
+ start += len(prefix)
+ end = text.find(suffix, start)
+ if end < 0:
+ raise SystemExit("Directory.Build.props has unterminated ")
+
+ version = text[start:end].strip()
+ if not version:
+ raise SystemExit("Directory.Build.props has empty ")
+
+ return version
+
+
+def write_consumer(version: str) -> None:
+ (APP / "NuGet.Config").write_text(
+ textwrap.dedent(
+ f"""
+
+
+
+
+
+
+
+
+ """
+ ).strip()
+ + "\n"
+ )
+ (APP / "CollectorConsumer.csproj").write_text(
+ textwrap.dedent(
+ f"""
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+ """
+ ).strip()
+ + "\n"
+ )
+ (APP / "Program.cs").write_text(
+ textwrap.dedent(
+ """
+ using System.Net;
+ using OpenTelemetry;
+ using OpenTelemetry.Exporter;
+ using OpenTelemetry.Trace;
+ using Qyl.AutoInstrumentation;
+
+ if (args.Length != 1)
+ throw new InvalidOperationException("Expected OTLP trace endpoint.");
+
+ using var provider = Sdk.CreateTracerProviderBuilder()
+ .SetSampler(new AlwaysOnSampler())
+ .AddSource(QylActivitySource.Name)
+ .AddOtlpExporter(options =>
+ {
+ options.Endpoint = new Uri(args[0]);
+ options.Protocol = OtlpExportProtocol.HttpProtobuf;
+ options.TimeoutMilliseconds = 10_000;
+ })
+ .Build();
+
+ using var http = new HttpClient(new StubHandler());
+
+ using var response = await http.GetAsync("https://downstream.example/probe?access_token=super-secret");
+ if (response.StatusCode != HttpStatusCode.NoContent)
+ throw new InvalidOperationException("Unexpected stub response.");
+
+ if (!provider.ForceFlush(10_000))
+ throw new InvalidOperationException("OTLP trace export did not flush.");
+
+ internal sealed class StubHandler : HttpMessageHandler
+ {
+ protected override Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NoContent)
+ {
+ RequestMessage = request
+ });
+ }
+ }
+ """
+ ).strip()
+ + "\n"
+ )
+
+
+def build_report(requests: list[CapturedRequest]) -> dict[str, Any]:
+ non_empty = [request for request in requests if request.body]
+ if len(non_empty) != 1:
+ raise SystemExit(f"expected exactly one non-empty OTLP request, got {len(non_empty)}")
+
+ request = non_empty[0]
+ if request.method != "POST":
+ raise SystemExit(f"expected POST, got {request.method}")
+ if request.path != "/v1/traces":
+ raise SystemExit(f"expected /v1/traces, got {request.path}")
+ if request.content_type != "application/x-protobuf":
+ raise SystemExit(f"expected application/x-protobuf, got {request.content_type}")
+
+ strings = extract_protobuf_strings(request.body)
+ missing = [value for value in REQUIRED_STRINGS if value not in strings]
+ forbidden = [value for value in FORBIDDEN_STRINGS if any(value in candidate for candidate in strings)]
+ if missing:
+ raise SystemExit(f"OTLP payload is missing qyl contract strings: {missing}")
+ if forbidden:
+ raise SystemExit(f"OTLP payload leaked forbidden sensitive strings: {forbidden}")
+
+ return {
+ "wireFormat": "otlp-http-protobuf",
+ "request": {
+ "method": request.method,
+ "path": request.path,
+ "contentType": request.content_type,
+ },
+ "matchedStrings": sorted(REQUIRED_STRINGS),
+ "forbiddenStrings": [],
+ }
+
+
+def extract_protobuf_strings(data: bytes) -> set[str]:
+ strings: set[str] = set()
+ parse_message(data, strings, 0)
+ return strings
+
+
+def parse_message(data: bytes, strings: set[str], depth: int) -> None:
+ if depth > 16:
+ return
+
+ index = 0
+ while index < len(data):
+ try:
+ tag, index = read_varint(data, index)
+ except ValueError:
+ return
+
+ if tag == 0:
+ return
+
+ wire_type = tag & 0b111
+ if wire_type == 0:
+ try:
+ _, index = read_varint(data, index)
+ except ValueError:
+ return
+ elif wire_type == 1:
+ index += 8
+ elif wire_type == 2:
+ try:
+ length, index = read_varint(data, index)
+ except ValueError:
+ return
+
+ end = index + length
+ if end > len(data):
+ return
+
+ segment = data[index:end]
+ index = end
+ text = try_decode_text(segment)
+ if text is not None:
+ strings.add(text)
+ if segment:
+ parse_message(segment, strings, depth + 1)
+ elif wire_type == 5:
+ index += 4
+ else:
+ return
+
+
+def read_varint(data: bytes, index: int) -> tuple[int, int]:
+ shift = 0
+ value = 0
+ while index < len(data):
+ byte = data[index]
+ index += 1
+ value |= (byte & 0x7F) << shift
+ if byte < 0x80:
+ return value, index
+ shift += 7
+ if shift > 63:
+ break
+ raise ValueError("invalid protobuf varint")
+
+
+def try_decode_text(data: bytes) -> str | None:
+ if not data or len(data) > 512:
+ return None
+ try:
+ text = data.decode("utf-8")
+ except UnicodeDecodeError:
+ return None
+
+ if not any(character.isalpha() for character in text):
+ return None
+ for character in text:
+ ordinal = ord(character)
+ if ordinal < 32 or ordinal == 127:
+ return None
+ return text
+
+
+def normalize_content_type(content_type: str) -> str:
+ return content_type.split(";", 1)[0].strip().lower()
+
+
+def run(command: list[str], cwd: Path) -> None:
+ completed = subprocess.run(command, cwd=cwd, check=False)
+ if completed.returncode != 0:
+ raise SystemExit(f"{' '.join(command)} failed with exit code {completed.returncode}")
+
+
+if __name__ == "__main__":
+ started = time.monotonic()
+ try:
+ main()
+ finally:
+ elapsed = time.monotonic() - started
+ print(f"otlp-collector-fixtures-elapsed={elapsed:.1f}s")
diff --git a/tools/verify-otlp-golden-fixtures.py b/tools/verify-otlp-golden-fixtures.py
new file mode 100644
index 0000000..6206c54
--- /dev/null
+++ b/tools/verify-otlp-golden-fixtures.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+from typing import Any
+
+
+ROOT = Path(__file__).resolve().parents[1]
+PROPS_PATH = ROOT / "Directory.Build.props"
+WEBAPI_REPORT_PATH = ROOT / "tools" / "Qyl.AutoInstrumentation.WebApiAotDemo" / "golden" / "report.json"
+OTLP_GOLDEN_PATH = ROOT / "tools" / "Qyl.AutoInstrumentation.OtlpGoldenFixtures" / "golden" / "webapi-aot-traces.otlp.json"
+
+EXPECTED_SIGNALS = [
+ "aspnetcore.server",
+ "efcore.sqlite",
+ "httpclient.downstream",
+ "httpclient.self",
+ "sqlclient.command",
+]
+
+
+def fail(message: str) -> None:
+ raise SystemExit(message)
+
+
+def read_version() -> str:
+ text = PROPS_PATH.read_text(encoding="utf-8")
+ prefix = ""
+ suffix = ""
+ start = text.find(prefix)
+ if start < 0:
+ fail("Directory.Build.props is missing ")
+
+ end = text.find(suffix, start)
+ if end < 0:
+ fail("Directory.Build.props has unterminated ")
+
+ return text[start + len(prefix):end].strip()
+
+
+def scalar_value(value: str) -> dict[str, Any]:
+ if value.isdecimal():
+ return {"intValue": int(value)}
+
+ return {"stringValue": value}
+
+
+def span_kind(kind: str) -> str:
+ return {
+ "Client": "SPAN_KIND_CLIENT",
+ "Internal": "SPAN_KIND_INTERNAL",
+ "Producer": "SPAN_KIND_PRODUCER",
+ "Consumer": "SPAN_KIND_CONSUMER",
+ "Server": "SPAN_KIND_SERVER",
+ }.get(kind, "SPAN_KIND_UNSPECIFIED")
+
+
+def status_code(status: str) -> str:
+ return {
+ "Error": "STATUS_CODE_ERROR",
+ "Ok": "STATUS_CODE_OK",
+ "Unset": "STATUS_CODE_UNSET",
+ }.get(status, "STATUS_CODE_UNSET")
+
+
+def span_id(index: int) -> str:
+ return f"{index + 1:016x}"
+
+
+def trace_id(index: int) -> str:
+ return f"{index + 1:032x}"
+
+
+def render_otlp(report: dict[str, Any], version: str) -> dict[str, Any]:
+ if report.get("RuntimeMode") != "nativeaot":
+ fail("web API golden must be from NativeAOT runtime")
+
+ if report.get("Pass") is not True:
+ fail("web API golden report is not passing")
+
+ signals = report.get("Signals")
+ if not isinstance(signals, list):
+ fail("web API golden report is missing Signals[]")
+
+ actual_signals = sorted(str(signal.get("Signal")) for signal in signals)
+ if actual_signals != EXPECTED_SIGNALS:
+ fail(f"unexpected signal set: expected={EXPECTED_SIGNALS} actual={actual_signals}")
+
+ spans: list[dict[str, Any]] = []
+ for index, signal in enumerate(sorted(signals, key=lambda item: str(item["Signal"]))):
+ tags = signal.get("Tags")
+ if not isinstance(tags, dict):
+ fail(f"signal is missing Tags object: {signal}")
+
+ attributes = [
+ {"key": str(key), "value": scalar_value(str(value))}
+ for key, value in sorted(tags.items(), key=lambda pair: str(pair[0]))
+ ]
+ attributes.insert(0, {"key": "qyl.fixture.signal", "value": {"stringValue": str(signal["Signal"])}})
+
+ spans.append(
+ {
+ "traceId": trace_id(index),
+ "spanId": span_id(index),
+ "parentSpanId": "",
+ "name": str(signal["Name"]),
+ "kind": span_kind(str(signal["Kind"])),
+ "startTimeUnixNano": "0",
+ "endTimeUnixNano": "0",
+ "attributes": attributes,
+ "status": {"code": status_code(str(signal["Status"]))},
+ }
+ )
+
+ return {
+ "resourceSpans": [
+ {
+ "resource": {
+ "attributes": [
+ {"key": "service.name", "value": {"stringValue": "qyl-webapi-aot-demo"}},
+ {"key": "telemetry.sdk.language", "value": {"stringValue": "dotnet"}},
+ ]
+ },
+ "scopeSpans": [
+ {
+ "scope": {
+ "name": "Qyl.AutoInstrumentation",
+ "version": version,
+ },
+ "spans": spans,
+ }
+ ],
+ }
+ ]
+ }
+
+
+def canonical_json(value: dict[str, Any]) -> str:
+ return json.dumps(value, indent=2, sort_keys=True) + "\n"
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Verify canonical OTLP-shaped golden fixtures.")
+ parser.add_argument("--update-golden", action="store_true", help="Update committed OTLP-shaped fixtures.")
+ args = parser.parse_args()
+
+ if not WEBAPI_REPORT_PATH.exists():
+ fail(f"missing web API golden report: {WEBAPI_REPORT_PATH}")
+
+ report = json.loads(WEBAPI_REPORT_PATH.read_text(encoding="utf-8"))
+ rendered = canonical_json(render_otlp(report, read_version()))
+
+ if args.update_golden:
+ OTLP_GOLDEN_PATH.parent.mkdir(parents=True, exist_ok=True)
+ OTLP_GOLDEN_PATH.write_text(rendered, encoding="utf-8")
+ print("otlp-golden-fixtures-updated")
+ return
+
+ if not OTLP_GOLDEN_PATH.exists():
+ fail(f"missing OTLP-shaped golden fixture: {OTLP_GOLDEN_PATH}")
+
+ expected = OTLP_GOLDEN_PATH.read_text(encoding="utf-8")
+ if expected != rendered:
+ received = OTLP_GOLDEN_PATH.with_suffix(".received.json")
+ received.write_text(rendered, encoding="utf-8")
+ fail(
+ "OTLP-shaped golden fixture mismatch\n"
+ f"expected={OTLP_GOLDEN_PATH}\n"
+ f"received={received}"
+ )
+
+ print("otlp-golden-fixtures-ok")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/verify-package-layout.py b/tools/verify-package-layout.py
index 63e7e62..fe109c2 100644
--- a/tools/verify-package-layout.py
+++ b/tools/verify-package-layout.py
@@ -21,6 +21,8 @@
REQUIRED_PACKAGE_ENTRIES = {
"analyzers/dotnet/cs/Qyl.AutoInstrumentation.SourceGenerators.dll",
+ "build/Qyl.AutoInstrumentation.targets",
+ "build/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs",
"buildTransitive/Qyl.AutoInstrumentation.targets",
"buildTransitive/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs",
}
@@ -122,24 +124,26 @@ def pack_runtime(feed: Path, env: dict[str, str]) -> Path:
return package
-def verify_targets(text: str) -> None:
+def verify_targets(name: str, text: str) -> None:
for token in [
+ "QylAutoInstrumentationCoreBuildAssetsImported",
+ "_QylAutoInstrumentationCoreBuildAssetsAlreadyImported",
"$(InterceptorsNamespaces);Qyl.AutoInstrumentation.Generated",
"$(InterceptorsPreviewNamespaces);Qyl.AutoInstrumentation.Generated",
"Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs",
]:
if token not in text:
- fail(f"buildTransitive targets missing token: {token}")
+ fail(f"{name} missing token: {token}")
-def verify_intercepts_attribute(text: str) -> None:
+def verify_intercepts_attribute(name: str, text: str) -> None:
for token in [
"namespace System.Runtime.CompilerServices",
"internal sealed class InterceptsLocationAttribute",
"public InterceptsLocationAttribute(int version, string data)",
]:
if token not in text:
- fail(f"InterceptsLocation attribute source missing token: {token}")
+ fail(f"{name} missing token: {token}")
def verify_package(package: Path) -> None:
@@ -155,19 +159,31 @@ def verify_package(package: Path) -> None:
if token in lowered:
fail(f"package contains forbidden mechanism entry token {token}: {name}")
- targets = archive.read("buildTransitive/Qyl.AutoInstrumentation.targets").decode("utf-8")
- attribute = archive.read("buildTransitive/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs").decode("utf-8")
+ build_targets = archive.read("build/Qyl.AutoInstrumentation.targets").decode("utf-8")
+ build_attribute = archive.read("build/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs").decode("utf-8")
+ transitive_targets = archive.read("buildTransitive/Qyl.AutoInstrumentation.targets").decode("utf-8")
+ transitive_attribute = archive.read("buildTransitive/Qyl.AutoInstrumentation.InterceptsLocationAttribute.g.cs").decode("utf-8")
nuspec_name = next((name for name in names if name.endswith(".nuspec")), None)
if nuspec_name is None:
fail("package nuspec missing")
nuspec = archive.read(nuspec_name).decode("utf-8")
- verify_targets(targets)
- verify_intercepts_attribute(attribute)
+ if build_targets != transitive_targets:
+ fail("build and buildTransitive targets diverged")
+
+ if build_attribute != transitive_attribute:
+ fail("build and buildTransitive InterceptsLocationAttribute sources diverged")
+
+ verify_targets("build targets", build_targets)
+ verify_targets("buildTransitive targets", transitive_targets)
+ verify_intercepts_attribute("build InterceptsLocationAttribute source", build_attribute)
+ verify_intercepts_attribute("buildTransitive InterceptsLocationAttribute source", transitive_attribute)
for name, text in [
- ("targets", targets),
- ("InterceptsLocationAttribute", attribute),
+ ("build targets", build_targets),
+ ("build InterceptsLocationAttribute", build_attribute),
+ ("buildTransitive targets", transitive_targets),
+ ("buildTransitive InterceptsLocationAttribute", transitive_attribute),
("nuspec", nuspec),
]:
for token in FORBIDDEN_CONTENT_TOKENS:
diff --git a/tools/verify-projectreference-behavior.py b/tools/verify-projectreference-behavior.py
new file mode 100644
index 0000000..bf4b140
--- /dev/null
+++ b/tools/verify-projectreference-behavior.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import os
+import platform
+import subprocess
+import tempfile
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+CORE_PROJECT = ROOT / "src" / "Qyl.AutoInstrumentation" / "Qyl.AutoInstrumentation.csproj"
+GENERATOR_PROJECT = ROOT / "src" / "Qyl.AutoInstrumentation.SourceGenerators" / "Qyl.AutoInstrumentation.SourceGenerators.csproj"
+GENERATOR_DLL = ROOT / "src" / "Qyl.AutoInstrumentation.SourceGenerators" / "bin" / "Release" / "netstandard2.0" / "Qyl.AutoInstrumentation.SourceGenerators.dll"
+CORE_TARGETS = ROOT / "src" / "Qyl.AutoInstrumentation" / "buildTransitive" / "Qyl.AutoInstrumentation.targets"
+TARGET_FRAMEWORK = "net10.0"
+NUGET_ORG = "https://api.nuget.org/v3/index.json"
+
+
+PROGRAM = r'''
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using Qyl.AutoInstrumentation;
+
+var captured = new List();
+using var activityListener = new ActivityListener
+{
+ ShouldListenTo = static source => source.Name == QylActivitySource.Name,
+ Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded,
+ ActivityStopped = activity => captured.Add(activity),
+};
+
+ActivitySource.AddActivityListener(activityListener);
+
+var concreteLogger = new CapturingLogger();
+ILogger logger = concreteLogger;
+logger.Log(
+ LogLevel.Information,
+ new EventId(42, "projectreference-log"),
+ "projectreference-log",
+ exception: null,
+ static (state, exception) => exception is null ? state : state + ":" + exception.GetType().Name);
+
+Console.WriteLine("logger.calls=" + concreteLogger.Calls.ToString(System.Globalization.CultureInfo.InvariantCulture));
+Console.WriteLine("logger.last=" + concreteLogger.Last);
+Console.WriteLine("activity.count=" + captured.Count.ToString(System.Globalization.CultureInfo.InvariantCulture));
+
+if (captured.Count == 1)
+{
+ var activity = captured[0];
+ var tags = activity.TagObjects.ToDictionary(
+ static tag => tag.Key,
+ static tag => Convert.ToString(tag.Value, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty,
+ StringComparer.Ordinal);
+ tags.TryGetValue(QylSemanticAttributes.QylInstrumentationDomain, out var domain);
+ tags.TryGetValue(QylSemanticAttributes.LogSeverity, out var severity);
+
+ Console.WriteLine("activity.name=" + activity.DisplayName);
+ Console.WriteLine("activity.kind=" + activity.Kind);
+ Console.WriteLine(QylSemanticAttributes.QylInstrumentationDomain + "=" + domain);
+ Console.WriteLine(QylSemanticAttributes.LogSeverity + "=" + severity);
+}
+
+return 0;
+
+internal sealed class CapturingLogger : ILogger
+{
+ public int Calls { get; private set; }
+
+ public string Last { get; private set; } = string.Empty;
+
+ public IDisposable? BeginScope(TState state) where TState : notnull => null;
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(
+ LogLevel logLevel,
+ EventId eventId,
+ TState state,
+ Exception? exception,
+ Func formatter)
+ {
+ Calls++;
+ Last = logLevel + ":" + eventId.Id.ToString(System.Globalization.CultureInfo.InvariantCulture) + ":" + formatter(state, exception);
+ }
+}
+'''
+
+
+EXPECTED_GOLDEN = """logger.calls=1
+logger.last=Information:42:projectreference-log
+activity.count=1
+activity.name=ILogger log
+activity.kind=Internal
+qyl.instrumentation.domain=log.ilogger
+log.severity=Information
+"""
+
+
+def fail(message: str) -> None:
+ raise SystemExit(message)
+
+
+def clean_env() -> dict[str, str]:
+ env = dict(os.environ)
+ for key in list(env):
+ if key.startswith("OTEL_") or key.startswith("QYL_"):
+ del env[key]
+
+ env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
+ env["DOTNET_NOLOGO"] = "1"
+ return env
+
+
+def run_checked(command: list[str], cwd: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
+ completed = subprocess.run(
+ command,
+ cwd=cwd,
+ env=env,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=False,
+ )
+ if completed.returncode != 0:
+ fail(
+ "command failed: "
+ + " ".join(command)
+ + f"\nexit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}"
+ )
+
+ return completed
+
+
+def runtime_identifier() -> str:
+ system = platform.system().lower()
+ machine = platform.machine().lower()
+ if system == "darwin":
+ return "osx-arm64" if machine in {"arm64", "aarch64"} else "osx-x64"
+ if system == "linux":
+ return "linux-arm64" if machine in {"arm64", "aarch64"} else "linux-x64"
+ if system == "windows":
+ return "win-arm64" if machine in {"arm64", "aarch64"} else "win-x64"
+
+ fail(f"unsupported NativeAOT gate platform: {platform.system()} {platform.machine()}")
+
+
+def native_executable_name() -> str:
+ return "Consumer.exe" if platform.system().lower() == "windows" else "Consumer"
+
+
+def write_project(directory: Path, packages: Path) -> Path:
+ directory.mkdir(parents=True)
+ project_path = directory / "Consumer.csproj"
+ project_path.write_text(
+ f'''
+
+ Exe
+ {TARGET_FRAMEWORK}
+ enable
+ enable
+ {NUGET_ORG}
+ {packages}
+ true
+ true
+ Generated
+
+
+
+
+
+
+
+
+
+
+
+
+''',
+ encoding="utf-8",
+ )
+ (directory / "Program.cs").write_text(PROGRAM, encoding="utf-8")
+ return project_path
+
+
+def verify_golden(label: str, stdout: str) -> None:
+ if stdout != EXPECTED_GOLDEN:
+ fail(f"{label} output mismatch\nexpected:\n{EXPECTED_GOLDEN}\nactual:\n{stdout}")
+
+
+def verify_generated_interceptor_source(directory: Path) -> None:
+ generated_files = sorted((directory / "Generated").rglob("QylAutoInstrumentation.Interceptors.g.cs"))
+ if len(generated_files) != 1:
+ fail(f"expected exactly one generated interceptor source file, found {len(generated_files)}")
+
+ text = generated_files[0].read_text(encoding="utf-8")
+ for token in [
+ "#nullable enable",
+ "Qyl.AutoInstrumentation.Generated",
+ "InterceptsLocationAttribute",
+ "global::Microsoft.Extensions.Logging.ILogger",
+ ]:
+ if token not in text:
+ fail(f"generated interceptor source missing token: {token}")
+
+
+def verify_managed(project: Path, directory: Path, env: dict[str, str]) -> None:
+ run_checked(["dotnet", "build", str(project), "-c", "Release", "-v", "quiet"], directory, env)
+ verify_generated_interceptor_source(directory)
+
+ app_dll = directory / "bin" / "Release" / TARGET_FRAMEWORK / "Consumer.dll"
+ completed = run_checked(["dotnet", str(app_dll)], directory, env)
+ verify_golden("managed ProjectReference consumer", completed.stdout)
+
+
+def verify_nativeaot(project: Path, directory: Path, env: dict[str, str]) -> None:
+ rid = runtime_identifier()
+ run_checked(
+ [
+ "dotnet",
+ "publish",
+ str(project),
+ "-c",
+ "Release",
+ "-r",
+ rid,
+ "-p:PublishAot=true",
+ "-p:SelfContained=true",
+ "-p:InvariantGlobalization=true",
+ "-v",
+ "quiet",
+ ],
+ directory,
+ env,
+ )
+
+ native_app = directory / "bin" / "Release" / TARGET_FRAMEWORK / rid / "publish" / native_executable_name()
+ completed = run_checked([str(native_app)], directory, env)
+ verify_golden("NativeAOT ProjectReference consumer", completed.stdout)
+
+
+def main() -> None:
+ env = clean_env()
+ run_checked(["dotnet", "build", str(GENERATOR_PROJECT), "-c", "Release", "-v", "quiet"], ROOT, env)
+ with tempfile.TemporaryDirectory(prefix="qyl-projectreference-consumer-") as temp:
+ directory = Path(temp) / "consumer"
+ packages = Path(temp) / "packages"
+ project = write_project(directory, packages)
+ verify_managed(project, directory, env)
+ verify_nativeaot(project, directory, env)
+
+ print("projectreference-behavior-ok")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/verify-public-api-baseline.py b/tools/verify-public-api-baseline.py
new file mode 100644
index 0000000..0b02145
--- /dev/null
+++ b/tools/verify-public-api-baseline.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+PROPS = ROOT / "Directory.Build.props"
+
+PUBLIC_API_PROJECTS = [
+ ROOT / "src" / "Qyl.AutoInstrumentation",
+ ROOT / "src" / "Qyl.AutoInstrumentation.DiagnosticListeners",
+ ROOT / "src" / "Qyl.AutoInstrumentation.Hosting",
+ ROOT / "src" / "Qyl.AutoInstrumentation.EntityFrameworkCore",
+ ROOT / "src" / "Qyl.AutoInstrumentation.SqlClient",
+]
+
+EXCLUDED_PROJECTS = [
+ "Qyl.AutoInstrumentation.SourceGenerators",
+ "Qyl.LiveInstrumentationDemo",
+ "Qyl.RealAspNetCoreDemo",
+ "Qyl.RealEfCoreDemo",
+ "Qyl.RealGrpcClientDemo",
+ "Qyl.RealHttpClientDemo",
+ "Qyl.RealSqlClientDemo",
+]
+
+
+def fail(message: str) -> None:
+ raise SystemExit(message)
+
+
+def verify_props() -> None:
+ text = PROPS.read_text(encoding="utf-8")
+ required_tokens = [
+ "3.3.4",
+ "Microsoft.CodeAnalysis.PublicApiAnalyzers",
+ "QylEnablePublicApiAnalyzers",
+ ]
+ for token in required_tokens:
+ if token not in text:
+ fail(f"Directory.Build.props missing PublicAPI token: {token}")
+
+ for project in EXCLUDED_PROJECTS:
+ if project in text and project != "Qyl.AutoInstrumentation.SourceGenerators":
+ fail(f"Directory.Build.props should not explicitly enable PublicAPI analyzers for {project}")
+
+
+def verify_api_file(path: Path, require_entries: bool) -> None:
+ if not path.exists():
+ fail(f"missing PublicAPI file: {path}")
+
+ lines = path.read_text(encoding="utf-8").splitlines()
+ if not lines or lines[0] != "#nullable enable":
+ fail(f"{path} must start with #nullable enable")
+
+ entries = [line for line in lines[1:] if line.strip()]
+ if require_entries and not entries:
+ fail(f"{path} must contain shipped public API entries")
+
+
+def main() -> None:
+ verify_props()
+ for project in PUBLIC_API_PROJECTS:
+ verify_api_file(project / "PublicAPI.Shipped.txt", require_entries=True)
+ verify_api_file(project / "PublicAPI.Unshipped.txt", require_entries=False)
+
+ print("public-api-baseline-ok")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tools/verify-webapi-aot-demo.py b/tools/verify-webapi-aot-demo.py
new file mode 100644
index 0000000..9e367d6
--- /dev/null
+++ b/tools/verify-webapi-aot-demo.py
@@ -0,0 +1,519 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import platform
+import re
+import subprocess
+import tempfile
+from pathlib import Path
+
+try:
+ import fcntl
+except ImportError:
+ fcntl = None
+
+
+ROOT = Path(__file__).resolve().parents[1]
+PACK_LOCK_PATH = Path(tempfile.gettempdir()) / "qyl-dotnet-autoinstrumentation-pack.lock"
+PROPS_PATH = ROOT / "Directory.Build.props"
+GOLDEN_PATH = ROOT / "tools" / "Qyl.AutoInstrumentation.WebApiAotDemo" / "golden" / "report.json"
+NUGET_ORG = "https://api.nuget.org/v3/index.json"
+TARGET_FRAMEWORK = "net10.0"
+
+PROJECTS_TO_PACK = [
+ ROOT / "src" / "Qyl.AutoInstrumentation" / "Qyl.AutoInstrumentation.csproj",
+ ROOT / "src" / "Qyl.AutoInstrumentation.DiagnosticListeners" / "Qyl.AutoInstrumentation.DiagnosticListeners.csproj",
+ ROOT / "src" / "Qyl.AutoInstrumentation.Hosting" / "Qyl.AutoInstrumentation.Hosting.csproj",
+ ROOT / "src" / "Qyl.AutoInstrumentation.EntityFrameworkCore" / "Qyl.AutoInstrumentation.EntityFrameworkCore.csproj",
+ ROOT / "src" / "Qyl.AutoInstrumentation.SqlClient" / "Qyl.AutoInstrumentation.SqlClient.csproj",
+]
+
+EFCORE_COMPILED_MODEL_SOURCES = [
+ ROOT / "demos" / "Qyl.RealEfCoreDemo" / "ProbeContext.cs",
+ ROOT / "demos" / "Qyl.RealEfCoreDemo" / "CompiledModels" / "ProbeContextAssemblyAttributes.cs",
+ ROOT / "demos" / "Qyl.RealEfCoreDemo" / "CompiledModels" / "ProbeContextModel.cs",
+ ROOT / "demos" / "Qyl.RealEfCoreDemo" / "CompiledModels" / "ProbeContextModelBuilder.cs",
+ ROOT / "demos" / "Qyl.RealEfCoreDemo" / "CompiledModels" / "ProbeItemEntityType.cs",
+ ROOT / "demos" / "Qyl.RealEfCoreDemo" / "CompiledModels" / "ProbeItemUnsafeAccessors.cs",
+]
+
+PROGRAM = r'''
+using System.Diagnostics;
+using System.Globalization;
+using System.Net;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Data.SqlClient;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+using Qyl.AutoInstrumentation;
+using Qyl.RealEfCoreDemo;
+
+var captured = new List();
+using var listener = new ActivityListener
+{
+ ShouldListenTo = static source => source.Name == QylActivitySource.Name,
+ Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded,
+ ActivityStopped = activity => captured.Add(CapturedActivity.From(activity)),
+};
+
+ActivitySource.AddActivityListener(listener);
+
+using var downstream = new HttpClient(new StubHandler())
+{
+ BaseAddress = new Uri("https://qyl-webapi.invalid"),
+};
+using (await downstream.GetAsync("/downstream?secret=redacted"))
+{
+}
+
+await using (var sqlConnection = new SqlConnection())
+{
+ await using var sqlCommand = sqlConnection.CreateCommand();
+ sqlCommand.CommandText = "SELECT 1";
+ try
+ {
+ _ = await sqlCommand.ExecuteScalarAsync();
+ }
+ catch (InvalidOperationException)
+ {
+ }
+}
+
+await using var sqlite = new SqliteConnection("Data Source=:memory:");
+await sqlite.OpenAsync();
+await CreateSchemaAsync(sqlite);
+
+var builder = WebApplication.CreateBuilder(args);
+builder.WebHost.UseUrls("http://127.0.0.1:0");
+builder.WebHost.SuppressStatusMessages(true);
+builder.Logging.ClearProviders();
+var app = builder.Build();
+
+app.MapGet("/probe/{id:int}", async () =>
+{
+ await using (var db = new ProbeContext(sqlite))
+ {
+ await db.Database.ExecuteSqlRawAsync("INSERT INTO Items (Name) VALUES ('webapi')");
+ }
+
+ return Results.NoContent();
+});
+
+await app.StartAsync();
+
+try
+{
+ var address = app.Urls.Single();
+ using var client = new HttpClient();
+ using (await client.GetAsync(address + "/probe/42?secret=redacted"))
+ {
+ }
+}
+finally
+{
+ await app.StopAsync();
+}
+
+var report = WebApiAotReport.Create(captured.ToArray());
+var json = JsonSerializer.Serialize(report, WebApiAotJsonContext.Default.WebApiAotReport);
+Console.WriteLine(json);
+
+return report.Pass ? 0 : 1;
+
+static async Task CreateSchemaAsync(SqliteConnection connection)
+{
+ await using var command = connection.CreateCommand();
+ command.CommandText = """
+ CREATE TABLE Items (
+ Id INTEGER NOT NULL CONSTRAINT PK_Items PRIMARY KEY AUTOINCREMENT,
+ Name TEXT NOT NULL
+ );
+ """;
+ await command.ExecuteNonQueryAsync();
+}
+
+internal sealed class StubHandler : HttpMessageHandler
+{
+ protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.NoContent)
+ {
+ RequestMessage = request,
+ });
+}
+
+internal sealed record CapturedActivity(
+ string Name,
+ string Kind,
+ string Status,
+ IReadOnlyDictionary Tags)
+{
+ public static CapturedActivity From(Activity activity)
+ => new(
+ activity.DisplayName,
+ activity.Kind.ToString(),
+ activity.Status.ToString(),
+ activity.TagObjects.ToDictionary(
+ static tag => tag.Key,
+ static tag => Convert.ToString(tag.Value, CultureInfo.InvariantCulture) ?? string.Empty,
+ StringComparer.Ordinal));
+}
+
+internal sealed record MatchedSignal(
+ string Signal,
+ string Name,
+ string Kind,
+ string Status,
+ IReadOnlyDictionary Tags);
+
+internal sealed record WebApiAotReport(
+ string RuntimeMode,
+ bool Pass,
+ string[] Failures,
+ MatchedSignal[] Signals,
+ MatchedSignal[] Activities)
+{
+ public static WebApiAotReport Create(CapturedActivity[] activities)
+ {
+ var failures = new List();
+ var signals = new List();
+
+ AddRequired(signals, failures, "aspnetcore.server", activities.FirstOrDefault(static activity =>
+ HasTag(activity, "qyl.instrumentation.domain", "http.server") &&
+ HasTag(activity, "http.route", "/probe/{id:int}")));
+
+ AddRequired(signals, failures, "httpclient.self", activities.FirstOrDefault(static activity =>
+ HasTag(activity, "qyl.instrumentation.domain", "http.client") &&
+ HasTag(activity, "server.address", "127.0.0.1")));
+
+ AddRequired(signals, failures, "httpclient.downstream", activities.FirstOrDefault(static activity =>
+ HasTag(activity, "qyl.instrumentation.domain", "http.client") &&
+ HasTag(activity, "http.request.method", "GET") &&
+ HasTag(activity, "http.response.status_code", "204") &&
+ !activity.Tags.ContainsKey("server.address")));
+
+ AddRequired(signals, failures, "efcore.sqlite", activities.FirstOrDefault(static activity =>
+ HasTag(activity, "qyl.instrumentation.domain", "db.efcore")));
+
+ AddRequired(signals, failures, "sqlclient.command", activities.FirstOrDefault(static activity =>
+ HasTag(activity, "qyl.instrumentation.domain", "db.sqlclient") &&
+ HasTag(activity, "db.operation.name", "SELECT") &&
+ HasTag(activity, "error.type", "System.InvalidOperationException")));
+
+ foreach (var signal in signals)
+ {
+ if (signal.Tags.ContainsKey("url.full") ||
+ signal.Tags.ContainsKey("url.path") ||
+ signal.Tags.ContainsKey("db.query.text"))
+ {
+ failures.Add("sensitive raw value leaked in " + signal.Signal);
+ }
+ }
+
+ return new WebApiAotReport(
+ RuntimeFeature.IsDynamicCodeSupported ? "dynamic-code-supported" : "nativeaot",
+ failures.Count is 0,
+ failures.ToArray(),
+ signals.OrderBy(static signal => signal.Signal, StringComparer.Ordinal).ToArray(),
+ activities
+ .Select(static activity => new MatchedSignal("activity", activity.Name, activity.Kind, activity.Status, Canonicalize(activity.Tags)))
+ .OrderBy(static signal => signal.Name, StringComparer.Ordinal)
+ .ThenBy(static signal => signal.Kind, StringComparer.Ordinal)
+ .ThenBy(static signal => string.Join(",", signal.Tags.Select(static pair => pair.Key + "=" + pair.Value)), StringComparer.Ordinal)
+ .ToArray());
+ }
+
+ private static void AddRequired(
+ ICollection signals,
+ ICollection failures,
+ string signal,
+ CapturedActivity? activity)
+ {
+ if (activity is null)
+ {
+ failures.Add("missing " + signal);
+ return;
+ }
+
+ signals.Add(new MatchedSignal(signal, activity.Name, activity.Kind, activity.Status, Canonicalize(activity.Tags)));
+ }
+
+ private static bool HasTag(CapturedActivity activity, string key, string expected)
+ => activity.Tags.TryGetValue(key, out var actual) &&
+ StringComparer.Ordinal.Equals(actual, expected);
+
+ private static IReadOnlyDictionary Canonicalize(IReadOnlyDictionary tags)
+ {
+ var keep = new[]
+ {
+ "qyl.instrumentation.domain",
+ "http.request.method",
+ "http.route",
+ "http.response.status_code",
+ "server.address",
+ "server.port",
+ "db.system",
+ "db.operation.name",
+ "db.query.summary",
+ "error.type",
+ };
+ var result = new SortedDictionary(StringComparer.Ordinal);
+ foreach (var key in keep)
+ {
+ if (!tags.TryGetValue(key, out var value))
+ continue;
+
+ result[key] = key is "server.port" ? "" : value;
+ }
+
+ return result;
+ }
+}
+
+[JsonSerializable(typeof(WebApiAotReport))]
+[JsonSourceGenerationOptions(WriteIndented = true)]
+internal sealed partial class WebApiAotJsonContext : JsonSerializerContext;
+'''
+
+
+def fail(message: str) -> None:
+ raise SystemExit(message)
+
+
+def clean_env() -> dict[str, str]:
+ env = dict(os.environ)
+ for key in list(env):
+ if key.startswith("OTEL_") or key.startswith("QYL_"):
+ del env[key]
+
+ env["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1"
+ env["DOTNET_NOLOGO"] = "1"
+ return env
+
+
+def read_version() -> str:
+ text = PROPS_PATH.read_text(encoding="utf-8")
+ prefix = ""
+ suffix = ""
+ start = text.find(prefix)
+ if start < 0:
+ fail("Directory.Build.props is missing ")
+
+ end = text.find(suffix, start)
+ if end < 0:
+ fail("Directory.Build.props has unterminated ")
+
+ return text[start + len(prefix):end].strip()
+
+
+def runtime_identifier() -> str:
+ system = platform.system().lower()
+ machine = platform.machine().lower()
+ if system == "darwin":
+ return "osx-arm64" if machine in {"arm64", "aarch64"} else "osx-x64"
+ if system == "linux":
+ return "linux-arm64" if machine in {"arm64", "aarch64"} else "linux-x64"
+ if system == "windows":
+ return "win-arm64" if machine in {"arm64", "aarch64"} else "win-x64"
+
+ fail(f"unsupported NativeAOT web API gate platform: {platform.system()} {platform.machine()}")
+
+
+def run_checked(command: list[str], cwd: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
+ completed = subprocess.run(
+ command,
+ cwd=cwd,
+ env=env,
+ text=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ check=False,
+ )
+ if completed.returncode != 0:
+ fail(
+ "command failed: "
+ + " ".join(command)
+ + f"\nexit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}"
+ )
+
+ return completed
+
+
+def pack_runtime(feed: Path, env: dict[str, str]) -> None:
+ feed.mkdir(parents=True)
+ with PACK_LOCK_PATH.open("w", encoding="utf-8") as lock:
+ if fcntl is not None:
+ fcntl.flock(lock, fcntl.LOCK_EX)
+ try:
+ for project in PROJECTS_TO_PACK:
+ run_checked(
+ ["dotnet", "pack", str(project), "-c", "Release", "-o", str(feed), "-v", "quiet"],
+ ROOT,
+ env,
+ )
+ finally:
+ if fcntl is not None:
+ fcntl.flock(lock, fcntl.LOCK_UN)
+
+
+def write_project(directory: Path, feed: Path, packages: Path, version: str) -> Path:
+ directory.mkdir(parents=True)
+ project_path = directory / "WebApiAotDemo.csproj"
+ compile_items = "\n".join(
+ f'