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' ' + for path in EFCORE_COMPILED_MODEL_SOURCES + ) + project_path.write_text( + f''' + + Exe + {TARGET_FRAMEWORK} + enable + enable + {feed};{NUGET_ORG} + {packages} + true + $(DefineConstants);USE_COMPILED_MODEL + + + + + + + + + + + +{compile_items} + + +''', + encoding="utf-8", + ) + (directory / "Program.cs").write_text(PROGRAM, encoding="utf-8") + return project_path + + +def publish_nativeaot(project: Path, output: Path, log: Path, env: dict[str, str]) -> Path: + completed = subprocess.run( + [ + "dotnet", + "publish", + str(project), + "-c", + "Release", + "-r", + runtime_identifier(), + "-p:PublishAot=true", + "-p:TreatWarningsAsErrors=false", + "--self-contained", + "true", + "-o", + str(output), + "-v", + "quiet", + ], + cwd=project.parent, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=False, + ) + log.write_text(completed.stdout, encoding="utf-8") + if completed.returncode != 0: + fail( + "NativeAOT web API publish failed\n" + f"exit={completed.returncode}\nlog={log}\n{completed.stdout}" + ) + + qyl_warnings = [ + line for line in completed.stdout.splitlines() + if re.search(r"\b(?:IL2[0-9]{3}|IL3[0-9]{3}|IL4[0-9]{3}|CA[0-9]{4})\b", line) and + "Qyl.AutoInstrumentation" in line + ] + if qyl_warnings: + fail("NativeAOT web API publish emitted qyl-owned analyzer warnings:\n" + "\n".join(qyl_warnings)) + + executable = output / ("WebApiAotDemo.exe" if platform.system().lower() == "windows" else "WebApiAotDemo") + if not executable.exists(): + fail(f"NativeAOT web API executable missing: {executable}") + + return executable + + +def run_executable(executable: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [str(executable)], + cwd=executable.parent, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + +def verify_or_update_golden(stdout: str, update: bool) -> None: + parsed = json.loads(stdout) + canonical = json.dumps(parsed, indent=2, sort_keys=True) + "\n" + if update: + GOLDEN_PATH.parent.mkdir(parents=True, exist_ok=True) + GOLDEN_PATH.write_text(canonical, encoding="utf-8") + return + + if not GOLDEN_PATH.exists(): + fail(f"missing web API AOT golden: {GOLDEN_PATH}") + + expected = GOLDEN_PATH.read_text(encoding="utf-8") + if canonical != expected: + received = GOLDEN_PATH.with_suffix(".received.json") + received.write_text(canonical, encoding="utf-8") + fail( + "web API AOT golden mismatch\n" + f"expected={GOLDEN_PATH}\n" + f"received={received}" + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Publish and run the NativeAOT web API instrumentation demo.") + parser.add_argument("--update-golden", action="store_true", help="Update the committed canonical output.") + args = parser.parse_args() + + env = clean_env() + version = read_version() + with tempfile.TemporaryDirectory(prefix="qyl-webapi-aot-demo-") as temp: + root = Path(temp) + feed = root / "feed" + packages = root / "packages" + publish = root / "publish" + publish_log = root / "publish.log" + pack_runtime(feed, env) + project = write_project(root / "consumer", feed, packages, version) + executable = publish_nativeaot(project, publish, publish_log, env) + completed = run_executable(executable, env) + + if completed.returncode != 0: + fail( + "NativeAOT web API demo failed\n" + f"exit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}" + ) + if completed.stderr: + fail(f"NativeAOT web API demo wrote stderr:\n{completed.stderr}") + + verify_or_update_golden(completed.stdout, args.update_golden) + print("webapi-aot-demo-ok qyl_warnings=0") + + +if __name__ == "__main__": + main() diff --git a/tools/verify-xml-doc-enforcement.py b/tools/verify-xml-doc-enforcement.py new file mode 100644 index 0000000..cf08fe4 --- /dev/null +++ b/tools/verify-xml-doc-enforcement.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE_GENERATOR_PROJECT = ROOT / "src" / "Qyl.AutoInstrumentation.SourceGenerators" / "Qyl.AutoInstrumentation.SourceGenerators.csproj" +RUNTIME_PROJECT = ROOT / "src" / "Qyl.AutoInstrumentation" / "Qyl.AutoInstrumentation.csproj" + + +REQUIRED_TOKENS = [ + "true", + "$(WarningsAsErrors);CS1591", +] + + +def fail(message: str) -> None: + raise SystemExit(message) + + +def verify_project_contract(project: Path, label: str) -> None: + text = project.read_text(encoding="utf-8") + for token in REQUIRED_TOKENS: + if token not in text: + fail(f"{label} XML-doc enforcement token missing: {token}") + + +def verify_project_build(project: Path, label: str) -> None: + completed = subprocess.run( + [ + "dotnet", + "build", + str(project), + "-c", + "Release", + "-v", + "quiet", + ], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if completed.returncode != 0: + fail( + f"{label} XML-doc enforcement build failed\n" + f"exit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}" + ) + + +def main() -> None: + verify_project_contract(SOURCE_GENERATOR_PROJECT, "source generator") + verify_project_contract(RUNTIME_PROJECT, "runtime") + verify_project_build(SOURCE_GENERATOR_PROJECT, "source generator") + verify_project_build(RUNTIME_PROJECT, "runtime") + print("xml-doc-enforcement-ok scope=source-generator,runtime") + + +if __name__ == "__main__": + main()