diff --git a/AGENTS.md b/AGENTS.md index 578fa60..8e2a789 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ The package family is public. Existing NuGet artifacts are immutable. Make intentional breaking convergence in a new major version, migrate known consumers, and do not add compatibility shims without a proven external requirement. -Three API categories are explicit, and 6.0.0 fixed their concrete form. Preserve it: +The following API/ABI categories define the active 8.0 architecture. Preserve them: 1. A small supported user API for bootstrap and configuration: Hosting `Boot()`/`AddQylAutoInstrumentation(...)`, `Qyl.Sdk` `AddQyl(...)`/`QylSdkOptions`, @@ -29,15 +29,24 @@ Three API categories are explicit, and 6.0.0 fixed their concrete form. Preserve `[EditorBrowsable(EditorBrowsableState.Never)]`, anchored by the `QylGeneratedCodeAbi.V8` const that every generated interceptor file references so a generator/runtime ABI mismatch fails compilation. That namespace, the anchor, and - the `V` bump on a breaking ABI change are load-bearing: the snapshot and - invariant verifiers pin these exact tokens. Do not rename or re-derive them. + the `V` bump on a breaking ABI change are load-bearing: the version-sync and + generated-source snapshot verifiers pin the exact token. Do not rename or re-derive it. Generated code must not reference `QylAutoInstrumentationOptions` or `QylInstrumentationDomains` — gate opt-ins at the policy type and emit domain names as literals. -3. Internal implementation types, semantic helpers, listeners, and runtime state — +3. A narrow generated-code/build-transitive package bootstrap ABI, also under the + `Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode` namespace and hidden with + `[EditorBrowsable(EditorBrowsableState.Never)]`: + `EntityFrameworkCoreAutoInstrumentationBootstrap` and + `SqlClientAutoInstrumentationBootstrap`. They are public because generated source + compiled into consumer assemblies calls them; they are package plumbing, not user + configuration APIs. +4. Internal implementation types, semantic helpers, listeners, meter registration + inventory, and runtime state — everything else, including `QylSemanticAttributes`, `QylActivityNames`, - `QylActivitySource`, `QylAutoInstrumentationOptions`, and `QylInstrumentationDomains`. - Reach across assemblies with IVT, never by widening a type to public. + `QylActivitySource`, `QylAutoInstrumentationOptions`, `QylInstrumentationDomains`, + and `QylMetricMeters`. Reach across assemblies with IVT, never by widening a type to + public. Cross-assembly accessibility does not make generator ABI a user-facing product API. Any Qyl-specific client-visible request, response, event, or error contract belongs @@ -55,10 +64,10 @@ Two generated namespaces exist, four characters apart. Do not conflate them: - `Qyl.OpenTelemetry.AutoInstrumentation.Generated` — where the generator emits interceptor methods, and the value `buildTransitive` adds to - `InterceptorsNamespaces`. Compiler-facing wiring; 6.0.0 did not move it. + `InterceptorsNamespaces`. Compiler-facing wiring; it remains load-bearing in 8.0. - `Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode` — the runtime ABI helpers - (`QylIntercepted*`, `QylMetricMeters`, the `QylGeneratedCodeAbi.V8` anchor) that - emitted interceptors call into. + (`QylIntercepted*` and the `QylGeneratedCodeAbi.V8` anchor) that emitted + interceptors call into. Emitted code lives in the first and delegates to the second. Renaming either side — or "fixing" the near-duplicate names — breaks the build assets or the pinned @@ -88,8 +97,8 @@ own the same call site. generated coverage matrix and conformance artifacts. Change inputs/generators, regenerate, and commit the outputs together. - The coverage matrix distinguishes runtime evidence from configuration bindings and - unsupported rows. Never summarize all 60 contract rows as 60 runtime-implemented or - NativeAOT-verified integrations. + unsupported rows. Never summarize the 60-row upstream contract or the separate + qyl-native promises as universally runtime-implemented or NativeAOT-verified. - Missing runtime values stay missing. Keep span names and metric dimensions bounded; sensitive values follow the repository's explicit redaction/opt-in controls. diff --git a/CHANGELOG.md b/CHANGELOG.md index d0e2bd7..83fd5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,82 @@ stack line and are owned by `` in `Directory.Build.props`. CI packs tha proves the indexed packages in clean managed and NativeAOT consumers, and only then creates the matching `v*` tag and GitHub release. +## [8.0.0] - 2026-07-19 + +Intentional breaking convergence plus version-pinned telemetry paths. The new AI, +MCP, and CoreWCF entries below describe only the exact library versions and hooks +exercised by repository evidence; they are not provider- or protocol-wide claims. +The exact `ModelContextProtocol` 1.4.1 client/server path has strict NativeAOT +evidence; the other new paths have managed evidence only. + +### Breaking changes + +- **BREAKING:** the generated-code ABI anchor moved from + `QylGeneratedCodeAbi.V6` to `QylGeneratedCodeAbi.V8`. Generated interceptors now + require the V8 runtime anchor, so mixing an 8.x generator with a 6.x runtime (or + the reverse) fails compilation. +- **BREAKING:** deleted the orphan `QylInterceptedWcfCore` generated-code helper and + its unused policy/domain/name tail. No generator called it; CoreWCF server spans + now use the official `CoreWCF.Primitives` `ActivitySource` path. +- **BREAKING:** deleted the generated HttpWebRequest, ASP.NET endpoint-map, + EF Core, Azure client, and `MeterProviderBuilder.AddMeter` interceptor lanes and + their unused generated-code helpers. Runtime listeners, specialist packages, + explicit SDK meter registration, and first-party Azure sources are now the single + owners of those signals. The HttpClient and gRPC client interceptor lanes remain + the call-site owners of outbound HTTP/gRPC spans, header/metadata capture, and + URL redaction; their completion listeners defer per signal ownership. +- **BREAKING:** deleted the DiagnosticListeners package's synthetic `qyl.db.efcore` and + `qyl.db.sqlclient` demo listeners. Real EF Core and Microsoft.Data.SqlClient events + are owned only by their dependency-isolating specialist packages. +- **BREAKING:** deleted the custom HTTP duration producer; the + `System.Net.Http/http.client.request.duration` instrument is authoritative. The + NServiceBus qyl meter is now `Qyl.OpenTelemetry.AutoInstrumentation.NServiceBus`, + and `Qyl.Sdk` no longer force-registers the library-native `Npgsql`, + `NServiceBus.Core`, and `NServiceBus.Core.Pipeline.Incoming` meters — consumers + that want those instruments exported register them via + `QylSdkOptions.AdditionalMeters`. MCP metrics are likewise outside the 8.0.0 + contract (mcp spans are registered; the `Experimental.ModelContextProtocol` meter + is consumer-registered). +- **BREAKING:** renamed the public diagnostic extension base + `DiagnosticListenerSubscriber` to `QylDiagnosticListenerSubscriber`. The five + concrete ASP.NET Core, EF Core, gRPC client, HttpClient, and SqlClient listener + types are internal; the abstract qyl-prefixed subscriber remains the supported + extension surface. + +### Changed + +- Every emitted interceptor now carries one adjacent machine-readable JSON manifest + containing its interceptor kind, signal, instrumentation ID, additional metric + IDs, and canonically derived contract keys. Contract verification reads emitted + generated output instead of reconstructing ownership from generator source text, + and proves that every remaining catalog kind appears in the checked artifact. +- Azure SDK instrumentation now uses the SDK's first-party `Azure.*` + `ActivitySource` path. Bootstrap enables `Azure.Experimental.EnableActivitySource`; + `Qyl.Sdk` subscribes the wildcard and normalizes the bounded qyl domain/name/error + contract before export. +- The `Qyl.Sdk` → Hosting → core NuGet dependency chain now preserves build and + analyzer assets. A clean consumer references only `Qyl.Sdk`, executes `AddQyl`, + requires an emitted interceptor, and runs the same payload as managed code and + NativeAOT. +- `Qyl.Sdk` registers the following version-pinned, environment-switchable telemetry paths: + - `Microsoft.Extensions.AI` 10.8.0 traces and metrics through the application's + explicit `UseOpenTelemetry()` chat-client wrapper; + - `Microsoft.Agents.AI` 1.13.0 traces and metrics through the application's + explicit `UseOpenTelemetry()` agent wrapper; + - `Microsoft.Agents.AI.Workflows` 1.13.0 traces through the application's explicit + `WithOpenTelemetry()` workflow hook; + - `ModelContextProtocol` 1.4.1 automatic official client/server traces, verified + on the exact path under both managed execution and strict NativeAOT; and + - `CoreWCF.Http` 1.9.1 managed server traces from `CoreWCF.Primitives`. +- The new signal-specific IDs are `MICROSOFTEXTENSIONSAI`, `MICROSOFTAGENTSAI`, + `MICROSOFTAGENTSAIWORKFLOWS`, and `MCP`; CoreWCF uses `WCFCORE`. Each follows the + standard `OTEL_DOTNET_AUTO_{SIGNAL}_{ID}_INSTRUMENTATION_ENABLED` switch shape. +- MCP is traces-only in 8.0. Its metrics are deliberately not registered because the + official instruments attach dynamic tool and resource names as dimensions, which + conflicts with qyl's bounded-cardinality policy. +- Direct OpenAI SDK instrumentation, raw Anthropic SDK instrumentation, + `Azure.AI.Inference`, Amazon Bedrock, and A2A are not claimed by 8.0. + ## [6.0.0] - 2026-07-18 Intentional pre-consumer convergence release: the last cheap breaking window diff --git a/Directory.Build.props b/Directory.Build.props index 7d0ef4a..ddc6cbc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -21,7 +21,7 @@ true true - 6.0.0 + 8.0.0 qyl https://github.com/ANcpLua/Qyl.OpenTelemetry.AutoInstrumentation git diff --git a/Directory.Packages.props b/Directory.Packages.props index edcfe6c..8d8ee33 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,6 +10,7 @@ + @@ -20,6 +21,8 @@ + + @@ -28,11 +31,13 @@ + + diff --git a/Qyl.OpenTelemetry.AutoInstrumentation.Demos.slnx b/Qyl.OpenTelemetry.AutoInstrumentation.Demos.slnx index 96f800e..d077142 100644 --- a/Qyl.OpenTelemetry.AutoInstrumentation.Demos.slnx +++ b/Qyl.OpenTelemetry.AutoInstrumentation.Demos.slnx @@ -1,11 +1,20 @@ + + + + + + + + + @@ -13,6 +22,7 @@ + diff --git a/README.md b/README.md index 0dde8fd..e2e2ff5 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Add the package that owns the integration you need. The supported zero-configura consumer path is a `PackageReference`; build and analyzer assets flow through NuGet. ```bash -dotnet add package Qyl.OpenTelemetry.AutoInstrumentation.Hosting +dotnet add package Qyl.Sdk ``` ## How it works @@ -47,23 +47,28 @@ ownership. The shortest path is `Qyl.Sdk`, which owns all of the wiring below as one call: ```csharp +using Qyl; + builder.AddQyl(); ``` -That activates the qyl listeners, registers the qyl/ASP.NET Core/HttpClient/GenAI/MCP -sources and the full qyl meter inventory (ASP.NET Core, HttpClient, DNS, database, -messaging, runtime, plus the GenAI meters such as `gen_ai.client.token.usage`), -copies `session.id` from the nearest tagged in-process ancestor to descendant spans -(remote parents and unrelated trace branches are not propagated), and exports traces, metrics, and logs over -OTLP — to `OTEL_EXPORTER_OTLP_ENDPOINT` when set, otherwise to a qyl collector -discovered on localhost (4318/4317). GenAI telemetry still requires the one-line agent -opt-in described below. +That activates the qyl listeners; registers the single qyl source for qyl-owned +ASP.NET Core, HttpClient, gRPC, and database spans plus the enabled first-party +library sources; and registers the native and qyl-owned meter inventory (ASP.NET +Core, HttpClient, DNS, database, messaging, and runtime). It copies `session.id` +from the nearest tagged in-process +ancestor to descendant spans (remote parents and unrelated trace branches are not +propagated); and exports traces, metrics, and logs over OTLP — to +`OTEL_EXPORTER_OTLP_ENDPOINT` when set, otherwise to a qyl collector discovered on +localhost (4318/4317). It also registers the exact library telemetry paths +listed below; wrapper-based libraries still require their explicit one-line opt-in. The rest of this section is the manual wiring for apps that want to own it. -These packages emit `Activity` and `Meter` telemetry; they ship no exporter. The -consuming application wires the OpenTelemetry SDK and chooses where the telemetry -goes. A working setup against the qyl collector adds +The lower-level instrumentation packages emit `Activity` and `Meter` telemetry; they +ship no exporter. An application that does not use `Qyl.Sdk` wires the OpenTelemetry +SDK and chooses where the telemetry goes. A working setup against the qyl collector +adds `OpenTelemetry.Extensions.Hosting` and `OpenTelemetry.Exporter.OpenTelemetryProtocol` alongside `Qyl.OpenTelemetry.AutoInstrumentation.Hosting`, then registers the sources: @@ -72,8 +77,6 @@ builder.Services.AddOpenTelemetry() .ConfigureResource(r => r.AddService("my-service")) .WithTracing(t => t .AddSource("Qyl.OpenTelemetry.AutoInstrumentation") // qyl listeners - .AddSource("Microsoft.AspNetCore") // BCL incoming HTTP - .AddSource("System.Net.Http") // BCL outgoing HttpClient .AddOtlpExporter()); builder.Logging.AddOpenTelemetry(o => o.AddOtlpExporter()); ``` @@ -82,31 +85,58 @@ Configure the exporter through the standard environment variables: `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318`, `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`, and `OTEL_SERVICE_NAME`. -GenAI applications using `Microsoft.Agents.AI` additionally opt in with -`agent.AsBuilder().UseOpenTelemetry().Build()` and register -`AddSource("Experimental.Microsoft.Agents.AI")` and -`AddSource("Experimental.Microsoft.Extensions.AI")`. The qyl collector ingests the -resulting `gen_ai.*` spans, including sessions and token usage; billed costs come -from provider APIs and model-catalog estimates on the collector side, not from -span attributes. The Anthropic .NET SDK itself emits no telemetry. +Do not also subscribe to `Microsoft.AspNetCore` or `System.Net.Http` traces when the +qyl listeners own those operations; doing so exports the same request twice. Azure +SDK tracing is the first-party exception: `Qyl.Sdk` enables +`Azure.Experimental.EnableActivitySource`, subscribes `Azure.*`, and normalizes the +exported Azure spans. A manually wired application must make those two choices +explicitly if it wants Azure SDK spans. + +### AI, MCP, and CoreWCF paths in 8.0 + +These are version-pinned library-hook claims, not provider- or protocol-wide claims. +The exact `ModelContextProtocol` 1.4.1 client/server path has strict NativeAOT +evidence; the other paths in this table have managed evidence only: + +| Library path | Application opt-in | Signals registered by `Qyl.Sdk` | Integration ID | +| --- | --- | --- | --- | +| `Microsoft.Extensions.AI` 10.8.0 | `chatClient.AsBuilder().UseOpenTelemetry().Build()` | traces and metrics from `Experimental.Microsoft.Extensions.AI` | `MICROSOFTEXTENSIONSAI` | +| `Microsoft.Agents.AI` 1.13.0 | `agent.AsBuilder().UseOpenTelemetry().Build()` | traces and metrics from `Experimental.Microsoft.Agents.AI` | `MICROSOFTAGENTSAI` | +| `Microsoft.Agents.AI.Workflows` 1.13.0 | `WorkflowBuilder.WithOpenTelemetry()` | traces from `Microsoft.Agents.AI.Workflows` | `MICROSOFTAGENTSAIWORKFLOWS` | +| `ModelContextProtocol` 1.4.1 | none; the official client/server SDK emits automatically | managed and strict NativeAOT traces from `Experimental.ModelContextProtocol` | `MCP` | +| `CoreWCF.Http` 1.9.1 | none; CoreWCF emits server activities | managed traces from `CoreWCF.Primitives` | `WCFCORE` | + +MCP metrics are intentionally not registered: the official instruments attach +dynamic tool and resource names as dimensions, which conflicts with qyl's bounded-cardinality +policy. The 8.0 contract does not claim direct OpenAI SDK instrumentation, raw Anthropic SDK +instrumentation, `Azure.AI.Inference`, Amazon Bedrock, or A2A. + +Every path is enabled by default when its signal is enabled. Set the applicable +signal-specific variable to `false` to disable it: + +- `MICROSOFTEXTENSIONSAI`: + `OTEL_DOTNET_AUTO_TRACES_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED` and + `OTEL_DOTNET_AUTO_METRICS_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED`. +- `MICROSOFTAGENTSAI`: + `OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED` and + `OTEL_DOTNET_AUTO_METRICS_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED`. +- `MICROSOFTAGENTSAIWORKFLOWS`: + `OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAIWORKFLOWS_INSTRUMENTATION_ENABLED`. +- `MCP`: `OTEL_DOTNET_AUTO_TRACES_MCP_INSTRUMENTATION_ENABLED`. +- `WCFCORE`: `OTEL_DOTNET_AUTO_TRACES_WCFCORE_INSTRUMENTATION_ENABLED`. + +The global `OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED` and per-signal +`OTEL_DOTNET_AUTO_{TRACES|METRICS|LOGS}_INSTRUMENTATION_ENABLED` switches still take +precedence. ## Coverage and evidence The generated [`coverage matrix`](docs/coverage-matrix.md) is the detailed contract -view. Its current 60 rows comprise: - -- 33 implemented signal rows: 26 with NativeAOT runtime evidence and 7 with managed - runtime evidence; -- 19 configuration rows: 12 option bindings and 7 control bindings — each backed by - emitted-telemetry assertions in both directions (the behavior appears when opted in - and never appears otherwise), not merely by option parsing; -- 8 unsupported NativeAOT rows, including four CLR-profiler/.NET Framework-only - options; each carries its verified blocking reason in the matrix. - -Those categories are intentionally separate. A configuration binding is not runtime -instrumentation, and the matrix is generated from the declared contract rather than +view. It keeps NativeAOT runtime evidence, managed runtime evidence, configuration +bindings, and unsupported rows separate. A configuration binding is not runtime +instrumentation, and the matrix is generated from the declared contracts rather than being independent empirical proof. Runtime claims are backed by executable demos or -consumers named in the underlying ownership contract. +consumers named in the underlying ownership contracts. The NativeAOT boundary applies to this compile-time/managed substrate. It does not claim parity with the CLR-profiler OpenTelemetry .NET automatic instrumentor, and it diff --git a/benchmarks/Qyl.OpenTelemetry.AutoInstrumentation.Benchmarks/Program.cs b/benchmarks/Qyl.OpenTelemetry.AutoInstrumentation.Benchmarks/Program.cs index 5d80aeb..107ea9e 100644 --- a/benchmarks/Qyl.OpenTelemetry.AutoInstrumentation.Benchmarks/Program.cs +++ b/benchmarks/Qyl.OpenTelemetry.AutoInstrumentation.Benchmarks/Program.cs @@ -1,7 +1,6 @@ using System.Data; using System.Data.Common; using System.Collections; -using System.Net; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Running; @@ -20,44 +19,12 @@ 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(); + await Task.CompletedTask; } } -[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)] @@ -84,38 +51,6 @@ public int InterceptedSqlClientCommand() } } -[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 diff --git a/demos/Qyl.RealAdoNetDemo/Program.cs b/demos/Qyl.RealAdoNetDemo/Program.cs index 62177cf..b2b05eb 100644 --- a/demos/Qyl.RealAdoNetDemo/Program.cs +++ b/demos/Qyl.RealAdoNetDemo/Program.cs @@ -108,7 +108,7 @@ public static AdoNetReport Create( Require(insert, "successful INSERT span", failures); Require(selectSuccess, "successful SELECT span", failures); Require(selectError, "error SELECT span", failures); - RequireTag(selectError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(SqliteException), failures); + RequireTag(selectError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(SqliteException).FullName!, failures); foreach (var span in adoNetSpans) { diff --git a/demos/Qyl.RealAspNetCoreMetricsDemo/Program.cs b/demos/Qyl.RealAspNetCoreMetricsDemo/Program.cs index 24b1476..141d9d7 100644 --- a/demos/Qyl.RealAspNetCoreMetricsDemo/Program.cs +++ b/demos/Qyl.RealAspNetCoreMetricsDemo/Program.cs @@ -17,13 +17,12 @@ using OpenTelemetry; using OpenTelemetry.Metrics; using Qyl.OpenTelemetry.AutoInstrumentation; -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; var exportedMetrics = new List(); using var meterProvider = Sdk .CreateMeterProviderBuilder() - .AddMeter("consumer.placeholder") + .AddMeter(DemoMetricNames.AspNetCore) .AddInMemoryExporter(exportedMetrics) .Build(); @@ -53,7 +52,7 @@ await renderer.Dispatcher.InvokeAsync(async () => var report = AspNetCoreMetricsReport.Create( RuntimeFeature.IsDynamicCodeSupported ? "dynamic-code-supported" : "nativeaot", - QylMetricMeters.GetEnabledMeterNames(), + DemoMetricNames.AspNetCore, exportedMetrics.Select(CapturedMetric.From).ToArray()); var json = JsonSerializer.Serialize(report, RealAspNetCoreMetricsJsonContext.Default.AspNetCoreMetricsReport); @@ -140,18 +139,18 @@ internal sealed record AspNetCoreMetricsReport( public static AspNetCoreMetricsReport Create(string runtimeMode, string[] enabledMeterNames, CapturedMetric[] metrics) { var failures = new List(); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreHostingMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreRoutingMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreDiagnosticsMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreRateLimitingMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreHeaderParsingMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreServerKestrelMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreHttpConnectionsMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreAuthorizationMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreAuthenticationMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreComponentsMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreComponentsLifecycleMeterName, failures); - RequireEnabledMeter(enabledMeterNames, QylMetricMeters.AspNetCoreComponentsServerCircuitsMeterName, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreHosting, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreRouting, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreDiagnostics, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreRateLimiting, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreHeaderParsing, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreServerKestrel, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreHttpConnections, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreAuthorization, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreAuthentication, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreComponents, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreComponentsLifecycle, failures); + RequireEnabledMeter(enabledMeterNames, DemoMetricNames.AspNetCoreComponentsServerCircuits, failures); var capturedAspNetCoreMetrics = metrics .Where(static metric => metric.MeterName.StartsWith("Microsoft.AspNetCore", StringComparison.Ordinal)) @@ -160,7 +159,7 @@ public static AspNetCoreMetricsReport Create(string runtimeMode, string[] enable .Where(static metric => metric.MeterName.StartsWith("Microsoft.AspNetCore.Components", StringComparison.Ordinal)) .ToArray(); var hostingMetrics = capturedAspNetCoreMetrics - .Where(static metric => StringComparer.Ordinal.Equals(metric.MeterName, QylMetricMeters.AspNetCoreHostingMeterName)) + .Where(static metric => StringComparer.Ordinal.Equals(metric.MeterName, DemoMetricNames.AspNetCoreHosting)) .ToArray(); if (componentMetrics.Length is 0) @@ -175,10 +174,10 @@ public static AspNetCoreMetricsReport Create(string runtimeMode, string[] enable failures.Add("observed metrics: " + string.Join("|", metrics.Select(static metric => metric.MeterName + ":" + metric.Name).OrderBy(static name => name, StringComparer.Ordinal))); } - RequireMetric(componentMetrics, QylMetricMeters.AspNetCoreComponentsLifecycleMeterName, "aspnetcore.components.render_diff.duration", failures); - RequireMetric(componentMetrics, QylMetricMeters.AspNetCoreComponentsLifecycleMeterName, "aspnetcore.components.render_diff.size", failures); - RequireMetric(componentMetrics, QylMetricMeters.AspNetCoreComponentsLifecycleMeterName, "aspnetcore.components.update_parameters.duration", failures); - RequireMetric(hostingMetrics, QylMetricMeters.AspNetCoreHostingMeterName, "http.server.request.duration", failures); + RequireMetric(componentMetrics, DemoMetricNames.AspNetCoreComponentsLifecycle, "aspnetcore.components.render_diff.duration", failures); + RequireMetric(componentMetrics, DemoMetricNames.AspNetCoreComponentsLifecycle, "aspnetcore.components.render_diff.size", failures); + RequireMetric(componentMetrics, DemoMetricNames.AspNetCoreComponentsLifecycle, "aspnetcore.components.update_parameters.duration", failures); + RequireMetric(hostingMetrics, DemoMetricNames.AspNetCoreHosting, "http.server.request.duration", failures); foreach (var metric in capturedAspNetCoreMetrics.Where(static metric => metric.PointCount <= 0)) failures.Add($"expected at least one metric point for {metric.MeterName}:{metric.Name}, got {metric.PointCount.ToString(CultureInfo.InvariantCulture)}"); @@ -199,6 +198,38 @@ private static void RequireMetric(IEnumerable metrics, string me } } +internal static class DemoMetricNames +{ + internal const string AspNetCoreHosting = "Microsoft.AspNetCore.Hosting"; + internal const string AspNetCoreRouting = "Microsoft.AspNetCore.Routing"; + internal const string AspNetCoreDiagnostics = "Microsoft.AspNetCore.Diagnostics"; + internal const string AspNetCoreRateLimiting = "Microsoft.AspNetCore.RateLimiting"; + internal const string AspNetCoreHeaderParsing = "Microsoft.AspNetCore.HeaderParsing"; + internal const string AspNetCoreServerKestrel = "Microsoft.AspNetCore.Server.Kestrel"; + internal const string AspNetCoreHttpConnections = "Microsoft.AspNetCore.Http.Connections"; + internal const string AspNetCoreAuthorization = "Microsoft.AspNetCore.Authorization"; + internal const string AspNetCoreAuthentication = "Microsoft.AspNetCore.Authentication"; + internal const string AspNetCoreComponents = "Microsoft.AspNetCore.Components"; + internal const string AspNetCoreComponentsLifecycle = "Microsoft.AspNetCore.Components.Lifecycle"; + internal const string AspNetCoreComponentsServerCircuits = "Microsoft.AspNetCore.Components.Server.Circuits"; + + internal static readonly string[] AspNetCore = + [ + AspNetCoreHosting, + AspNetCoreRouting, + AspNetCoreDiagnostics, + AspNetCoreRateLimiting, + AspNetCoreHeaderParsing, + AspNetCoreServerKestrel, + AspNetCoreHttpConnections, + AspNetCoreAuthorization, + AspNetCoreAuthentication, + AspNetCoreComponents, + AspNetCoreComponentsLifecycle, + AspNetCoreComponentsServerCircuits, + ]; +} + [JsonSerializable(typeof(AspNetCoreMetricsReport))] [JsonSourceGenerationOptions(WriteIndented = true)] internal sealed partial class RealAspNetCoreMetricsJsonContext : JsonSerializerContext; diff --git a/demos/Qyl.RealAzureDemo/Program.cs b/demos/Qyl.RealAzureDemo/Program.cs index 6717bc8..a3a08bd 100644 --- a/demos/Qyl.RealAzureDemo/Program.cs +++ b/demos/Qyl.RealAzureDemo/Program.cs @@ -6,16 +6,31 @@ using System.Text.Json.Serialization; using Azure; using Azure.Storage.Blobs; -using Qyl.OpenTelemetry.AutoInstrumentation; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry.Trace; +using Qyl; -var capturedActivities = new List(); -using var activityListener = new ActivityListener +var exportedActivities = new List(); +var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings { - ShouldListenTo = static source => source.Name == "Qyl.OpenTelemetry.AutoInstrumentation", - Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, - ActivityStopped = activity => capturedActivities.Add(CapturedActivity.From(activity)), -}; -ActivitySource.AddActivityListener(activityListener); + ApplicationName = "Qyl.RealAzureDemo", + DisableDefaults = true, +}); +builder.AddQyl(options => +{ + options.ServiceName = "qyl-real-azure-demo"; + options.CollectorEndpoint = new Uri("http://127.0.0.1:1"); + options.EnableCollectorDiscovery = false; + options.EnableLogExport = false; + options.EnableMetricsExport = false; + options.EnableSessionPropagation = false; +}); +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddInMemoryExporter(exportedActivities)); + +using var host = builder.Build(); +await host.StartAsync(); var endpoint = new Uri($"http://{IPAddress.Loopback}:9/devstoreaccount1"); var options = new BlobClientOptions @@ -46,9 +61,12 @@ Console.WriteLine("expected-azure-error=" + exception.GetType().Name); } +host.Services.GetRequiredService().ForceFlush(5_000); +await host.StopAsync(); + var report = AzureReport.Create( RuntimeFeature.IsDynamicCodeSupported ? "dynamic-code-supported" : "nativeaot", - capturedActivities.ToArray()); + exportedActivities.Select(CapturedActivity.From).ToArray()); var json = JsonSerializer.Serialize(report, RealAzureJsonContext.Default.AzureReport); Console.WriteLine(json); @@ -87,15 +105,22 @@ public static AzureReport Create(string runtimeMode, CapturedActivity[] activiti StringComparer.Ordinal.Equals(domain, "azure.sdk")) .ToArray(); - if (azureSpans.Length != 2) - failures.Add($"expected 2 Azure spans, got {azureSpans.Length.ToString(CultureInfo.InvariantCulture)}"); + if (azureSpans.Length != 4) + failures.Add($"expected 4 Azure spans, got {azureSpans.Length.ToString(CultureInfo.InvariantCulture)}"); + + var transportSpans = azureSpans + .Where(static span => StringComparer.Ordinal.Equals(span.Kind, "Client")) + .ToArray(); + var operationSpans = azureSpans + .Where(static span => StringComparer.Ordinal.Equals(span.Kind, "Internal")) + .ToArray(); + if (transportSpans.Length != 2) + failures.Add($"expected 2 Azure transport spans, got {transportSpans.Length.ToString(CultureInfo.InvariantCulture)}"); + if (operationSpans.Length != 2) + failures.Add($"expected 2 Azure operation spans, got {operationSpans.Length.ToString(CultureInfo.InvariantCulture)}"); foreach (var span in azureSpans) { - if (!StringComparer.Ordinal.Equals(span.Name, "Azure SDK")) - failures.Add($"unexpected Azure span name: {span.Name}"); - if (!StringComparer.Ordinal.Equals(span.Kind, "Client")) - failures.Add($"expected Azure span kind Client, got {span.Kind}"); if (!StringComparer.Ordinal.Equals(span.Status, "Error")) failures.Add($"expected Azure span status Error, got {span.Status}"); @@ -105,6 +130,20 @@ public static AzureReport Create(string runtimeMode, CapturedActivity[] activiti RequireMissingTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Url.UrlAttributes.Path, failures); } + foreach (var span in transportSpans) + { + if (!StringComparer.Ordinal.Equals(span.Name, "GET")) + failures.Add($"unexpected Azure transport span name: {span.Name}"); + RequireTag(span, "http.request.method", "GET", failures); + RequireTag(span, "server.address", "127.0.0.1", failures); + } + + foreach (var span in operationSpans) + { + if (!StringComparer.Ordinal.Equals(span.Name, "BlobServiceClient.GetProperties")) + failures.Add($"unexpected Azure operation span name: {span.Name}"); + } + return new AzureReport(runtimeMode, failures.Count is 0, failures.ToArray(), azureSpans); } diff --git a/demos/Qyl.RealAzureDemo/Qyl.RealAzureDemo.csproj b/demos/Qyl.RealAzureDemo/Qyl.RealAzureDemo.csproj index 8311ecd..0375de5 100644 --- a/demos/Qyl.RealAzureDemo/Qyl.RealAzureDemo.csproj +++ b/demos/Qyl.RealAzureDemo/Qyl.RealAzureDemo.csproj @@ -5,24 +5,16 @@ net10.0 Qyl.RealAzureDemo Qyl.RealAzureDemo - Real zero-code Azure SDK source-interceptor proof for qyl. + Real zero-code Azure SDK first-party ActivitySource proof for qyl. + - - - + - - diff --git a/demos/Qyl.RealCoreWcfDemo/Program.cs b/demos/Qyl.RealCoreWcfDemo/Program.cs new file mode 100644 index 0000000..cc99883 --- /dev/null +++ b/demos/Qyl.RealCoreWcfDemo/Program.cs @@ -0,0 +1,226 @@ +using System.Diagnostics; +using System.Globalization; +using System.ServiceModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using CoreWCF.Configuration; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Trace; +using Qyl; +using ClientBasicHttpBinding = System.ServiceModel.BasicHttpBinding; +using ClientEndpointAddress = System.ServiceModel.EndpointAddress; +using ServerBasicHttpBinding = CoreWCF.BasicHttpBinding; + +var exportedActivities = new List(); +var builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseUrls("http://127.0.0.1:0"); +builder.Logging.ClearProviders(); +builder.Services.AddHealthChecks(); +builder.Services.AddServiceModelServices(); +builder.AddQyl(options => +{ + options.EnableCollectorDiscovery = false; + options.CollectorEndpoint = new Uri("http://127.0.0.1:1"); + options.EnableLogExport = false; + options.EnableMetricsExport = false; +}); +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddInMemoryExporter(exportedActivities)); + +var app = builder.Build(); +app.MapHealthChecks("/healthz"); +app.UseServiceModel(services => +{ + services.AddService(); + services.AddServiceEndpoint(new ServerBasicHttpBinding(), "/probe.svc"); +}); + +await app.StartAsync(); +using var coreWcfSubscriptionProbe = new ActivitySource("CoreWCF.Primitives"); +var registrationObserved = coreWcfSubscriptionProbe.HasListeners(); + +try +{ + var endpoint = new ClientEndpointAddress(app.Urls.Single() + "/probe.svc"); + await using var factory = new ChannelFactory(new ClientBasicHttpBinding(), endpoint); + var channel = factory.CreateChannel(); + + var response = channel.Echo("secret-payload"); + if (!StringComparer.Ordinal.Equals(response, "echo-ok")) + throw new InvalidOperationException($"unexpected CoreWCF response: {response}"); + + var expectedFaultObserved = false; + try + { + channel.Fail(); + } + catch (FaultException) + { + expectedFaultObserved = true; + } + + if (!expectedFaultObserved) + throw new InvalidOperationException("CoreWCF failure operation did not return a SOAP fault"); + + ((IClientChannel)channel).Close(); +} +finally +{ + await app.StopAsync(); +} + +var registrationEnabled = !string.Equals( + Environment.GetEnvironmentVariable("OTEL_DOTNET_AUTO_TRACES_WCFCORE_INSTRUMENTATION_ENABLED"), + "false", + StringComparison.OrdinalIgnoreCase); +var report = CoreWcfReport.Create(exportedActivities, registrationEnabled, registrationObserved); +Console.WriteLine(JsonSerializer.Serialize(report, CoreWcfJsonContext.Default.CoreWcfReport)); +return report.Pass ? 0 : 1; + +[CoreWCF.ServiceContract] +[System.ServiceModel.ServiceContract] +public interface IProbeService +{ + [CoreWCF.OperationContract] + [System.ServiceModel.OperationContract] + string Echo(string value); + + [CoreWCF.OperationContract] + [System.ServiceModel.OperationContract] + void Fail(); +} + +public sealed class ProbeService : IProbeService +{ + public string Echo(string value) => value.Length > 0 ? "echo-ok" : "echo-empty"; + + public void Fail() => throw new InvalidOperationException("expected-corewcf-failure"); +} + +internal sealed record CapturedActivity( + string Source, + string Name, + string Kind, + string Status, + IReadOnlyDictionary Tags) +{ + internal static CapturedActivity From(Activity activity) + => new( + activity.Source.Name, + 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 CoreWcfReport( + bool Pass, + string[] Failures, + bool QylRegistrationEnabled, + bool QylRegistrationObserved, + CapturedActivity[] Activities) +{ + internal static CoreWcfReport Create( + IEnumerable activities, + bool registrationEnabled, + bool registrationObserved) + { + var failures = new List(); + var coreWcfActivities = activities + .Where(static activity => StringComparer.Ordinal.Equals(activity.Source.Name, "CoreWCF.Primitives")) + .Select(CapturedActivity.From) + .ToArray(); + + var expectedCount = registrationEnabled ? 2 : 0; + if (registrationObserved != registrationEnabled) + failures.Add($"CoreWCF source registration mismatch: expected {registrationEnabled}, observed {registrationObserved}"); + if (coreWcfActivities.Length != expectedCount) + failures.Add($"expected {expectedCount} CoreWCF server spans, got {coreWcfActivities.Length}"); + + if (!registrationEnabled) + return new CoreWcfReport( + failures.Count is 0, + failures.ToArray(), + false, + registrationObserved, + coreWcfActivities); + + RequireOperation(coreWcfActivities, nameof(IProbeService.Echo), failures); + RequireOperation(coreWcfActivities, nameof(IProbeService.Fail), failures); + + foreach (var activity in coreWcfActivities) + { + if (!StringComparer.Ordinal.Equals(activity.Kind, nameof(ActivityKind.Server))) + failures.Add($"expected CoreWCF Server activity, got {activity.Kind}"); + RequireTag(activity, "rpc.system", "dotnet_wcf", failures); + RequirePresentTag(activity, "server.address", failures); + RequirePresentTag(activity, "server.port", failures); + RequireTag(activity, "wcf.channel.path", "/probe.svc", failures); + + if (activity.Tags.TryGetValue("rpc.method", out var rpcMethod)) + { + var expectedStatus = rpcMethod.EndsWith("/Fail", StringComparison.Ordinal) ? "Error" : "Unset"; + if (!StringComparer.Ordinal.Equals(activity.Status, expectedStatus)) + failures.Add($"expected {rpcMethod} status {expectedStatus}, got {activity.Status}"); + } + + if (activity.Name.Contains("secret-payload", StringComparison.Ordinal) || + activity.Tags.Values.Any(static value => value.Contains("secret-payload", StringComparison.Ordinal))) + { + failures.Add("CoreWCF telemetry captured the request payload"); + } + } + + return new CoreWcfReport( + failures.Count is 0, + failures.ToArray(), + true, + registrationObserved, + coreWcfActivities); + } + + private static void RequireOperation( + IEnumerable activities, + string method, + ICollection failures) + { + if (!activities.Any(activity => + activity.Tags.TryGetValue("rpc.method", out var actual) && + actual.EndsWith("/" + method, StringComparison.Ordinal))) + { + failures.Add($"missing CoreWCF rpc.method={method} span"); + } + } + + private static void RequireTag( + CapturedActivity activity, + string key, + string expected, + ICollection failures) + { + if (!activity.Tags.TryGetValue(key, out var actual)) + { + failures.Add($"missing {key}"); + return; + } + + if (!StringComparer.Ordinal.Equals(actual, expected)) + failures.Add($"expected {key}={expected}, got {actual}"); + } + + private static void RequirePresentTag( + CapturedActivity activity, + string key, + ICollection failures) + { + if (!activity.Tags.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) + failures.Add($"missing {key}"); + } +} + +[JsonSerializable(typeof(CoreWcfReport))] +[JsonSourceGenerationOptions(WriteIndented = true)] +internal sealed partial class CoreWcfJsonContext : JsonSerializerContext; diff --git a/demos/Qyl.RealCoreWcfDemo/Qyl.RealCoreWcfDemo.csproj b/demos/Qyl.RealCoreWcfDemo/Qyl.RealCoreWcfDemo.csproj new file mode 100644 index 0000000..d6eb6e0 --- /dev/null +++ b/demos/Qyl.RealCoreWcfDemo/Qyl.RealCoreWcfDemo.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + Qyl.RealCoreWcfDemo + Qyl.RealCoreWcfDemo + Managed CoreWCF native ActivitySource registration proof for qyl. + + + + + + + + + + + + + diff --git a/demos/Qyl.RealElasticTransportDemo/Program.cs b/demos/Qyl.RealElasticTransportDemo/Program.cs index fd722a9..260e966 100644 --- a/demos/Qyl.RealElasticTransportDemo/Program.cs +++ b/demos/Qyl.RealElasticTransportDemo/Program.cs @@ -104,7 +104,7 @@ public static ElasticTransportReport Create(string runtimeMode, CapturedActivity RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemName, Qyl.OpenTelemetry.SemanticConventions.Incubating.Attributes.Db.DbAttributes.SystemNameValues.Elasticsearch, failures); RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.OperationName, "request", failures); - RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(InvalidOperationException), failures); + RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(InvalidOperationException).FullName!, failures); RequireMissingTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.QueryText, failures); } diff --git a/demos/Qyl.RealElasticsearchDemo/Program.cs b/demos/Qyl.RealElasticsearchDemo/Program.cs index 100891b..7077e53 100644 --- a/demos/Qyl.RealElasticsearchDemo/Program.cs +++ b/demos/Qyl.RealElasticsearchDemo/Program.cs @@ -96,7 +96,7 @@ public static ElasticsearchReport Create(string runtimeMode, CapturedActivity[] RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemName, Qyl.OpenTelemetry.SemanticConventions.Incubating.Attributes.Db.DbAttributes.SystemNameValues.Elasticsearch, failures); RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.OperationName, "request", failures); - RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(TransportException), failures); + RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(TransportException).FullName!, failures); RequireMissingTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.QueryText, failures); } diff --git a/demos/Qyl.RealGenAiDemo/Program.cs b/demos/Qyl.RealGenAiDemo/Program.cs new file mode 100644 index 0000000..b3648c7 --- /dev/null +++ b/demos/Qyl.RealGenAiDemo/Program.cs @@ -0,0 +1,728 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Agents.AI; +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Qyl; +using static ProbeContract; + +var activities = new ConcurrentQueue(); +var measurements = new ConcurrentQueue(); +var publishedMeterNames = new ConcurrentDictionary(StringComparer.Ordinal); +var exportedActivities = new List(); +var exportedMetrics = new List(); + +var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings +{ + ApplicationName = "Qyl.RealGenAiDemo", + DisableDefaults = true, +}); +builder.Services.AddLogging(); +builder.Logging.ClearProviders(); +builder.AddQyl(options => +{ + options.ServiceName = "qyl-real-genai-demo"; + options.CollectorEndpoint = new Uri("http://127.0.0.1:1"); + options.EnableCollectorDiscovery = false; + options.EnableLogExport = false; +}); +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddInMemoryExporter(exportedActivities)) + .WithMetrics(metrics => metrics.AddInMemoryExporter(exportedMetrics)); + +using var host = builder.Build(); +await host.StartAsync(); + +using var activityListener = new ActivityListener +{ + ShouldListenTo = static source => source.Name is ExtensionsSourceName or AgentsSourceName or WorkflowsSourceName, + Sample = static (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded, + ActivityStopped = activity => activities.Enqueue(CapturedActivity.From(activity)), +}; +ActivitySource.AddActivityListener(activityListener); + +using var meterListener = new MeterListener +{ + InstrumentPublished = (instrument, listener) => + { + var meterName = instrument.Meter.Name; + if (meterName is ExtensionsSourceName or AgentsSourceName || + meterName.Contains("Agents.AI.Workflows", StringComparison.Ordinal)) + { + publishedMeterNames.TryAdd(meterName, 0); + listener.EnableMeasurementEvents(instrument); + } + }, +}; +meterListener.SetMeasurementEventCallback(CaptureMeasurement); +meterListener.SetMeasurementEventCallback(CaptureMeasurement); +meterListener.SetMeasurementEventCallback(CaptureMeasurement); +meterListener.Start(); + +var bareResponseVerified = await RunBareMeAiAsync(); +var agentResponseVerified = await RunAgentAsync(); +var workflowOutputVerified = await RunWorkflowAsync(); + +host.Services.GetRequiredService().ForceFlush(5_000); +host.Services.GetRequiredService().ForceFlush(5_000); +await host.StopAsync(); + +var report = GenAiReport.Create( + RuntimeFeature.IsDynamicCodeSupported ? "dynamic-code-supported" : "nativeaot", + bareResponseVerified, + agentResponseVerified, + workflowOutputVerified, + activities.ToArray(), + measurements.ToArray(), + publishedMeterNames.Keys.Order(StringComparer.Ordinal).ToArray(), + exportedActivities + .Select(static activity => activity.Source.Name) + .Distinct(StringComparer.Ordinal) + .Order(StringComparer.Ordinal) + .ToArray(), + exportedMetrics + .Select(static metric => metric.MeterName) + .Distinct(StringComparer.Ordinal) + .Order(StringComparer.Ordinal) + .ToArray()); + +Console.WriteLine(JsonSerializer.Serialize(report, RealGenAiJsonContext.Default.GenAiReport)); +return report.Pass ? 0 : 1; + +void CaptureMeasurement( + Instrument instrument, + T measurement, + ReadOnlySpan> tags, + object? state) + where T : struct +{ + _ = state; + measurements.Enqueue(CapturedMeasurement.From(instrument, measurement, tags)); +} + +static async Task RunBareMeAiAsync() +{ + using IChatClient client = new FixedChatClient() + .AsBuilder() + .UseOpenTelemetry() + .Build(); + + var response = await client.GetResponseAsync( + [new ChatMessage(ChatRole.User, UserSensitiveContent)]); + + var streamedText = new List(); + await foreach (var update in client.GetStreamingResponseAsync( + [new ChatMessage(ChatRole.User, UserSensitiveContent)])) + { + if (!string.IsNullOrEmpty(update.Text)) + streamedText.Add(update.Text); + } + + return StringComparer.Ordinal.Equals(response.Text, AssistantSensitiveContent) && + StringComparer.Ordinal.Equals(string.Concat(streamedText), AssistantSensitiveContent); +} + +static async Task RunAgentAsync() +{ + using var innerClient = new FixedChatClient(); + var innerAgent = new ChatClientAgent( + innerClient, + new ChatClientAgentOptions + { + Id = "fixed-agent-id", + Name = "fixed-agent", + Description = "fixed agent telemetry probe", + ChatOptions = new ChatOptions + { + Instructions = "Return the fixed response.", + }, + }); + + using var agent = (OpenTelemetryAgent)innerAgent + .AsBuilder() + .UseOpenTelemetry() + .Build(); + + var response = await agent.RunAsync(UserSensitiveContent); + return StringComparer.Ordinal.Equals(response.Text, AssistantSensitiveContent); +} + +static async Task RunWorkflowAsync() +{ + Func uppercase = static value => value.ToUpperInvariant(); + Func suffix = static value => value + "|complete"; + var uppercaseExecutor = uppercase.BindAsExecutor("uppercase"); + var suffixExecutor = suffix.BindAsExecutor("suffix"); + + var workflow = new WorkflowBuilder(uppercaseExecutor) + .AddEdge(uppercaseExecutor, suffixExecutor) + .WithOutputFrom(suffixExecutor) + .WithOpenTelemetry() + .Build(); + + await using var run = await InProcessExecution.RunAsync(workflow, WorkflowSensitiveContent); + return run.NewEvents + .OfType() + .Any(static output => StringComparer.Ordinal.Equals( + Convert.ToString(output.Data, CultureInfo.InvariantCulture), + "PRIVATE-WORKFLOW-CONTENT|complete")); +} + +internal sealed class FixedChatClient : IChatClient +{ + private static readonly ChatClientMetadata Metadata = new( + FixedProvider, + new Uri("https://example.invalid"), + FixedModel); + + public Task GetResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(messages); + _ = options; + cancellationToken.ThrowIfCancellationRequested(); + return Task.FromResult(CreateResponse()); + } + + public async IAsyncEnumerable GetStreamingResponseAsync( + IEnumerable messages, + ChatOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(messages); + _ = options; + foreach (var update in CreateResponse().ToChatResponseUpdates()) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + yield return update; + } + } + + public object? GetService(Type serviceType, object? serviceKey = null) + { + ArgumentNullException.ThrowIfNull(serviceType); + if (serviceKey is not null) + return null; + + if (serviceType == typeof(ChatClientMetadata)) + return Metadata; + + return serviceType == typeof(IChatClient) || serviceType == typeof(FixedChatClient) + ? this + : null; + } + + public void Dispose() + { + } + + private static ChatResponse CreateResponse() + => new(new ChatMessage(ChatRole.Assistant, AssistantSensitiveContent) + { + MessageId = "fixed-message-id", + }) + { + ResponseId = "fixed-response-id", + ModelId = FixedModel, + CreatedAt = new DateTimeOffset(2026, 7, 19, 0, 0, 0, TimeSpan.Zero), + FinishReason = ChatFinishReason.Stop, + Usage = new UsageDetails + { + InputTokenCount = 7, + OutputTokenCount = 3, + TotalTokenCount = 10, + }, + }; +} + +internal sealed record CapturedActivity( + string SourceName, + string Name, + string Kind, + string Status, + string SpanId, + string ParentSpanId, + Dictionary Tags, + string[] EventTagValues) +{ + public static CapturedActivity From(Activity activity) + { + var tags = activity.TagObjects.ToDictionary( + static tag => tag.Key, + static tag => TelemetryValue.Format(tag.Value), + StringComparer.Ordinal); + var eventTagValues = activity.Events + .SelectMany(static activityEvent => activityEvent.Tags) + .Select(static tag => TelemetryValue.Format(tag.Value)) + .ToArray(); + + return new CapturedActivity( + activity.Source.Name, + activity.DisplayName, + activity.Kind.ToString(), + activity.Status.ToString(), + activity.SpanId.ToHexString(), + activity.ParentSpanId.ToHexString(), + tags, + eventTagValues); + } +} + +internal sealed record CapturedMeasurement( + string MeterName, + string InstrumentName, + string Value, + Dictionary Tags) +{ + public static CapturedMeasurement From( + Instrument instrument, + T measurement, + ReadOnlySpan> tags) + where T : struct + { + var capturedTags = new Dictionary(tags.Length, StringComparer.Ordinal); + foreach (var tag in tags) + capturedTags.Add(tag.Key, TelemetryValue.Format(tag.Value)); + + return new CapturedMeasurement( + instrument.Meter.Name, + instrument.Name, + Convert.ToString(measurement, CultureInfo.InvariantCulture) ?? string.Empty, + capturedTags); + } +} + +internal sealed record GenAiReport( + string RuntimeMode, + bool Pass, + string[] Failures, + bool BareResponseVerified, + bool AgentResponseVerified, + bool WorkflowOutputVerified, + CapturedActivity[] Activities, + CapturedMeasurement[] Measurements, + string[] PublishedMeterNames, + string[] QylExportedActivitySources, + string[] QylExportedMeterNames) +{ + private const string OperationDuration = "gen_ai.client.operation.duration"; + private const string TokenUsage = "gen_ai.client.token.usage"; + private const string TimeToFirstChunk = "gen_ai.client.operation.time_to_first_chunk"; + private const string TimePerOutputChunk = "gen_ai.client.operation.time_per_output_chunk"; + + public static GenAiReport Create( + string runtimeMode, + bool bareResponseVerified, + bool agentResponseVerified, + bool workflowOutputVerified, + CapturedActivity[] activities, + CapturedMeasurement[] measurements, + string[] publishedMeterNames, + string[] qylExportedActivitySources, + string[] qylExportedMeterNames) + { + var failures = new List(); + Require(bareResponseVerified, "bare MEAI response did not traverse the fixed client", failures); + Require(agentResponseVerified, "agent response did not traverse the fixed client", failures); + Require(workflowOutputVerified, "workflow did not produce the expected deterministic output", failures); + + ValidateMeAiActivities(activities, failures); + ValidateAgentActivities(activities, failures); + ValidateWorkflowActivities(activities, failures); + ValidateSensitiveData(activities, measurements, failures); + ValidateMetrics(measurements, publishedMeterNames, failures); + ValidateQylRegistration(qylExportedActivitySources, qylExportedMeterNames, failures); + + return new GenAiReport( + runtimeMode, + failures.Count is 0, + failures.ToArray(), + bareResponseVerified, + agentResponseVerified, + workflowOutputVerified, + activities, + measurements, + publishedMeterNames, + qylExportedActivitySources, + qylExportedMeterNames); + } + + private static void ValidateQylRegistration( + string[] activitySources, + string[] meterNames, + ICollection failures) + { + var expectedSources = new List(3); + AddExpected( + expectedSources, + "OTEL_DOTNET_AUTO_TRACES_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED", + ExtensionsSourceName); + AddExpected( + expectedSources, + "OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED", + AgentsSourceName); + AddExpected( + expectedSources, + "OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAIWORKFLOWS_INSTRUMENTATION_ENABLED", + WorkflowsSourceName); + Require( + activitySources.Order(StringComparer.Ordinal).SequenceEqual(expectedSources.Order(StringComparer.Ordinal), StringComparer.Ordinal), + $"Qyl.Sdk activity registration mismatch: expected={string.Join('|', expectedSources)} actual={string.Join('|', activitySources)}", + failures); + + RequireRegistration( + meterNames, + "OTEL_DOTNET_AUTO_METRICS_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED", + ExtensionsSourceName, + failures); + RequireRegistration( + meterNames, + "OTEL_DOTNET_AUTO_METRICS_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED", + AgentsSourceName, + failures); + Require( + meterNames.All(static name => !name.Contains("Agents.AI.Workflows", StringComparison.Ordinal)), + "Qyl.Sdk exported an unexpected Workflow meter", + failures); + } + + private static void AddExpected(List names, string variable, string telemetryName) + { + if (IsEnabled(variable)) + names.Add(telemetryName); + } + + private static void RequireRegistration( + IEnumerable names, + string variable, + string telemetryName, + ICollection failures) + { + var registered = names.Contains(telemetryName, StringComparer.Ordinal); + Require( + registered == IsEnabled(variable), + $"Qyl.Sdk meter registration mismatch for {telemetryName}: registered={registered}", + failures); + } + + private static bool IsEnabled(string variable) + => !string.Equals(Environment.GetEnvironmentVariable(variable), "false", StringComparison.OrdinalIgnoreCase); + + private static void ValidateMeAiActivities( + IEnumerable activities, + ICollection failures) + { + var spans = activities + .Where(static activity => StringComparer.Ordinal.Equals(activity.SourceName, ExtensionsSourceName)) + .ToArray(); + if (spans.Length != 2) + failures.Add($"expected 2 bare MEAI spans, got {spans.Length.ToString(CultureInfo.InvariantCulture)}"); + + foreach (var span in spans) + ValidateChatSpan(span, failures); + + Require( + spans.Any(static span => span.Tags.TryGetValue("gen_ai.request.stream", out var value) && value == "True"), + "missing streaming MEAI span", + failures); + } + + private static void ValidateAgentActivities( + IEnumerable activities, + ICollection failures) + { + var spans = activities + .Where(static activity => StringComparer.Ordinal.Equals(activity.SourceName, AgentsSourceName)) + .ToArray(); + var invokeSpans = spans + .Where(static span => HasTag(span, "gen_ai.operation.name", "invoke_agent")) + .ToArray(); + var chatSpans = spans + .Where(static span => HasTag(span, "gen_ai.operation.name", "chat")) + .ToArray(); + + Require(invokeSpans.Length == 1, $"expected 1 invoke_agent span, got {invokeSpans.Length.ToString(CultureInfo.InvariantCulture)}", failures); + Require(chatSpans.Length == 1, $"expected 1 agent chat span, got {chatSpans.Length.ToString(CultureInfo.InvariantCulture)}", failures); + + foreach (var span in invokeSpans) + { + ValidateChatSpan(span, failures, "invoke_agent"); + RequireTag(span, "gen_ai.agent.id", "fixed-agent-id", failures); + RequireTag(span, "gen_ai.agent.name", "fixed-agent", failures); + RequireTag(span, "gen_ai.agent.description", "fixed agent telemetry probe", failures); + } + + foreach (var span in chatSpans) + ValidateChatSpan(span, failures); + + if (invokeSpans.Length == 1 && chatSpans.Length == 1) + { + Require( + StringComparer.Ordinal.Equals(chatSpans[0].ParentSpanId, invokeSpans[0].SpanId), + "agent chat span did not form a nested telemetry path", + failures); + } + } + + private static void ValidateWorkflowActivities( + IEnumerable activities, + ICollection failures) + { + var spans = activities + .Where(static activity => StringComparer.Ordinal.Equals(activity.SourceName, WorkflowsSourceName)) + .ToArray(); + RequireActivity(spans, "workflow.build", failures); + RequireActivity(spans, "workflow.session", failures); + RequireActivity(spans, "workflow_invoke", failures); + Require( + spans.Count(static span => span.Name.StartsWith("executor.process ", StringComparison.Ordinal)) >= 2, + "expected at least 2 workflow executor.process spans", + failures); + RequireActivity(spans, "message.send", failures); + } + + private static void ValidateSensitiveData( + IEnumerable activities, + IEnumerable measurements, + ICollection failures) + { + string[] forbiddenKeys = + [ + "gen_ai.input.messages", + "gen_ai.output.messages", + "gen_ai.system_instructions", + "executor.input", + "executor.output", + "message.content", + ]; + string[] forbiddenValues = + [ + UserSensitiveContent, + AssistantSensitiveContent, + WorkflowSensitiveContent, + WorkflowSensitiveContent.ToUpperInvariant(), + ]; + + foreach (var activity in activities) + { + foreach (var key in forbiddenKeys) + { + if (activity.Tags.ContainsKey(key)) + failures.Add($"sensitive telemetry tag was present: {activity.SourceName}:{activity.Name}:{key}"); + } + + foreach (var value in activity.Tags.Values.Append(activity.Name).Concat(activity.EventTagValues)) + { + foreach (var forbiddenValue in forbiddenValues) + { + if (value.Contains(forbiddenValue, StringComparison.Ordinal)) + failures.Add($"sensitive content leaked through {activity.SourceName}:{activity.Name}"); + } + } + } + + foreach (var measurement in measurements) + { + foreach (var value in measurement.Tags.Values) + { + foreach (var forbiddenValue in forbiddenValues) + { + if (value.Contains(forbiddenValue, StringComparison.Ordinal)) + failures.Add($"sensitive content leaked through {measurement.MeterName}:{measurement.InstrumentName}"); + } + } + } + } + + private static void ValidateMetrics( + CapturedMeasurement[] measurements, + IEnumerable publishedMeterNames, + ICollection failures) + { + var meterNames = publishedMeterNames.ToArray(); + Require(meterNames.Contains(ExtensionsSourceName, StringComparer.Ordinal), "MEAI meter was not published", failures); + Require(meterNames.Contains(AgentsSourceName, StringComparer.Ordinal), "Agent Framework meter was not published", failures); + Require( + meterNames.All(static name => !name.Contains("Agents.AI.Workflows", StringComparison.Ordinal)), + "Workflow unexpectedly published a meter instrument", + failures); + + RequireMetric(measurements, ExtensionsSourceName, OperationDuration, 2, failures); + RequireMetric(measurements, ExtensionsSourceName, TokenUsage, 4, failures); + RequireMetric(measurements, ExtensionsSourceName, TimeToFirstChunk, 1, failures); + RequireMetric(measurements, ExtensionsSourceName, TimePerOutputChunk, 1, failures); + RequireMetric(measurements, AgentsSourceName, OperationDuration, 2, failures); + RequireMetric(measurements, AgentsSourceName, TokenUsage, 4, failures); + RequireTokenValues(measurements, ExtensionsSourceName, failures); + RequireTokenValues(measurements, AgentsSourceName, failures); + Require( + measurements.Length == 14, + $"expected exactly 14 GenAI measurements, got {measurements.Length.ToString(CultureInfo.InvariantCulture)}", + failures); + + string[] allowedMetricTags = + [ + "error.type", + "gen_ai.operation.name", + "gen_ai.provider.name", + "gen_ai.request.model", + "gen_ai.response.model", + "gen_ai.token.type", + "server.address", + "server.port", + ]; + foreach (var measurement in measurements) + { + RequireTag(measurement, "gen_ai.operation.name", "chat", failures); + RequireTag(measurement, "gen_ai.request.model", FixedModel, failures); + RequireTag(measurement, "gen_ai.provider.name", FixedProvider, failures); + foreach (var key in measurement.Tags.Keys) + { + if (!allowedMetricTags.Contains(key, StringComparer.Ordinal)) + failures.Add($"unexpected metric dimension {measurement.MeterName}:{measurement.InstrumentName}:{key}"); + } + } + } + + private static void ValidateChatSpan( + CapturedActivity span, + ICollection failures, + string operation = "chat") + { + Require( + StringComparer.Ordinal.Equals(span.Kind, ActivityKind.Client.ToString()), + $"expected Client activity kind for {span.SourceName}:{span.Name}, got {span.Kind}", + failures); + RequireTag(span, "gen_ai.operation.name", operation, failures); + RequireTag(span, "gen_ai.request.model", FixedModel, failures); + RequireTag(span, "gen_ai.response.model", FixedModel, failures); + RequireTag(span, "gen_ai.provider.name", FixedProvider, failures); + RequireTag(span, "gen_ai.usage.input_tokens", "7", failures); + RequireTag(span, "gen_ai.usage.output_tokens", "3", failures); + } + + private static void RequireActivity( + IEnumerable activities, + string name, + ICollection failures) + => Require( + activities.Any(activity => StringComparer.Ordinal.Equals(activity.Name, name)), + $"missing workflow activity {name}", + failures); + + private static void RequireMetric( + IEnumerable measurements, + string meterName, + string instrumentName, + int expectedCount, + ICollection failures) + { + var count = measurements.Count(measurement => + StringComparer.Ordinal.Equals(measurement.MeterName, meterName) && + StringComparer.Ordinal.Equals(measurement.InstrumentName, instrumentName)); + Require( + count == expectedCount, + $"expected {expectedCount.ToString(CultureInfo.InvariantCulture)} measurements for {meterName}:{instrumentName}, got {count.ToString(CultureInfo.InvariantCulture)}", + failures); + } + + private static void RequireTokenValues( + IEnumerable measurements, + string meterName, + ICollection failures) + { + var tokens = measurements + .Where(measurement => + StringComparer.Ordinal.Equals(measurement.MeterName, meterName) && + StringComparer.Ordinal.Equals(measurement.InstrumentName, TokenUsage)) + .ToArray(); + Require( + tokens.Any(static measurement => measurement.Value == "7" && HasTag(measurement, "gen_ai.token.type", "input")), + $"missing bounded input token measurement for {meterName}", + failures); + Require( + tokens.Any(static measurement => measurement.Value == "3" && HasTag(measurement, "gen_ai.token.type", "output")), + $"missing bounded output token measurement for {meterName}", + failures); + } + + private static void RequireTag( + CapturedActivity activity, + string key, + string expected, + ICollection failures) + { + if (!HasTag(activity, key, expected)) + { + activity.Tags.TryGetValue(key, out var actual); + failures.Add($"expected {activity.SourceName}:{activity.Name}:{key}={expected}, got {actual ?? ""}"); + } + } + + private static void RequireTag( + CapturedMeasurement measurement, + string key, + string expected, + ICollection failures) + { + if (!HasTag(measurement, key, expected)) + { + measurement.Tags.TryGetValue(key, out var actual); + failures.Add($"expected {measurement.MeterName}:{measurement.InstrumentName}:{key}={expected}, got {actual ?? ""}"); + } + } + + private static bool HasTag(CapturedActivity activity, string key, string expected) + => activity.Tags.TryGetValue(key, out var actual) && StringComparer.Ordinal.Equals(actual, expected); + + private static bool HasTag(CapturedMeasurement measurement, string key, string expected) + => measurement.Tags.TryGetValue(key, out var actual) && StringComparer.Ordinal.Equals(actual, expected); + + private static void Require(bool condition, string failure, ICollection failures) + { + if (!condition) + failures.Add(failure); + } +} + +internal static class ProbeContract +{ + internal const string ExtensionsSourceName = "Experimental.Microsoft.Extensions.AI"; + internal const string AgentsSourceName = "Experimental.Microsoft.Agents.AI"; + internal const string WorkflowsSourceName = "Microsoft.Agents.AI.Workflows"; + internal const string FixedModel = "fixed-model"; + internal const string FixedProvider = "fixed-provider"; + internal const string UserSensitiveContent = "private-user-content"; + internal const string AssistantSensitiveContent = "private-assistant-content"; + internal const string WorkflowSensitiveContent = "private-workflow-content"; +} + +internal static class TelemetryValue +{ + public static string Format(object? value) + => value switch + { + null => string.Empty, + string text => text, + IEnumerable values => string.Join("|", values), + IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty, + _ => value.ToString() ?? string.Empty, + }; +} + +[JsonSerializable(typeof(GenAiReport))] +[JsonSourceGenerationOptions(WriteIndented = true)] +internal sealed partial class RealGenAiJsonContext : JsonSerializerContext; diff --git a/demos/Qyl.RealGenAiDemo/Qyl.RealGenAiDemo.csproj b/demos/Qyl.RealGenAiDemo/Qyl.RealGenAiDemo.csproj new file mode 100644 index 0000000..a1fb240 --- /dev/null +++ b/demos/Qyl.RealGenAiDemo/Qyl.RealGenAiDemo.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + Qyl.RealGenAiDemo + Qyl.RealGenAiDemo + Real first-party GenAI, agent, and workflow telemetry proof for qyl. + + + + + + + + + + + + + + diff --git a/demos/Qyl.RealGraphQlDemo/Program.cs b/demos/Qyl.RealGraphQlDemo/Program.cs index b53558c..7f8fa7f 100644 --- a/demos/Qyl.RealGraphQlDemo/Program.cs +++ b/demos/Qyl.RealGraphQlDemo/Program.cs @@ -130,7 +130,7 @@ public static GraphQlReport Create(string runtimeMode, CapturedActivity[] activi if (error is null) failures.Add("missing error GraphQL execute span"); else - ExpectTag(error, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(ArgumentNullException), failures); + ExpectTag(error, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(ArgumentNullException).FullName!, failures); foreach (var span in graphQlSpans) { diff --git a/demos/Qyl.RealGrpcClientDemo/LiveProbeClient.cs b/demos/Qyl.RealGrpcClientDemo/LiveProbeClient.cs index 202cc36..58fcc38 100644 --- a/demos/Qyl.RealGrpcClientDemo/LiveProbeClient.cs +++ b/demos/Qyl.RealGrpcClientDemo/LiveProbeClient.cs @@ -12,6 +12,12 @@ internal sealed class LiveProbeClient : ClientBase private static readonly Marshaller Bytes = new(static value => value, static value => value); private static readonly Method CollectMethod = new( MethodType.Unary, "qyl.LiveProbe", "Collect", Bytes, Bytes); + private static readonly Method CollectStreamMethod = new( + MethodType.ServerStreaming, "qyl.LiveProbe", "CollectStream", Bytes, Bytes); + private static readonly Method CollectClientStreamMethod = new( + MethodType.ClientStreaming, "qyl.LiveProbe", "CollectClientStream", Bytes, Bytes); + private static readonly Method CollectDuplexMethod = new( + MethodType.DuplexStreaming, "qyl.LiveProbe", "CollectDuplex", Bytes, Bytes); public LiveProbeClient(ChannelBase channel) : base(channel) { @@ -24,6 +30,15 @@ private LiveProbeClient(ClientBaseConfiguration configuration) : base(configurat public AsyncUnaryCall CollectAsync(byte[] request, Metadata? headers = null) => CallInvoker.AsyncUnaryCall(CollectMethod, host: null, new CallOptions(headers), request); + public AsyncServerStreamingCall CollectStream(byte[] request, Metadata? headers = null) + => CallInvoker.AsyncServerStreamingCall(CollectStreamMethod, host: null, new CallOptions(headers), request); + + public AsyncClientStreamingCall CollectClientStream(Metadata? headers = null) + => CallInvoker.AsyncClientStreamingCall(CollectClientStreamMethod, host: null, new CallOptions(headers)); + + public AsyncDuplexStreamingCall CollectDuplex(Metadata? headers = null) + => CallInvoker.AsyncDuplexStreamingCall(CollectDuplexMethod, host: null, new CallOptions(headers)); + protected override LiveProbeClient NewInstance(ClientBaseConfiguration configuration) => new(configuration); } diff --git a/demos/Qyl.RealGrpcClientDemo/Program.cs b/demos/Qyl.RealGrpcClientDemo/Program.cs index 4e088af..f330ccc 100644 --- a/demos/Qyl.RealGrpcClientDemo/Program.cs +++ b/demos/Qyl.RealGrpcClientDemo/Program.cs @@ -71,6 +71,18 @@ { var client = new LiveProbeClient(channel); _ = await client.CollectAsync(Array.Empty(), requestMetadata); + + // Compile-binding coverage for the three streaming interceptor shapes. The generated + // interceptor manifest (and its contract invariant) requires every catalog kind to bind a + // real ClientBase call site in demo evidence; the loopback server only speaks unary, so + // these call sites are guarded behind an env flag no verifier sets and never execute here. + if (string.Equals( + Environment.GetEnvironmentVariable("QYL_GRPC_DEMO_STREAMING_SHAPES"), "1", StringComparison.Ordinal)) + { + using var serverStreaming = client.CollectStream(Array.Empty()); + using var clientStreaming = client.CollectClientStream(); + using var duplexStreaming = client.CollectDuplex(); + } } else { diff --git a/demos/Qyl.RealHttpClientDemo/Program.cs b/demos/Qyl.RealHttpClientDemo/Program.cs index b6bfa35..2602064 100644 --- a/demos/Qyl.RealHttpClientDemo/Program.cs +++ b/demos/Qyl.RealHttpClientDemo/Program.cs @@ -7,7 +7,6 @@ using System.Text.Json; using System.Text.Json.Serialization; using Qyl.OpenTelemetry.AutoInstrumentation; -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; var captured = new List(); var capturedMetrics = new List(); @@ -25,7 +24,7 @@ { InstrumentPublished = static (instrument, listener) => { - if (StringComparer.Ordinal.Equals(instrument.Meter.Name, QylMetricMeters.HttpClientMeterName) && + if (StringComparer.Ordinal.Equals(instrument.Meter.Name, DemoMetricNames.HttpClient) && StringComparer.Ordinal.Equals(instrument.Name, "http.client.request.duration")) { listener.EnableMeasurementEvents(instrument); @@ -176,7 +175,7 @@ public static HttpClientReport Create(string runtimeMode, CapturedActivity[] act var httpClientMetrics = metrics .Where(static metric => - StringComparer.Ordinal.Equals(metric.MeterName, QylMetricMeters.HttpClientMeterName) && + StringComparer.Ordinal.Equals(metric.MeterName, DemoMetricNames.HttpClient) && StringComparer.Ordinal.Equals(metric.Name, "http.client.request.duration")) .ToArray(); @@ -255,6 +254,11 @@ private static void RequireStatus(CapturedActivity? activity, string expected, I } } +internal static class DemoMetricNames +{ + internal const string HttpClient = "System.Net.Http"; +} + [JsonSerializable(typeof(HttpClientReport))] [JsonSourceGenerationOptions(WriteIndented = true)] internal sealed partial class RealHttpClientJsonContext : JsonSerializerContext; diff --git a/demos/Qyl.RealILoggerDemo/Program.cs b/demos/Qyl.RealILoggerDemo/Program.cs index e203bb2..28b433f 100644 --- a/demos/Qyl.RealILoggerDemo/Program.cs +++ b/demos/Qyl.RealILoggerDemo/Program.cs @@ -110,7 +110,7 @@ public static ILoggerReport Create(string runtimeMode, string[] logLines, Captur var error = FindBySeverity(loggerSpans, "Error"); Require(information, "information span", failures); Require(error, "error span", failures); - RequireTag(error, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(InvalidOperationException), failures); + RequireTag(error, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(InvalidOperationException).FullName!, failures); foreach (var span in loggerSpans) { diff --git a/demos/Qyl.RealMassTransitDemo/Program.cs b/demos/Qyl.RealMassTransitDemo/Program.cs index f2bb89c..59ed695 100644 --- a/demos/Qyl.RealMassTransitDemo/Program.cs +++ b/demos/Qyl.RealMassTransitDemo/Program.cs @@ -136,7 +136,7 @@ public static MassTransitReport Create(string runtimeMode, CapturedActivity[] ac Require(publishSuccess, "successful publish span", failures); Require(sendSuccess, "successful send span", failures); Require(publishError, "error publish span", failures); - RequireTag(publishError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, "ArgumentException", failures); + RequireTag(publishError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(ArgumentException).FullName!, failures); foreach (var span in massTransitSpans) { diff --git a/demos/Qyl.RealMcpDemo/Program.cs b/demos/Qyl.RealMcpDemo/Program.cs new file mode 100644 index 0000000..9491df0 --- /dev/null +++ b/demos/Qyl.RealMcpDemo/Program.cs @@ -0,0 +1,428 @@ +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using OpenTelemetry.Trace; +using Qyl; + +const string mcpSourceName = "Experimental.ModelContextProtocol"; +const string rootSourceName = "Qyl.RealMcpDemo"; +const string sessionId = "qyl-mcp-evidence-session"; + +var exportedActivities = new ActivityExportCollection(); +var builder = new HostApplicationBuilder(new HostApplicationBuilderSettings +{ + ApplicationName = "Qyl.RealMcpDemo", + DisableDefaults = true, +}); +builder.Services.AddLogging(); +builder.Logging.ClearProviders(); +builder.AddQyl(options => +{ + options.ServiceName = "qyl-real-mcp-demo"; + options.EnableCollectorDiscovery = false; + options.EnableLogExport = false; + options.EnableMetricsExport = false; + options.AdditionalSources.Add(rootSourceName); +}); +builder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddInMemoryExporter(exportedActivities)); +builder.Services.AddMcpServer(options => + { + options.ServerInfo = new Implementation + { + Name = "qyl-real-mcp-server", + Version = "1.0.0", + }; + }) + .WithTools(); + +using var host = builder.Build(); +await host.StartAsync(); + +var clientToServer = new Pipe(); +var serverToClient = new Pipe(); +await using var serverInput = clientToServer.Reader.AsStream(); +await using var serverOutput = serverToClient.Writer.AsStream(); +await using var clientWrite = clientToServer.Writer.AsStream(); +await using var clientRead = serverToClient.Reader.AsStream(); + +var loggerFactory = host.Services.GetRequiredService(); +var serverOptions = host.Services.GetRequiredService>().Value; +await using var serverTransport = new StreamServerTransport( + serverInput, + serverOutput, + "qyl-real-mcp-server", + loggerFactory); +await using var server = McpServer.Create(serverTransport, serverOptions, loggerFactory, host.Services); +using var serverCancellation = new CancellationTokenSource(TimeSpan.FromSeconds(30)); +var serverTask = server.RunAsync(serverCancellation.Token); + +string rootTraceId; +string rootSpanId; +var toolListed = false; +var toolCalled = false; +using (var rootSource = new ActivitySource(rootSourceName)) +using (var root = rootSource.StartActivity("mcp evidence", ActivityKind.Internal) + ?? throw new InvalidOperationException("qyl did not register the demo root ActivitySource")) +{ + root.SetTag("session.id", sessionId); + rootTraceId = root.TraceId.ToString(); + rootSpanId = root.SpanId.ToString(); + + var clientTransport = new StreamClientTransport(clientWrite, clientRead, loggerFactory); + await using var client = await McpClient.CreateAsync(clientTransport, cancellationToken: serverCancellation.Token); + + var tools = await client.ListToolsAsync(cancellationToken: serverCancellation.Token); + toolListed = tools.Count == 1 && StringComparer.Ordinal.Equals(tools[0].Name, ProbeTools.ToolName); + + var callResult = await client.CallToolAsync( + new CallToolRequestParams + { + Name = ProbeTools.ToolName, + Arguments = new Dictionary(StringComparer.Ordinal) + { + ["secret"] = JsonSerializer.SerializeToElement( + ProbeTools.SensitiveArgument, + RealMcpJsonContext.Default.String), + }, + }, + serverCancellation.Token); + toolCalled = callResult.IsError is not true + && callResult.Content.Count == 1 + && callResult.Content[0] is TextContentBlock text + && StringComparer.Ordinal.Equals(text.Text, ProbeTools.SensitiveResult); +} + +await serverCancellation.CancelAsync(); +await serverTask.WaitAsync(TimeSpan.FromSeconds(10)); +await host.StopAsync(); + +var activities = exportedActivities.Snapshot() + .Where(activity => StringComparer.Ordinal.Equals(activity.Source.Name, mcpSourceName)) + .Select(CapturedActivity.From) + .OrderBy(static activity => activity.Name, StringComparer.Ordinal) + .ThenBy(static activity => activity.Kind, StringComparer.Ordinal) + .ToArray(); +var registrationEnabled = !string.Equals( + Environment.GetEnvironmentVariable("OTEL_DOTNET_AUTO_TRACES_MCP_INSTRUMENTATION_ENABLED"), + "false", + StringComparison.OrdinalIgnoreCase); +var report = McpReport.Create( + RuntimeFeature.IsDynamicCodeSupported ? "dynamic-code-supported" : "nativeaot", + toolListed, + toolCalled, + registrationEnabled, + rootTraceId, + rootSpanId, + activities); + +Console.WriteLine(JsonSerializer.Serialize(report, RealMcpJsonContext.Default.McpReport)); +return report.Pass ? 0 : 1; + +[McpServerToolType] +internal sealed class ProbeTools +{ + internal const string ToolName = "qyl_sensitive_probe"; + internal const string SensitiveArgument = "mcp-sensitive-argument-73f8a9"; + internal const string SensitiveResult = "mcp-sensitive-result-9d20c4"; + + [McpServerTool(Name = ToolName, ReadOnly = true, Idempotent = true, OpenWorld = false)] + [Description("Returns a deterministic private result after validating a private argument.")] + public string Probe( + [Description("A private value used to prove that MCP telemetry does not capture tool arguments.")] + string secret) + => StringComparer.Ordinal.Equals(secret, SensitiveArgument) + ? SensitiveResult + : throw new ArgumentException("The probe argument was not the expected value.", nameof(secret)); +} + +internal sealed record CapturedActivity( + string Source, + string Name, + string Kind, + string Status, + string? StatusDescription, + string TraceId, + string SpanId, + string ParentSpanId, + IReadOnlyDictionary Tags) +{ + internal static CapturedActivity From(Activity activity) + => new( + activity.Source.Name, + activity.DisplayName, + activity.Kind.ToString(), + activity.Status.ToString(), + activity.StatusDescription, + activity.TraceId.ToString(), + activity.SpanId.ToString(), + activity.ParentSpanId.ToString(), + activity.TagObjects.ToDictionary( + static tag => tag.Key, + static tag => Convert.ToString(tag.Value, CultureInfo.InvariantCulture) ?? string.Empty, + StringComparer.Ordinal)); +} + +internal sealed record McpReport( + string RuntimeMode, + bool Pass, + string[] Failures, + bool ToolListed, + bool ToolCalled, + bool QylRegistrationEnabled, + string RootTraceId, + string RootSpanId, + CapturedActivity[] Activities) +{ + internal static McpReport Create( + string runtimeMode, + bool toolListed, + bool toolCalled, + bool registrationEnabled, + string rootTraceId, + string rootSpanId, + CapturedActivity[] activities) + { + var failures = new List(); + if (!toolListed) + failures.Add("the client did not list exactly the rooted probe tool"); + if (!toolCalled) + failures.Add("the rooted probe tool did not return its deterministic result"); + var expectedCount = registrationEnabled ? 8 : 0; + if (activities.Length != expectedCount) + failures.Add($"expected {expectedCount} MCP activities, got {activities.Length.ToString(CultureInfo.InvariantCulture)}"); + + if (!registrationEnabled) + { + return new McpReport( + runtimeMode, + failures.Count is 0, + failures.ToArray(), + toolListed, + toolCalled, + false, + rootTraceId, + rootSpanId, + activities); + } + + RequireOperationPair(activities, "initialize", "initialize", rootTraceId, rootSpanId, failures); + RequireOperationPair( + activities, + "notifications/initialized", + "notifications/initialized", + rootTraceId, + rootSpanId, + failures); + RequireOperationPair(activities, "tools/list", "tools/list", rootTraceId, rootSpanId, failures); + RequireOperationPair( + activities, + $"tools/call {ProbeTools.ToolName}", + "tools/call", + rootTraceId, + rootSpanId, + failures); + + RequireStableSession(activities, "Client", failures); + RequireStableSession(activities, "Server", failures); + + foreach (var activity in activities) + { + if (!StringComparer.Ordinal.Equals(activity.Source, "Experimental.ModelContextProtocol")) + failures.Add($"unexpected MCP ActivitySource: {activity.Source}"); + if (!StringComparer.Ordinal.Equals(activity.Status, "Unset")) + failures.Add($"expected successful status for {activity.Name}/{activity.Kind}, got {activity.Status}"); + RequireTag(activity, "network.transport", "pipe", failures); + + if (ContainsSensitiveValue(activity)) + failures.Add($"sensitive tool argument or result leaked into {activity.Name}/{activity.Kind}"); + } + + var toolActivities = activities + .Where(static activity => StringComparer.Ordinal.Equals( + activity.Name, + $"tools/call {ProbeTools.ToolName}")) + .ToArray(); + foreach (var activity in toolActivities) + { + RequireTag(activity, "gen_ai.tool.name", ProbeTools.ToolName, failures); + RequireTag(activity, "gen_ai.operation.name", "execute_tool", failures); + } + + return new McpReport( + runtimeMode, + failures.Count is 0, + failures.ToArray(), + toolListed, + toolCalled, + true, + rootTraceId, + rootSpanId, + activities); + } + + private static void RequireOperationPair( + CapturedActivity[] activities, + string activityName, + string method, + string rootTraceId, + string rootSpanId, + ICollection failures) + { + var pair = activities + .Where(activity => StringComparer.Ordinal.Equals(activity.Name, activityName)) + .ToArray(); + if (pair.Length != 2) + { + failures.Add($"expected one client and one server activity for {activityName}, got {pair.Length.ToString(CultureInfo.InvariantCulture)}"); + return; + } + + var client = pair.SingleOrDefault(static activity => StringComparer.Ordinal.Equals(activity.Kind, "Client")); + var server = pair.SingleOrDefault(static activity => StringComparer.Ordinal.Equals(activity.Kind, "Server")); + if (client is null || server is null) + { + failures.Add($"missing client/server activity pair for {activityName}"); + return; + } + + RequireTag(client, "mcp.method.name", method, failures); + RequireTag(server, "mcp.method.name", method, failures); + if (!StringComparer.Ordinal.Equals(client.TraceId, rootTraceId) + || !StringComparer.Ordinal.Equals(client.ParentSpanId, rootSpanId)) + { + failures.Add($"client {activityName} was not parented by the qyl evidence root"); + } + + if (!StringComparer.Ordinal.Equals(server.TraceId, client.TraceId) + || !StringComparer.Ordinal.Equals(server.ParentSpanId, client.SpanId)) + { + failures.Add($"server {activityName} did not continue the client trace context"); + } + + RequireTag(client, "session.id", "qyl-mcp-evidence-session", failures); + if (!StringComparer.Ordinal.Equals(method, "initialize")) + { + RequireTag(client, "mcp.protocol.version", "2025-11-25", failures); + RequireTag(server, "mcp.protocol.version", "2025-11-25", failures); + } + } + + private static void RequireStableSession( + IEnumerable activities, + string kind, + ICollection failures) + { + var sessionIds = activities + .Where(activity => StringComparer.Ordinal.Equals(activity.Kind, kind)) + .Select(activity => activity.Tags.GetValueOrDefault("mcp.session.id")) + .Where(static value => !string.IsNullOrEmpty(value)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + if (sessionIds.Length != 1) + failures.Add($"expected one stable {kind} MCP session identifier, got {sessionIds.Length.ToString(CultureInfo.InvariantCulture)}"); + } + + private static bool ContainsSensitiveValue(CapturedActivity activity) + { + if (ContainsSensitiveValue(activity.Name) || ContainsSensitiveValue(activity.StatusDescription)) + return true; + + return activity.Tags.Any(static tag => + ContainsSensitiveValue(tag.Key) || ContainsSensitiveValue(tag.Value)); + } + + private static bool ContainsSensitiveValue(string? value) + => value?.Contains(ProbeTools.SensitiveArgument, StringComparison.Ordinal) is true + || value?.Contains(ProbeTools.SensitiveResult, StringComparison.Ordinal) is true; + + private static void RequireTag( + CapturedActivity activity, + string key, + string expected, + ICollection failures) + { + if (!activity.Tags.TryGetValue(key, out var actual)) + { + failures.Add($"missing {key} on {activity.Name}/{activity.Kind}"); + return; + } + + if (!StringComparer.Ordinal.Equals(actual, expected)) + failures.Add($"expected {key}={expected} on {activity.Name}/{activity.Kind}, got {actual}"); + } +} + +[JsonSerializable(typeof(McpReport))] +[JsonSourceGenerationOptions(WriteIndented = true)] +internal sealed partial class RealMcpJsonContext : JsonSerializerContext; + +internal sealed class ActivityExportCollection : ICollection +{ + private readonly Lock _gate = new(); + private readonly List _items = []; + + public int Count + { + get + { + lock (_gate) + return _items.Count; + } + } + + public bool IsReadOnly => false; + + public void Add(Activity item) + { + lock (_gate) + _items.Add(item); + } + + public void Clear() + { + lock (_gate) + _items.Clear(); + } + + public bool Contains(Activity item) + { + lock (_gate) + return _items.Contains(item); + } + + public void CopyTo(Activity[] array, int arrayIndex) + { + lock (_gate) + _items.CopyTo(array, arrayIndex); + } + + public bool Remove(Activity item) + { + lock (_gate) + return _items.Remove(item); + } + + public IEnumerator GetEnumerator() + => ((IEnumerable)Snapshot()).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + internal Activity[] Snapshot() + { + lock (_gate) + return _items.ToArray(); + } +} diff --git a/demos/Qyl.RealMcpDemo/Qyl.RealMcpDemo.csproj b/demos/Qyl.RealMcpDemo/Qyl.RealMcpDemo.csproj new file mode 100644 index 0000000..a4e8d6b --- /dev/null +++ b/demos/Qyl.RealMcpDemo/Qyl.RealMcpDemo.csproj @@ -0,0 +1,20 @@ + + + + Exe + net10.0 + Qyl.RealMcpDemo + Qyl.RealMcpDemo + Official in-process MCP client/server trace and NativeAOT proof for qyl. + + + + + + + + + + + + diff --git a/demos/Qyl.RealMongoDbDemo/Program.cs b/demos/Qyl.RealMongoDbDemo/Program.cs index c50bec5..541baf8 100644 --- a/demos/Qyl.RealMongoDbDemo/Program.cs +++ b/demos/Qyl.RealMongoDbDemo/Program.cs @@ -128,7 +128,7 @@ public static MongoDbReport Create(string runtimeMode, CapturedActivity[] activi Require(insertError, "error insert span", failures); Require(countSuccess, "successful count span", failures); Require(deleteSuccess, "successful delete span", failures); - RequireTag(insertError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, "MongoWriteException", failures); + RequireTag(insertError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(MongoDB.Driver.MongoWriteException).FullName!, failures); foreach (var span in mongoSpans) { diff --git a/demos/Qyl.RealMySqlConnectorDemo/Program.cs b/demos/Qyl.RealMySqlConnectorDemo/Program.cs index d950562..9eb8e87 100644 --- a/demos/Qyl.RealMySqlConnectorDemo/Program.cs +++ b/demos/Qyl.RealMySqlConnectorDemo/Program.cs @@ -85,7 +85,7 @@ public static MySqlConnectorReport Create(string runtimeMode, CapturedActivity[] RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemName, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemNameValues.Mysql, failures); RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.OperationName, "SELECT", failures); - RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(InvalidOperationException), failures); + RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(InvalidOperationException).FullName!, failures); RequireMissingTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.QueryText, failures); } diff --git a/demos/Qyl.RealMySqlDataDemo/Program.cs b/demos/Qyl.RealMySqlDataDemo/Program.cs index 0fbabe0..666d10b 100644 --- a/demos/Qyl.RealMySqlDataDemo/Program.cs +++ b/demos/Qyl.RealMySqlDataDemo/Program.cs @@ -85,7 +85,7 @@ public static MySqlDataReport Create(string runtimeMode, CapturedActivity[] acti RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemName, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemNameValues.Mysql, failures); RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.OperationName, "SELECT", failures); - RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(InvalidOperationException), failures); + RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(InvalidOperationException).FullName!, failures); RequireMissingTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.QueryText, failures); } diff --git a/demos/Qyl.RealNServiceBusDemo/Program.cs b/demos/Qyl.RealNServiceBusDemo/Program.cs index 1c1d30d..2f1aec4 100644 --- a/demos/Qyl.RealNServiceBusDemo/Program.cs +++ b/demos/Qyl.RealNServiceBusDemo/Program.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging; using NServiceBus; using Qyl.OpenTelemetry.AutoInstrumentation; -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; var captured = new List(); var capturedMetrics = new List(); @@ -25,7 +24,7 @@ using var meterListener = new MeterListener(); meterListener.InstrumentPublished = static (instrument, listener) => { - if (StringComparer.Ordinal.Equals(instrument.Meter.Name, QylMetricMeters.NServiceBusMeterName) && + if (StringComparer.Ordinal.Equals(instrument.Meter.Name, DemoMetricNames.NServiceBus) && StringComparer.Ordinal.Equals(instrument.Name, "nservicebus.messaging.operation.duration")) { listener.EnableMeasurementEvents(instrument); @@ -186,7 +185,7 @@ public static NServiceBusReport Create( .ToArray(); var nServiceBusMetrics = metrics .Where(static metric => - StringComparer.Ordinal.Equals(metric.MeterName, QylMetricMeters.NServiceBusMeterName) && + StringComparer.Ordinal.Equals(metric.MeterName, DemoMetricNames.NServiceBus) && StringComparer.Ordinal.Equals(metric.Name, "nservicebus.messaging.operation.duration")) .ToArray(); @@ -279,6 +278,11 @@ private static void RequireMetricTag(CapturedMetric metric, string key, string e } } +internal static class DemoMetricNames +{ + internal const string NServiceBus = "Qyl.OpenTelemetry.AutoInstrumentation.NServiceBus"; +} + [JsonSerializable(typeof(NServiceBusReport))] [JsonSourceGenerationOptions(WriteIndented = true)] internal sealed partial class RealNServiceBusJsonContext : JsonSerializerContext; diff --git a/demos/Qyl.RealNetRuntimeMetricsDemo/Program.cs b/demos/Qyl.RealNetRuntimeMetricsDemo/Program.cs index 70440dd..c6fae91 100644 --- a/demos/Qyl.RealNetRuntimeMetricsDemo/Program.cs +++ b/demos/Qyl.RealNetRuntimeMetricsDemo/Program.cs @@ -5,13 +5,12 @@ using OpenTelemetry; using OpenTelemetry.Metrics; using Qyl.OpenTelemetry.AutoInstrumentation; -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; var exportedMetrics = new List(); using var meterProvider = Sdk .CreateMeterProviderBuilder() - .AddMeter(QylMetricMeters.NetRuntimeMeterName) + .AddMeter(DemoMetricNames.Runtime) .AddRuntimeInstrumentation() .AddInMemoryExporter(exportedMetrics) .Build(); @@ -75,7 +74,7 @@ public static NetRuntimeMetricsReport Create(string runtimeMode, CapturedMetric[ { var failures = new List(); var runtimeMetrics = metrics - .Where(static metric => StringComparer.Ordinal.Equals(metric.MeterName, QylMetricMeters.NetRuntimeMeterName)) + .Where(static metric => StringComparer.Ordinal.Equals(metric.MeterName, DemoMetricNames.Runtime)) .ToArray(); if (runtimeMetrics.Length is 0) @@ -104,6 +103,11 @@ private static void RequireMetric(IEnumerable metrics, string na } } +internal static class DemoMetricNames +{ + internal const string Runtime = "System.Runtime"; +} + [JsonSerializable(typeof(NetRuntimeMetricsReport))] [JsonSourceGenerationOptions(WriteIndented = true)] internal sealed partial class RealNetRuntimeMetricsJsonContext : JsonSerializerContext; diff --git a/demos/Qyl.RealNpgsqlDemo/Program.cs b/demos/Qyl.RealNpgsqlDemo/Program.cs index c25f396..1de9bc6 100644 --- a/demos/Qyl.RealNpgsqlDemo/Program.cs +++ b/demos/Qyl.RealNpgsqlDemo/Program.cs @@ -6,7 +6,6 @@ using System.Text.Json.Serialization; using Npgsql; using Qyl.OpenTelemetry.AutoInstrumentation; -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; var capturedActivities = new List(); using var activityListener = new ActivityListener @@ -22,7 +21,7 @@ { InstrumentPublished = static (instrument, listener) => { - if (instrument.Meter.Name == QylMetricMeters.DatabaseMeterName) + if (instrument.Meter.Name == DemoMetricNames.Database) listener.EnableMeasurementEvents(instrument); }, }; @@ -128,7 +127,7 @@ public static NpgsqlReport Create(string runtimeMode, CapturedActivity[] activit RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemName, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemNameValues.Postgresql, failures); RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.OperationName, "SELECT", failures); - RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(InvalidOperationException), failures); + RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(InvalidOperationException).FullName!, failures); RequireMissingTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.QueryText, failures); } @@ -160,6 +159,11 @@ private static void RequireMissingTag(CapturedActivity activity, string key, ICo } } +internal static class DemoMetricNames +{ + internal const string Database = "Qyl.OpenTelemetry.AutoInstrumentation.Database"; +} + [JsonSerializable(typeof(NpgsqlReport))] [JsonSourceGenerationOptions(WriteIndented = true)] internal sealed partial class RealNpgsqlJsonContext : JsonSerializerContext; diff --git a/demos/Qyl.RealOracleMdaDemo/Program.cs b/demos/Qyl.RealOracleMdaDemo/Program.cs index 2914506..b2093c1 100644 --- a/demos/Qyl.RealOracleMdaDemo/Program.cs +++ b/demos/Qyl.RealOracleMdaDemo/Program.cs @@ -92,7 +92,7 @@ public static OracleMdaReport Create(string runtimeMode, CapturedActivity[] acti RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemName, Qyl.OpenTelemetry.SemanticConventions.Incubating.Attributes.Db.DbAttributes.SystemNameValues.OracleDb, failures); RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.OperationName, "SELECT", failures); RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.QuerySummary, "SELECT", failures); - RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(InvalidOperationException), failures); + RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(InvalidOperationException).FullName!, failures); if (StatementOptIn) RequireTagPrefix(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.QueryText, "SELECT", failures); else diff --git a/demos/Qyl.RealQuartzDemo/Program.cs b/demos/Qyl.RealQuartzDemo/Program.cs index c262de6..cbe7356 100644 --- a/demos/Qyl.RealQuartzDemo/Program.cs +++ b/demos/Qyl.RealQuartzDemo/Program.cs @@ -122,7 +122,7 @@ public static QuartzReport Create(string runtimeMode, CapturedActivity[] activit if (error is null) failures.Add("missing error Quartz execute span"); else if (!error.Tags.TryGetValue(Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, out var errorType) || - !StringComparer.Ordinal.Equals(errorType, "InvalidOperationException")) + !StringComparer.Ordinal.Equals(errorType, "System.InvalidOperationException")) failures.Add("expected error.type=InvalidOperationException on error span"); foreach (var span in quartzSpans) diff --git a/demos/Qyl.RealRabbitMqDemo/Program.cs b/demos/Qyl.RealRabbitMqDemo/Program.cs index 3b4eca3..8faa7d6 100644 --- a/demos/Qyl.RealRabbitMqDemo/Program.cs +++ b/demos/Qyl.RealRabbitMqDemo/Program.cs @@ -136,7 +136,7 @@ public static RabbitMqReport Create(string runtimeMode, CapturedActivity[] activ } foreach (var span in error) - RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, "AlreadyClosedException", failures); + RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(RabbitMQ.Client.Exceptions.AlreadyClosedException).FullName!, failures); return new RabbitMqReport(runtimeMode, failures.Count is 0, failures.ToArray(), rabbitSpans); } diff --git a/demos/Qyl.RealRedisDemo/Program.cs b/demos/Qyl.RealRedisDemo/Program.cs index cd1a4ed..bafe790 100644 --- a/demos/Qyl.RealRedisDemo/Program.cs +++ b/demos/Qyl.RealRedisDemo/Program.cs @@ -120,7 +120,7 @@ public static RedisReport Create(string runtimeMode, CapturedActivity[] activiti Require(get, "successful GET span", failures); Require(del, "successful DEL span", failures); Require(executeError, "error EXECUTE span", failures); - RequireTag(executeError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, "RedisServerException", failures); + RequireTag(executeError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(StackExchange.Redis.RedisServerException).FullName!, failures); foreach (var span in redisSpans) { diff --git a/demos/Qyl.RealSqlClientDemo/Program.cs b/demos/Qyl.RealSqlClientDemo/Program.cs index 22d732e..26372f5 100644 --- a/demos/Qyl.RealSqlClientDemo/Program.cs +++ b/demos/Qyl.RealSqlClientDemo/Program.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Diagnostics.Metrics; using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Json; @@ -23,13 +24,29 @@ ActivitySource.AddActivityListener(listener); +var capturedMetrics = new List(); +using var meterListener = new MeterListener +{ + InstrumentPublished = static (instrument, listener) => + { + if (instrument.Meter.Name == DemoMetricNames.Database) + listener.EnableMeasurementEvents(instrument); + }, +}; +meterListener.SetMeasurementEventCallback((instrument, measurement, tags, _) => +{ + if (instrument.Name == "db.client.operation.duration") + capturedMetrics.Add(CapturedMetric.From(instrument, measurement, tags)); +}); +meterListener.Start(); + await WaitForSqlServerAsync(connectionString); await using (var connection = new SqlConnection(connectionString)) { await connection.OpenAsync(); - await ExecuteNonQueryAsync(connection, "CREATE TABLE #QylProbe (Id int NOT NULL, Name nvarchar(32) NOT NULL)"); + ExecuteNonQuery(connection, "CREATE TABLE #QylProbe (Id int NOT NULL, Name nvarchar(32) NOT NULL)"); await ExecuteNonQueryAsync(connection, "INSERT INTO #QylProbe (Id, Name) VALUES (1, N'alpha')"); _ = await ExecuteScalarAsync(connection, "SELECT Id FROM #QylProbe WHERE Id = 1"); @@ -45,8 +62,10 @@ var report = SqlClientReport.Create( RuntimeFeature.IsDynamicCodeSupported ? "dynamic-code-supported" : "nativeaot", + Environment.GetEnvironmentVariable("QYL_SQLCLIENT_EXPECTED_TRACE_OWNER") ?? "source_interceptor", Environment.GetEnvironmentVariable("QYL_SQLCLIENT_EXPECTED_PORT") ?? "11433", - captured.ToArray()); + captured.ToArray(), + capturedMetrics.ToArray()); var json = JsonSerializer.Serialize(report, RealSqlClientJsonContext.Default.SqlClientReport); Console.WriteLine(json); @@ -76,6 +95,13 @@ static async Task WaitForSqlServerAsync(string connectionString) throw new InvalidOperationException("SQL Server did not become ready.", lastException); } +static void ExecuteNonQuery(SqlConnection connection, string sql) +{ + using var command = connection.CreateCommand(); + command.CommandText = sql; + command.ExecuteNonQuery(); +} + static async Task ExecuteNonQueryAsync(SqlConnection connection, string sql) { await using var command = connection.CreateCommand(); @@ -94,6 +120,7 @@ internal sealed record CapturedActivity( string Name, string Kind, string Status, + double DurationSeconds, IReadOnlyDictionary Tags) { public static CapturedActivity From(Activity activity) @@ -101,29 +128,69 @@ public static CapturedActivity From(Activity activity) activity.DisplayName, activity.Kind.ToString(), activity.Status.ToString(), + activity.Duration.TotalSeconds, activity.TagObjects.ToDictionary( static tag => tag.Key, static tag => Convert.ToString(tag.Value, CultureInfo.InvariantCulture) ?? string.Empty, StringComparer.Ordinal)); } +internal sealed record CapturedMetric( + string Name, + double Value, + IReadOnlyDictionary Tags) +{ + public static CapturedMetric From(Instrument instrument, double value, ReadOnlySpan> tags) + { + var capturedTags = new Dictionary(StringComparer.Ordinal); + foreach (var tag in tags) + capturedTags[tag.Key] = Convert.ToString(tag.Value, CultureInfo.InvariantCulture) ?? string.Empty; + + return new CapturedMetric(instrument.Name, value, capturedTags); + } +} + internal sealed record SqlClientReport( string RuntimeMode, + string TraceOwner, bool Pass, string[] Failures, - CapturedActivity[] Activities) + CapturedActivity[] Activities, + CapturedMetric[] Metrics) { - public static SqlClientReport Create(string runtimeMode, string expectedServerPort, CapturedActivity[] activities) + public static SqlClientReport Create( + string runtimeMode, + string traceOwner, + string expectedServerPort, + CapturedActivity[] activities, + CapturedMetric[] metrics) { var failures = new List(); + var sourceInterceptorExpected = StringComparer.Ordinal.Equals(traceOwner, "source_interceptor"); + if (!sourceInterceptorExpected && !StringComparer.Ordinal.Equals(traceOwner, "specialist_listener")) + failures.Add($"unknown trace owner: {traceOwner}"); + var expectedDomain = sourceInterceptorExpected ? "db.client" : "db.sqlclient"; var sqlSpans = activities .Where(static activity => - activity.Tags.TryGetValue("qyl.instrumentation.domain", out var domain) && - StringComparer.Ordinal.Equals(domain, "db.sqlclient")) + activity.Tags.ContainsKey("qyl.instrumentation.domain")) + .Where(activity => StringComparer.Ordinal.Equals(activity.Tags["qyl.instrumentation.domain"], expectedDomain)) + .ToArray(); + var sqlMetrics = metrics + .Where(static metric => + StringComparer.Ordinal.Equals(metric.Name, "db.client.operation.duration") && + metric.Tags.TryGetValue(Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemName, out var system) && + StringComparer.Ordinal.Equals(system, Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes.SystemNameValues.MicrosoftSqlServer)) .ToArray(); + var expectedMetricCount = sourceInterceptorExpected ? 4 : 0; + if (activities.Length != 4) + failures.Add($"expected exactly 4 captured qyl activities, got {activities.Length}"); if (sqlSpans.Length != 4) - failures.Add($"expected 4 real SqlClient command spans, got {sqlSpans.Length}"); + failures.Add($"expected 4 {traceOwner} SqlClient command spans, got {sqlSpans.Length}"); + if (metrics.Length != expectedMetricCount) + failures.Add($"expected exactly {expectedMetricCount} captured qyl metrics, got {metrics.Length}"); + if (sqlMetrics.Length != expectedMetricCount) + failures.Add($"expected {expectedMetricCount} SqlClient duration metric points, got {sqlMetrics.Length}"); var successSelect = FindByOperationAndStatus(sqlSpans, "SELECT", "Unset"); var errorSelect = FindByOperationAndStatus(sqlSpans, "SELECT", "Error"); @@ -133,9 +200,12 @@ public static SqlClientReport Create(string runtimeMode, string expectedServerPo RequireTag(successSelect, "db.system.name", "microsoft.sql_server", failures); RequireTag(successSelect, "db.namespace", "tempdb", failures); RequireTag(successSelect, "db.operation.name", "SELECT", failures); - RequireTag(successSelect, "db.query.summary", "Text SELECT", failures); - RequireTag(successSelect, "server.address", "127.0.0.1", failures); - RequireTag(successSelect, "server.port", expectedServerPort, failures); + RequireTag(successSelect, "db.query.summary", sourceInterceptorExpected ? "SELECT" : "Text SELECT", failures); + if (!sourceInterceptorExpected) + { + RequireTag(successSelect, "server.address", "127.0.0.1", failures); + RequireTag(successSelect, "server.port", expectedServerPort, failures); + } if (string.Equals( Environment.GetEnvironmentVariable("OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT"), "true", @@ -147,16 +217,27 @@ public static SqlClientReport Create(string runtimeMode, string expectedServerPo { RequireMissingTag(successSelect, "db.query.text", failures); } - RequireTag(errorSelect, "error.type", "208", failures); + RequireTag(errorSelect, "error.type", sourceInterceptorExpected ? typeof(SqlException).FullName! : "208", failures); RequireTag(errorSelect, "db.operation.name", "SELECT", failures); foreach (var span in sqlSpans) { - if (span.Name is not "SQL CREATE" and not "SQL INSERT" and not "SQL SELECT") + var expectedPrefix = sourceInterceptorExpected ? "DB " : "SQL "; + if (!span.Name.StartsWith(expectedPrefix, StringComparison.Ordinal)) failures.Add($"unexpected SqlClient span name: {span.Name}"); + if (!StringComparer.Ordinal.Equals(span.Kind, ActivityKind.Client.ToString())) + failures.Add($"expected SqlClient span kind Client, got {span.Kind}"); + if (span.DurationSeconds <= 0) + failures.Add($"expected positive SqlClient span duration, got {span.DurationSeconds.ToString(CultureInfo.InvariantCulture)}"); } - return new SqlClientReport(runtimeMode, failures.Count is 0, failures.ToArray(), activities); + foreach (var metric in sqlMetrics) + { + if (metric.Value <= 0) + failures.Add($"expected positive SqlClient duration, got {metric.Value.ToString(CultureInfo.InvariantCulture)}"); + } + + return new SqlClientReport(runtimeMode, traceOwner, failures.Count is 0, failures.ToArray(), activities, metrics); } private static CapturedActivity? FindByOperationAndStatus(IEnumerable activities, string operation, string status) @@ -211,6 +292,11 @@ private static void RequireMissingTag(CapturedActivity? activity, string key, IC } } +internal static class DemoMetricNames +{ + internal const string Database = "Qyl.OpenTelemetry.AutoInstrumentation.Database"; +} + [JsonSerializable(typeof(SqlClientReport))] [JsonSourceGenerationOptions(WriteIndented = true)] internal sealed partial class RealSqlClientJsonContext : JsonSerializerContext; diff --git a/demos/Qyl.RealSqlClientDemo/Qyl.RealSqlClientDemo.csproj b/demos/Qyl.RealSqlClientDemo/Qyl.RealSqlClientDemo.csproj index fd4b42a..fa59b10 100644 --- a/demos/Qyl.RealSqlClientDemo/Qyl.RealSqlClientDemo.csproj +++ b/demos/Qyl.RealSqlClientDemo/Qyl.RealSqlClientDemo.csproj @@ -5,7 +5,8 @@ net10.0 Qyl.RealSqlClientDemo Qyl.RealSqlClientDemo - Real zero-code Microsoft.Data.SqlClient DiagnosticSource proof for qyl. + Real Microsoft.Data.SqlClient source-interceptor ownership and specialist-listener fallback proof for qyl. + true @@ -14,6 +15,13 @@ + + @@ -22,4 +30,7 @@ Visible="false" /> + + diff --git a/demos/Qyl.RealSqliteDemo/Program.cs b/demos/Qyl.RealSqliteDemo/Program.cs index d365b09..d6130b9 100644 --- a/demos/Qyl.RealSqliteDemo/Program.cs +++ b/demos/Qyl.RealSqliteDemo/Program.cs @@ -102,7 +102,7 @@ public static SqliteReport Create(string runtimeMode, CapturedActivity[] activit Require(insert, "successful INSERT span", failures); Require(selectSuccess, "successful SELECT span", failures); Require(selectError, "error SELECT span", failures); - RequireTag(selectError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(SqliteException), failures); + RequireTag(selectError, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(SqliteException).FullName!, failures); foreach (var span in sqliteSpans) { diff --git a/demos/Qyl.RealWcfClientDemo/Program.cs b/demos/Qyl.RealWcfClientDemo/Program.cs index 0c64aa5..c173870 100644 --- a/demos/Qyl.RealWcfClientDemo/Program.cs +++ b/demos/Qyl.RealWcfClientDemo/Program.cs @@ -122,7 +122,7 @@ public static WcfClientReport Create(string runtimeMode, CapturedActivity[] acti failures.Add($"expected WCF span status Error, got {span.Status}"); RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Incubating.Attributes.Rpc.RpcAttributes.SystemName, Qyl.OpenTelemetry.SemanticConventions.Incubating.Attributes.Rpc.RpcAttributes.SystemValues.DotnetWcf, failures); - RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, nameof(CommunicationException), failures); + RequireTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes.Type, typeof(CommunicationException).FullName!, failures); RequireMissingTag(span, Qyl.OpenTelemetry.SemanticConventions.Attributes.Url.UrlAttributes.Full, failures); } diff --git a/docs/contracts/qyl-aot-ownership.yaml b/docs/contracts/qyl-aot-ownership.yaml index a43ba47..74b194f 100644 --- a/docs/contracts/qyl-aot-ownership.yaml +++ b/docs/contracts/qyl-aot-ownership.yaml @@ -68,13 +68,15 @@ ownership_items: key: signals.traces.AZURE lane: framework_initialization qyl_status: implemented - call_site_visibility: user_code + call_site_visibility: library_internal payload_access: not_applicable evidence_level: verified_nativeaot - primary_owner: QylAutoInstrumentationGenerator framework initialization + primary_owner: Azure SDK first-party Azure.* ActivitySources enabled by QylInstrumentation/Qyl.Sdk with Qyl enrichment evidence: - - src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs - - tools/verify-source-interceptor-consumer.py + - src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentation.cs + - src/Qyl.Sdk/QylTelemetrySources.cs + - src/Qyl.Sdk/QylAzureSpanProcessor.cs + - src/Qyl.Sdk/QylSdkHostApplicationBuilderExtensions.cs - demos/Qyl.RealAzureDemo - tools/verify-real-azure-demo.py conformance_signals: @@ -137,13 +139,12 @@ ownership_items: key: signals.traces.ENTITYFRAMEWORKCORE lane: runtime_public_telemetry qyl_status: implemented - call_site_visibility: both + call_site_visibility: library_internal payload_access: typed_public evidence_level: verified_nativeaot - primary_owner: Qyl public telemetry listeners/meters + primary_owner: EntityFrameworkCore specialist package EntityFrameworkCoreDiagnosticListener evidence: - - src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners - - src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricMeters.cs + - src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs - demos/Qyl.RealEfCoreDemo - tools/verify-real-efcore-demo.py conformance_signals: @@ -444,9 +445,11 @@ ownership_items: call_site_visibility: user_code payload_access: not_applicable evidence_level: verified_nativeaot - primary_owner: QylAutoInstrumentationGenerator + primary_owner: QylAutoInstrumentationGenerator; dependency-specific SqlClient listener fallback when no interceptor is emitted evidence: - src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs + - src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedDbCommand.cs + - src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientDiagnosticListener.cs - tools/verify-source-interceptor-consumer.py - demos/Qyl.RealSqlClientDemo - tools/verify-real-sqlclient-demo.py @@ -527,17 +530,17 @@ ownership_items: - tools/verify-real-wcf-client-demo.py - contract_item_id: OTEL_DOTNET_AUTO_CONTRACT_025 key: signals.traces.WCFCORE - lane: unsupported_nativeaot - qyl_status: unsupported_nativeaot + lane: official_library_hook + qyl_status: implemented call_site_visibility: library_internal - payload_access: reflection_required - evidence_level: none - primary_owner: unsupported by qyl NativeAOT substrate + payload_access: typed_public + supported_versions: CoreWCF.Primitives ==1.9.1; CoreWCF.Http ==1.9.1; server ActivitySource path only + evidence_level: verified_managed + primary_owner: Qyl.Sdk CoreWCF.Primitives ActivitySource registration evidence: - - docs/coverage-matrix.md - unsupported_reason: 'Not yet implemented rather than impossible: CoreWCF >=1.8 natively emits server-side - spans through its own ActivitySource (''CoreWCF.Primitives''), so registration-lane coverage is feasible - (managed); planned on the qyl roadmap. Unsupported today because no executable owner exists yet.' + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealCoreWcfDemo + - tools/verify-real-corewcf-demo.py - contract_item_id: OTEL_DOTNET_AUTO_CONTRACT_026 key: signals.traces.WCFSERVICE lane: unsupported_nativeaot @@ -719,6 +722,7 @@ ownership_items: primary_owner: QylAutoInstrumentationGenerator + QylDbClientMetrics evidence: - src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs + - src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedDbCommand.cs - src/Qyl.OpenTelemetry.AutoInstrumentation/QylDbClientMetrics.cs - tools/verify-source-interceptor-consumer.py - demos/Qyl.RealSqlClientDemo diff --git a/docs/contracts/qyl-native-instrumentations.yaml b/docs/contracts/qyl-native-instrumentations.yaml new file mode 100644 index 0000000..1c38ffe --- /dev/null +++ b/docs/contracts/qyl-native-instrumentations.yaml @@ -0,0 +1,255 @@ +schema_id: qyl-native-instrumentation-contract-items +schema_version: 1.0.0 +generated_at: '2026-07-19' +source: + title: qyl-native version-pinned instrumentation promises + source_kind: qyl_owned_contract_with_official_library_sources + content_basis: First-class telemetry hooks registered by Qyl.Sdk 8.0.0 and exercised by owned real-library demos + release: 8.0.0 +contract_items: +- kind: signal_specific_instrumentation_promise + key: signals.traces.MICROSOFTEXTENSIONSAI + signal: traces + instrumentation_id: MICROSOFTEXTENSIONSAI + environment_toggle: OTEL_DOTNET_AUTO_TRACES_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Extensions.AI + url: https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0 + supported_versions: Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.Microsoft.Extensions.AI; the application explicitly installs the official UseOpenTelemetry wrapper. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 61 + contract_item_id: QYL_NATIVE_CONTRACT_001 + upstream_sources: + - label: Microsoft.Extensions.AI 10.8.0 + url: https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0 + - label: official UseOpenTelemetry documentation + url: https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk source registration plus Microsoft.Extensions.AI UseOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py +- kind: signal_specific_instrumentation_promise + key: signals.metrics.MICROSOFTEXTENSIONSAI + signal: metrics + instrumentation_id: MICROSOFTEXTENSIONSAI + environment_toggle: OTEL_DOTNET_AUTO_METRICS_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Extensions.AI + url: https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0 + supported_versions: Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.Microsoft.Extensions.AI; the application explicitly installs the official UseOpenTelemetry wrapper. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 62 + contract_item_id: QYL_NATIVE_CONTRACT_002 + upstream_sources: + - label: Microsoft.Extensions.AI 10.8.0 + url: https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0 + - label: official UseOpenTelemetry documentation + url: https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk meter registration plus Microsoft.Extensions.AI UseOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py +- kind: signal_specific_instrumentation_promise + key: signals.traces.MICROSOFTAGENTSAI + signal: traces + instrumentation_id: MICROSOFTAGENTSAI + environment_toggle: OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Agents.AI + url: https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0 + supported_versions: Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.Microsoft.Agents.AI; the application explicitly installs the official UseOpenTelemetry wrapper. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 63 + contract_item_id: QYL_NATIVE_CONTRACT_003 + upstream_sources: + - label: Microsoft.Agents.AI 1.13.0 + url: https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0 + - label: official UseOpenTelemetry API + url: https://learn.microsoft.com/dotnet/api/microsoft.agents.ai.opentelemetryagentbuilderextensions.useopentelemetry + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk source registration plus Microsoft.Agents.AI UseOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py +- kind: signal_specific_instrumentation_promise + key: signals.metrics.MICROSOFTAGENTSAI + signal: metrics + instrumentation_id: MICROSOFTAGENTSAI + environment_toggle: OTEL_DOTNET_AUTO_METRICS_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Agents.AI + url: https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0 + supported_versions: Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.Microsoft.Agents.AI; the application explicitly installs the official UseOpenTelemetry wrapper. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 64 + contract_item_id: QYL_NATIVE_CONTRACT_004 + upstream_sources: + - label: Microsoft.Agents.AI 1.13.0 + url: https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0 + - label: official UseOpenTelemetry API + url: https://learn.microsoft.com/dotnet/api/microsoft.agents.ai.opentelemetryagentbuilderextensions.useopentelemetry + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk meter registration plus Microsoft.Agents.AI UseOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py +- kind: signal_specific_instrumentation_promise + key: signals.traces.MICROSOFTAGENTSAIWORKFLOWS + signal: traces + instrumentation_id: MICROSOFTAGENTSAIWORKFLOWS + environment_toggle: OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAIWORKFLOWS_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Agents.AI.Workflows + url: https://www.nuget.org/packages/Microsoft.Agents.AI.Workflows/1.13.0 + supported_versions: Microsoft.Agents.AI.Workflows ==1.13.0; WorkflowBuilder.WithOpenTelemetry() path only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Microsoft.Agents.AI.Workflows; the application explicitly calls WithOpenTelemetry. + - The 8.0.0 promise is traces only because this exact workflow hook exposes an ActivitySource, not a corresponding meter. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 65 + contract_item_id: QYL_NATIVE_CONTRACT_005 + upstream_sources: + - label: Microsoft.Agents.AI.Workflows 1.13.0 + url: https://www.nuget.org/packages/Microsoft.Agents.AI.Workflows/1.13.0 + - label: official Agent Framework repository + url: https://github.com/microsoft/agent-framework + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk source registration plus Microsoft.Agents.AI.Workflows WithOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py +- kind: signal_specific_instrumentation_promise + key: signals.traces.MCP + signal: traces + instrumentation_id: MCP + environment_toggle: OTEL_DOTNET_AUTO_TRACES_MCP_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: ModelContextProtocol + url: https://www.nuget.org/packages/ModelContextProtocol/1.4.1 + supported_versions: ModelContextProtocol ==1.4.1; automatic official client/server ActivitySource path only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.ModelContextProtocol; the official SDK emits the client/server spans automatically. + - MCP metrics are deliberately outside the 8.0.0 contract because the official instruments attach dynamic tool and resource names as dimensions, violating qyl's bounded-cardinality policy. + - This exact path and version is verified in managed and NativeAOT execution; it is not a protocol-wide claim. + index: 66 + contract_item_id: QYL_NATIVE_CONTRACT_006 + upstream_sources: + - label: ModelContextProtocol 1.4.1 + url: https://www.nuget.org/packages/ModelContextProtocol/1.4.1 + - label: official MCP C# SDK + url: https://github.com/modelcontextprotocol/csharp-sdk + lane: official_library_hook + qyl_status: implemented + call_site_visibility: library_internal + payload_access: typed_public + evidence_level: verified_nativeaot + primary_owner: Qyl.Sdk source registration plus automatic ModelContextProtocol telemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealMcpDemo + - tools/verify-real-mcp-demo.py + conformance_signals: + - kind: span + name: mcp.initialize + required_attributes: + - mcp.method.name + - mcp.session.id + - network.transport + recommended_attributes: + - session.id + opt_in_attributes: [] + - kind: span + name: mcp.notifications.initialized + required_attributes: + - mcp.method.name + - mcp.session.id + - network.transport + recommended_attributes: + - session.id + opt_in_attributes: [] + - kind: span + name: mcp.tools.list + required_attributes: + - mcp.method.name + - mcp.session.id + - network.transport + recommended_attributes: + - session.id + opt_in_attributes: [] + - kind: span + name: mcp.tools.call + required_attributes: + - gen_ai.operation.name + - gen_ai.tool.name + - mcp.method.name + - mcp.protocol.version + - mcp.session.id + - network.transport + recommended_attributes: + - session.id + opt_in_attributes: [] diff --git a/docs/coverage-matrix.md b/docs/coverage-matrix.md index 54553c8..7cb8f67 100644 --- a/docs/coverage-matrix.md +++ b/docs/coverage-matrix.md @@ -4,15 +4,17 @@ This matrix is generated from `docs/generated/qyl-aot-contract.resolved.yaml`. -The raw upstream contract lives in `docs/contracts/otel-dotnet-auto-60.upstream.yaml`; qyl ownership and evidence live in `docs/contracts/qyl-aot-ownership.yaml`. -Every row carries a clickable upstream source anchored to the official OpenTelemetry documentation source. +The exact 60-row upstream contract lives in `docs/contracts/otel-dotnet-auto-60.upstream.yaml`; qyl-native promises live in `docs/contracts/qyl-native-instrumentations.yaml`; qyl ownership and evidence for upstream rows live in `docs/contracts/qyl-aot-ownership.yaml`. +Every row identifies its contract origin and carries a clickable authoritative source. ## Counts | Count | Value | |---|---:| -| Total contract items | 60 | -| Signal promises | 37 | +| Total contract items | 66 | +| Upstream OpenTelemetry .NET auto-instrumentation items | 60 | +| qyl-native items | 6 | +| Signal promises | 43 | | Global environment controls | 7 | | Instrumentation options | 16 | @@ -21,9 +23,9 @@ Every row carries a clickable upstream source anchored to the official OpenTelem | Status | Count | |---|---:| | `control_bound` | 7 | -| `implemented` | 33 | +| `implemented` | 40 | | `option_bound` | 12 | -| `unsupported_nativeaot` | 8 | +| `unsupported_nativeaot` | 7 | ## Lane counts @@ -32,82 +34,102 @@ Every row carries a clickable upstream source anchored to the official OpenTelem | `environment_control` | 7 | | `framework_initialization` | 1 | | `instrumentation_option` | 12 | +| `official_library_hook` | 7 | | `runtime_public_telemetry` | 8 | | `source_interceptor` | 24 | -| `unsupported_nativeaot` | 8 | +| `unsupported_nativeaot` | 7 | ## Evidence counts | Evidence level | Count | |---|---:| -| `none` | 8 | -| `verified_managed` | 17 | -| `verified_nativeaot` | 35 | +| `none` | 7 | +| `verified_managed` | 23 | +| `verified_nativeaot` | 36 | ## Matrix -| # | Key | Upstream source | Lane | qyl status | Call-site visibility | Payload access | Evidence | Owner | -|---:|---|---|---|---|---|---|---|---| -| 1 | `signals.traces.ADONET` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L144-L180) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 2 | `signals.traces.ASPNET` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L145-L184) | `unsupported_nativeaot` | `unsupported_nativeaot` | `user_code` | `reflection_required` | `none` | unsupported by qyl NativeAOT substrate | -| 3 | `signals.traces.ASPNETCORE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L146) | `runtime_public_telemetry` | `implemented` | `both` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | -| 4 | `signals.traces.AZURE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L147-L186) | `framework_initialization` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator framework initialization | -| 5 | `signals.traces.ELASTICSEARCH` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L148-L189) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 6 | `signals.traces.ELASTICTRANSPORT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L149) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 7 | `signals.traces.ENTITYFRAMEWORKCORE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L150-L195) | `runtime_public_telemetry` | `implemented` | `both` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | -| 8 | `signals.traces.GRAPHQL` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L151) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 9 | `signals.traces.GRPCNETCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L152) | `runtime_public_telemetry` | `implemented` | `both` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | -| 10 | `signals.traces.HTTPCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L153) | `runtime_public_telemetry` | `implemented` | `both` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | -| 11 | `signals.traces.KAFKA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L154-L198) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | -| 12 | `signals.traces.MASSTRANSIT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L155) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 13 | `signals.traces.MONGODB` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L156-L201) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | -| 14 | `signals.traces.MYSQLCONNECTOR` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L157) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 15 | `signals.traces.MYSQLDATA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L158) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 16 | `signals.traces.NPGSQL` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L159-L195) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 17 | `signals.traces.NSERVICEBUS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L160) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | -| 18 | `signals.traces.ORACLEMDA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L161) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 19 | `signals.traces.RABBITMQ` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L162-L204) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 20 | `signals.traces.QUARTZ` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L163) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | -| 21 | `signals.traces.SQLCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L164-L210) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 22 | `signals.traces.SQLITE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L165) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 23 | `signals.traces.STACKEXCHANGEREDIS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L166-L213) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 24 | `signals.traces.WCFCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L167) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | -| 25 | `signals.traces.WCFCORE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L168) | `unsupported_nativeaot` | `unsupported_nativeaot` | `library_internal` | `reflection_required` | `none` | unsupported by qyl NativeAOT substrate | -| 26 | `signals.traces.WCFSERVICE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L169) | `unsupported_nativeaot` | `unsupported_nativeaot` | `library_internal` | `reflection_required` | `none` | unsupported by qyl NativeAOT substrate | -| 27 | `signals.metrics.ASPNET` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L223) | `unsupported_nativeaot` | `unsupported_nativeaot` | `user_code` | `reflection_required` | `none` | unsupported by qyl NativeAOT substrate | -| 28 | `signals.metrics.ASPNETCORE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L224) | `runtime_public_telemetry` | `implemented` | `library_internal` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | -| 29 | `signals.metrics.HTTPCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L225) | `runtime_public_telemetry` | `implemented` | `both` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | -| 30 | `signals.metrics.NETRUNTIME` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L226) | `runtime_public_telemetry` | `implemented` | `library_internal` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | -| 31 | `signals.metrics.NPGSQL` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L227) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator + QylDbClientMetrics | -| 32 | `signals.metrics.NSERVICEBUS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L228) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator + QylNServiceBusMetrics | -| 33 | `signals.metrics.PROCESS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L229) | `runtime_public_telemetry` | `implemented` | `library_internal` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | -| 34 | `signals.metrics.SQLCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L230-L236) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator + QylDbClientMetrics | -| 35 | `signals.logs.ILOGGER` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L244-L251) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 36 | `signals.logs.LOG4NET` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L245-L254) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | -| 37 | `signals.logs.NLOG` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L246-L257) | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | -| 38 | `global_environment_controls.OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L128) | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 39 | `global_environment_controls.OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L129) | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 40 | `global_environment_controls.OTEL_DOTNET_AUTO_TRACES_{0}_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L120-L130) | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 41 | `global_environment_controls.OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L131) | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 42 | `global_environment_controls.OTEL_DOTNET_AUTO_METRICS_{0}_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L120-L132) | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 43 | `global_environment_controls.OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L133) | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 44 | `global_environment_controls.OTEL_DOTNET_AUTO_LOGS_{0}_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L120-L134) | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 45 | `instrumentation_options.OTEL_DOTNET_AUTO_ENTITYFRAMEWORKCORE_SET_DBSTATEMENT_FOR_TEXT` | [60-item source row](https://github.com/open-telemetry/opentelemetry.io/blob/db8337edbbac824aebbb330acea18a7042b38806/content/en/docs/zero-code/dotnet/instrumentations.md#L156)
[current removal note](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/CHANGELOG.md#L414-L418) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | -| 46 | `instrumentation_options.OTEL_DOTNET_AUTO_GRAPHQL_SET_DOCUMENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L263) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | -| 47 | `instrumentation_options.OTEL_DOTNET_AUTO_ORACLEMDA_SET_DBSTATEMENT_FOR_TEXT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L264) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | -| 48 | `instrumentation_options.OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT` | [60-item source row](https://github.com/open-telemetry/opentelemetry.io/blob/db8337edbbac824aebbb330acea18a7042b38806/content/en/docs/zero-code/dotnet/instrumentations.md#L159)
[current removal note](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/CHANGELOG.md#L416-L418) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | -| 49 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L265) | `unsupported_nativeaot` | `unsupported_nativeaot` | `not_applicable` | `not_applicable` | `none` | not_applicable | -| 50 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L266) | `unsupported_nativeaot` | `unsupported_nativeaot` | `not_applicable` | `not_applicable` | `none` | not_applicable | -| 51 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L267) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | -| 52 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L268) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | -| 53 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_REQUEST_METADATA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L269) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | -| 54 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_RESPONSE_METADATA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L270) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | -| 55 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L271) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 56 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L272) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 57 | `instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L273) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | -| 58 | `instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L274) | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | -| 59 | `instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_ASPNET_DISABLE_URL_QUERY_REDACTION` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L275) | `unsupported_nativeaot` | `unsupported_nativeaot` | `not_applicable` | `not_applicable` | `none` | not_applicable | -| 60 | `instrumentation_options.OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L276-L284) | `unsupported_nativeaot` | `unsupported_nativeaot` | `not_applicable` | `not_applicable` | `none` | not_applicable | +| # | Origin | Key | Authoritative source | Supported path/version | Lane | qyl status | Call-site visibility | Payload access | Evidence | Owner | +|---:|---|---|---|---|---|---|---|---|---|---| +| 1 | upstream 60 | `signals.traces.ADONET` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L144-L180) | System.Data.Common >=4.0.0 && <12.0.0; System.Data for .NET Framework >=2.0.0 && <5.0.0; netstandard >=2.0.0 && <3.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 2 | upstream 60 | `signals.traces.ASPNET` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L145-L184) | * | `unsupported_nativeaot` | `unsupported_nativeaot` | `user_code` | `reflection_required` | `none` | unsupported by qyl NativeAOT substrate | +| 3 | upstream 60 | `signals.traces.ASPNETCORE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L146) | * | `runtime_public_telemetry` | `implemented` | `both` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | +| 4 | upstream 60 | `signals.traces.AZURE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L147-L186) | Azure.* prefixed packages released after 2021-10-01 | `framework_initialization` | `implemented` | `library_internal` | `not_applicable` | `verified_nativeaot` | Azure SDK first-party Azure.* ActivitySources enabled by QylInstrumentation/Qyl.Sdk with Qyl enrichment | +| 5 | upstream 60 | `signals.traces.ELASTICSEARCH` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L148-L189) | >=8.0.0 && <8.10.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 6 | upstream 60 | `signals.traces.ELASTICTRANSPORT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L149) | >=0.4.16 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 7 | upstream 60 | `signals.traces.ENTITYFRAMEWORKCORE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L150-L195) | >=6.0.12 | `runtime_public_telemetry` | `implemented` | `library_internal` | `typed_public` | `verified_nativeaot` | EntityFrameworkCore specialist package EntityFrameworkCoreDiagnosticListener | +| 8 | upstream 60 | `signals.traces.GRAPHQL` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L151) | >=7.5.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 9 | upstream 60 | `signals.traces.GRPCNETCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L152) | >=2.52.0 && <3.0.0 | `runtime_public_telemetry` | `implemented` | `both` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | +| 10 | upstream 60 | `signals.traces.HTTPCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L153) | * | `runtime_public_telemetry` | `implemented` | `both` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | +| 11 | upstream 60 | `signals.traces.KAFKA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L154-L198) | >=1.4.0 && <3.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | +| 12 | upstream 60 | `signals.traces.MASSTRANSIT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L155) | >=8.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 13 | upstream 60 | `signals.traces.MONGODB` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L156-L201) | >=2.7.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | +| 14 | upstream 60 | `signals.traces.MYSQLCONNECTOR` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L157) | >=2.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 15 | upstream 60 | `signals.traces.MYSQLDATA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L158) | >=8.1.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 16 | upstream 60 | `signals.traces.NPGSQL` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L159-L195) | >=6.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 17 | upstream 60 | `signals.traces.NSERVICEBUS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L160) | >=8.0.0 && <10.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | +| 18 | upstream 60 | `signals.traces.ORACLEMDA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L161) | >=23.4.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 19 | upstream 60 | `signals.traces.RABBITMQ` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L162-L204) | >=5.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 20 | upstream 60 | `signals.traces.QUARTZ` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L163) | >=3.4.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | +| 21 | upstream 60 | `signals.traces.SQLCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L164-L210) | * | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator; dependency-specific SqlClient listener fallback when no interceptor is emitted | +| 22 | upstream 60 | `signals.traces.SQLITE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L165) | >=8.0.0 && <12.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 23 | upstream 60 | `signals.traces.STACKEXCHANGEREDIS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L166-L213) | >=2.6.122 && <4.0.0 on .NET; >=2.6.122 && <3.0.0 on .NET Framework | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 24 | upstream 60 | `signals.traces.WCFCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L167) | * | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | +| 25 | upstream 60 | `signals.traces.WCFCORE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L168) | CoreWCF.Primitives ==1.9.1; CoreWCF.Http ==1.9.1; server ActivitySource path only | `official_library_hook` | `implemented` | `library_internal` | `typed_public` | `verified_managed` | Qyl.Sdk CoreWCF.Primitives ActivitySource registration | +| 26 | upstream 60 | `signals.traces.WCFSERVICE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L169) | * | `unsupported_nativeaot` | `unsupported_nativeaot` | `library_internal` | `reflection_required` | `none` | unsupported by qyl NativeAOT substrate | +| 27 | upstream 60 | `signals.metrics.ASPNET` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L223) | * | `unsupported_nativeaot` | `unsupported_nativeaot` | `user_code` | `reflection_required` | `none` | unsupported by qyl NativeAOT substrate | +| 28 | upstream 60 | `signals.metrics.ASPNETCORE` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L224) | * | `runtime_public_telemetry` | `implemented` | `library_internal` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | +| 29 | upstream 60 | `signals.metrics.HTTPCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L225) | * | `runtime_public_telemetry` | `implemented` | `both` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | +| 30 | upstream 60 | `signals.metrics.NETRUNTIME` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L226) | * | `runtime_public_telemetry` | `implemented` | `library_internal` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | +| 31 | upstream 60 | `signals.metrics.NPGSQL` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L227) | >=6.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator + QylDbClientMetrics | +| 32 | upstream 60 | `signals.metrics.NSERVICEBUS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L228) | >=8.0.0 && <10.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator + QylNServiceBusMetrics | +| 33 | upstream 60 | `signals.metrics.PROCESS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L229) | * | `runtime_public_telemetry` | `implemented` | `library_internal` | `typed_public` | `verified_nativeaot` | Qyl public telemetry listeners/meters | +| 34 | upstream 60 | `signals.metrics.SQLCLIENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L230-L236) | * | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator + QylDbClientMetrics | +| 35 | upstream 60 | `signals.logs.ILOGGER` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L244-L251) | >=8.0.0 && <12.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 36 | upstream 60 | `signals.logs.LOG4NET` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L245-L254) | >=2.0.13 && <4.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_managed` | QylAutoInstrumentationGenerator | +| 37 | upstream 60 | `signals.logs.NLOG` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L246-L257) | >=5.0.0 && <7.0.0 | `source_interceptor` | `implemented` | `user_code` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationGenerator | +| 38 | upstream 60 | `global_environment_controls.OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L128) | n/a | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 39 | upstream 60 | `global_environment_controls.OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L129) | n/a | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 40 | upstream 60 | `global_environment_controls.OTEL_DOTNET_AUTO_TRACES_{0}_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L120-L130) | n/a | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 41 | upstream 60 | `global_environment_controls.OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L131) | n/a | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 42 | upstream 60 | `global_environment_controls.OTEL_DOTNET_AUTO_METRICS_{0}_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L120-L132) | n/a | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 43 | upstream 60 | `global_environment_controls.OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L133) | n/a | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 44 | upstream 60 | `global_environment_controls.OTEL_DOTNET_AUTO_LOGS_{0}_INSTRUMENTATION_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L120-L134) | n/a | `environment_control` | `control_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 45 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_ENTITYFRAMEWORKCORE_SET_DBSTATEMENT_FOR_TEXT` | [60-item source row](https://github.com/open-telemetry/opentelemetry.io/blob/db8337edbbac824aebbb330acea18a7042b38806/content/en/docs/zero-code/dotnet/instrumentations.md#L156)
[current removal note](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/CHANGELOG.md#L414-L418) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | +| 46 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_GRAPHQL_SET_DOCUMENT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L263) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | +| 47 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_ORACLEMDA_SET_DBSTATEMENT_FOR_TEXT` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L264) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | +| 48 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT` | [60-item source row](https://github.com/open-telemetry/opentelemetry.io/blob/db8337edbbac824aebbb330acea18a7042b38806/content/en/docs/zero-code/dotnet/instrumentations.md#L159)
[current removal note](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/CHANGELOG.md#L416-L418) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | +| 49 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L265) | n/a | `unsupported_nativeaot` | `unsupported_nativeaot` | `not_applicable` | `not_applicable` | `none` | not_applicable | +| 50 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L266) | n/a | `unsupported_nativeaot` | `unsupported_nativeaot` | `not_applicable` | `not_applicable` | `none` | not_applicable | +| 51 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L267) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | +| 52 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L268) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | +| 53 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_REQUEST_METADATA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L269) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | +| 54 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_GRPCNETCLIENT_INSTRUMENTATION_CAPTURE_RESPONSE_METADATA` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L270) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | +| 55 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L271) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 56 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L272) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 57 | upstream 60 | `instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L273) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_nativeaot` | QylAutoInstrumentationOptions | +| 58 | upstream 60 | `instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_HTTPCLIENT_DISABLE_URL_QUERY_REDACTION` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L274) | n/a | `instrumentation_option` | `option_bound` | `not_applicable` | `not_applicable` | `verified_managed` | QylAutoInstrumentationOptions | +| 59 | upstream 60 | `instrumentation_options.OTEL_DOTNET_EXPERIMENTAL_ASPNET_DISABLE_URL_QUERY_REDACTION` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L275) | n/a | `unsupported_nativeaot` | `unsupported_nativeaot` | `not_applicable` | `not_applicable` | `none` | not_applicable | +| 60 | upstream 60 | `instrumentation_options.OTEL_DOTNET_AUTO_SQLCLIENT_NETFX_ILREWRITE_ENABLED` | [current config](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L276-L284) | n/a | `unsupported_nativeaot` | `unsupported_nativeaot` | `not_applicable` | `not_applicable` | `none` | not_applicable | +| 61 | qyl native | `signals.traces.MICROSOFTEXTENSIONSAI` | [Microsoft.Extensions.AI 10.8.0](https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0)
[official UseOpenTelemetry documentation](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai) | Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only | `official_library_hook` | `implemented` | `user_code` | `typed_public` | `verified_managed` | Qyl.Sdk source registration plus Microsoft.Extensions.AI UseOpenTelemetry | +| 62 | qyl native | `signals.metrics.MICROSOFTEXTENSIONSAI` | [Microsoft.Extensions.AI 10.8.0](https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0)
[official UseOpenTelemetry documentation](https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai) | Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only | `official_library_hook` | `implemented` | `user_code` | `typed_public` | `verified_managed` | Qyl.Sdk meter registration plus Microsoft.Extensions.AI UseOpenTelemetry | +| 63 | qyl native | `signals.traces.MICROSOFTAGENTSAI` | [Microsoft.Agents.AI 1.13.0](https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0)
[official UseOpenTelemetry API](https://learn.microsoft.com/dotnet/api/microsoft.agents.ai.opentelemetryagentbuilderextensions.useopentelemetry) | Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only | `official_library_hook` | `implemented` | `user_code` | `typed_public` | `verified_managed` | Qyl.Sdk source registration plus Microsoft.Agents.AI UseOpenTelemetry | +| 64 | qyl native | `signals.metrics.MICROSOFTAGENTSAI` | [Microsoft.Agents.AI 1.13.0](https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0)
[official UseOpenTelemetry API](https://learn.microsoft.com/dotnet/api/microsoft.agents.ai.opentelemetryagentbuilderextensions.useopentelemetry) | Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only | `official_library_hook` | `implemented` | `user_code` | `typed_public` | `verified_managed` | Qyl.Sdk meter registration plus Microsoft.Agents.AI UseOpenTelemetry | +| 65 | qyl native | `signals.traces.MICROSOFTAGENTSAIWORKFLOWS` | [Microsoft.Agents.AI.Workflows 1.13.0](https://www.nuget.org/packages/Microsoft.Agents.AI.Workflows/1.13.0)
[official Agent Framework repository](https://github.com/microsoft/agent-framework) | Microsoft.Agents.AI.Workflows ==1.13.0; WorkflowBuilder.WithOpenTelemetry() path only | `official_library_hook` | `implemented` | `user_code` | `typed_public` | `verified_managed` | Qyl.Sdk source registration plus Microsoft.Agents.AI.Workflows WithOpenTelemetry | +| 66 | qyl native | `signals.traces.MCP` | [ModelContextProtocol 1.4.1](https://www.nuget.org/packages/ModelContextProtocol/1.4.1)
[official MCP C# SDK](https://github.com/modelcontextprotocol/csharp-sdk) | ModelContextProtocol ==1.4.1; automatic official client/server ActivitySource path only | `official_library_hook` | `implemented` | `library_internal` | `typed_public` | `verified_nativeaot` | Qyl.Sdk source registration plus automatic ModelContextProtocol telemetry | + +## qyl-native 8.0 scope boundaries + +These are exact library-hook promises. Evidence level is explicit per row; none imply provider-wide coverage. + +| Key | Exact supported path/version | Boundary notes | +|---|---|---| +| `signals.traces.MICROSOFTEXTENSIONSAI` | Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only | Qyl.Sdk registers Experimental.Microsoft.Extensions.AI; the application explicitly installs the official UseOpenTelemetry wrapper.
This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. | +| `signals.metrics.MICROSOFTEXTENSIONSAI` | Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only | Qyl.Sdk registers Experimental.Microsoft.Extensions.AI; the application explicitly installs the official UseOpenTelemetry wrapper.
This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. | +| `signals.traces.MICROSOFTAGENTSAI` | Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only | Qyl.Sdk registers Experimental.Microsoft.Agents.AI; the application explicitly installs the official UseOpenTelemetry wrapper.
This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. | +| `signals.metrics.MICROSOFTAGENTSAI` | Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only | Qyl.Sdk registers Experimental.Microsoft.Agents.AI; the application explicitly installs the official UseOpenTelemetry wrapper.
This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. | +| `signals.traces.MICROSOFTAGENTSAIWORKFLOWS` | Microsoft.Agents.AI.Workflows ==1.13.0; WorkflowBuilder.WithOpenTelemetry() path only | Qyl.Sdk registers Microsoft.Agents.AI.Workflows; the application explicitly calls WithOpenTelemetry.
The 8.0.0 promise is traces only because this exact workflow hook exposes an ActivitySource, not a corresponding meter.
This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. | +| `signals.traces.MCP` | ModelContextProtocol ==1.4.1; automatic official client/server ActivitySource path only | Qyl.Sdk registers Experimental.ModelContextProtocol; the official SDK emits the client/server spans automatically.
MCP metrics are deliberately outside the 8.0.0 contract because the official instruments attach dynamic tool and resource names as dimensions, violating qyl's bounded-cardinality policy.
This exact path and version is verified in managed and NativeAOT execution; it is not a protocol-wide claim. | ## Unsupported NativeAOT reasons @@ -117,7 +139,6 @@ table only by gaining an executable owner or by upstream removing the promise. | Key | Reason | |---|---| | `signals.traces.ASPNET` | Classic ASP.NET (System.Web MVC/WebApi) executes only on .NET Framework 4.x inside IIS; System.Web does not exist on .NET 10, so no in-process mechanism is available to this managed substrate. Devil's-advocate check (2026-07-18): Microsoft.AspNetCore.SystemWebAdapters covers apps mid-migration to ASP.NET Core, not the literal promise; rejected as a rescue. | -| `signals.traces.WCFCORE` | Not yet implemented rather than impossible: CoreWCF >=1.8 natively emits server-side spans through its own ActivitySource ('CoreWCF.Primitives'), so registration-lane coverage is feasible (managed); planned on the qyl roadmap. Unsupported today because no executable owner exists yet. | | `signals.traces.WCFSERVICE` | System.ServiceModel server-side hosting (ServiceHost/ChannelDispatcher) does not exist on modern .NET; the promise as written is dead on .NET 10. The successor surface is CoreWCF (see signals.traces.WCFCORE). | | `signals.metrics.ASPNET` | Upstream emits these metrics from the System.Web TelemetryHttpModule pipeline (.NET Framework-only) and gates them on the AspNet trace instrumentation, which is itself impossible here (see signals.traces.ASPNET). | | `instrumentation_options.OTEL_DOTNET_AUTO_TRACES_ASPNET_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS` | Parameterizes the classic System.Web ASPNET trace integration, which is impossible on this substrate (see signals.traces.ASPNET); an option to a nonexistent integration cannot be honored. | diff --git a/docs/generated/qyl-aot-contract.resolved.yaml b/docs/generated/qyl-aot-contract.resolved.yaml index 5c56d9b..861dffb 100644 --- a/docs/generated/qyl-aot-contract.resolved.yaml +++ b/docs/generated/qyl-aot-contract.resolved.yaml @@ -1,22 +1,29 @@ # # Regenerate with tools/generate-contract-artifacts.py --write. schema_id: qyl-aot-autoinstrumentation-resolved-contract -schema_version: 1.0.0 -generated_at: '2026-06-13' -source: - title: Available instrumentations - source_kind: official_github_markdown - content_basis: OpenTelemetry .NET Automatic Instrumentation supported libraries - primary_repository: open-telemetry/opentelemetry-dotnet-instrumentation - primary_commit: 611b815a62a66b5ece634337328757444fb9c2e9 - primary_path: docs/config.md - primary_url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md - legacy_60_item_repository: open-telemetry/opentelemetry.io - legacy_60_item_commit: db8337edbbac824aebbb330acea18a7042b38806 - legacy_60_item_path: content/en/docs/zero-code/dotnet/instrumentations.md - legacy_60_item_url: https://github.com/open-telemetry/opentelemetry.io/blob/db8337edbbac824aebbb330acea18a7042b38806/content/en/docs/zero-code/dotnet/instrumentations.md +schema_version: 2.0.0 +generated_at: '2026-07-19' +sources: + upstream_otel_dotnet_auto_60: + title: Available instrumentations + source_kind: official_github_markdown + content_basis: OpenTelemetry .NET Automatic Instrumentation supported libraries + primary_repository: open-telemetry/opentelemetry-dotnet-instrumentation + primary_commit: 611b815a62a66b5ece634337328757444fb9c2e9 + primary_path: docs/config.md + primary_url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md + legacy_60_item_repository: open-telemetry/opentelemetry.io + legacy_60_item_commit: db8337edbbac824aebbb330acea18a7042b38806 + legacy_60_item_path: content/en/docs/zero-code/dotnet/instrumentations.md + legacy_60_item_url: https://github.com/open-telemetry/opentelemetry.io/blob/db8337edbbac824aebbb330acea18a7042b38806/content/en/docs/zero-code/dotnet/instrumentations.md + qyl_native: + title: qyl-native version-pinned instrumentation promises + source_kind: qyl_owned_contract_with_official_library_sources + content_basis: First-class telemetry hooks registered by Qyl.Sdk 8.0.0 and exercised by owned real-library demos + release: 8.0.0 generated_from: upstream_contract: docs/contracts/otel-dotnet-auto-60.upstream.yaml + qyl_native_contract: docs/contracts/qyl-native-instrumentations.yaml qyl_ownership: docs/contracts/qyl-aot-ownership.yaml contract_items: - kind: signal_specific_instrumentation_promise @@ -43,6 +50,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L144-L180 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -90,6 +98,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L145-L184 + contract_origin: upstream_otel_dotnet_auto_60 lane: unsupported_nativeaot qyl_status: unsupported_nativeaot call_site_visibility: user_code @@ -122,6 +131,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L146 + contract_origin: upstream_otel_dotnet_auto_60 lane: runtime_public_telemetry qyl_status: implemented call_site_visibility: both @@ -162,15 +172,18 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L147-L186 + contract_origin: upstream_otel_dotnet_auto_60 lane: framework_initialization qyl_status: implemented - call_site_visibility: user_code + call_site_visibility: library_internal payload_access: not_applicable evidence_level: verified_nativeaot - primary_owner: QylAutoInstrumentationGenerator framework initialization + primary_owner: Azure SDK first-party Azure.* ActivitySources enabled by QylInstrumentation/Qyl.Sdk with Qyl enrichment evidence: - - src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs - - tools/verify-source-interceptor-consumer.py + - src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentation.cs + - src/Qyl.Sdk/QylTelemetrySources.cs + - src/Qyl.Sdk/QylAzureSpanProcessor.cs + - src/Qyl.Sdk/QylSdkHostApplicationBuilderExtensions.cs - demos/Qyl.RealAzureDemo - tools/verify-real-azure-demo.py conformance_signals: @@ -202,6 +215,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L148-L189 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -243,6 +257,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L149 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -286,15 +301,15 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L150-L195 + contract_origin: upstream_otel_dotnet_auto_60 lane: runtime_public_telemetry qyl_status: implemented - call_site_visibility: both + call_site_visibility: library_internal payload_access: typed_public evidence_level: verified_nativeaot - primary_owner: Qyl public telemetry listeners/meters + primary_owner: EntityFrameworkCore specialist package EntityFrameworkCoreDiagnosticListener evidence: - - src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners - - src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricMeters.cs + - src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs - demos/Qyl.RealEfCoreDemo - tools/verify-real-efcore-demo.py conformance_signals: @@ -329,6 +344,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L151 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -369,6 +385,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L152 + contract_origin: upstream_otel_dotnet_auto_60 lane: runtime_public_telemetry qyl_status: implemented call_site_visibility: both @@ -414,6 +431,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L153 + contract_origin: upstream_otel_dotnet_auto_60 lane: runtime_public_telemetry qyl_status: implemented call_site_visibility: both @@ -466,6 +484,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L154-L198 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -498,6 +517,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L155 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -546,6 +566,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L156-L201 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -576,6 +597,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L157 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -620,6 +642,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L158 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -662,6 +685,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L159-L195 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -705,6 +729,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L160 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -741,6 +766,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L161 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -786,6 +812,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L162-L204 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -829,6 +856,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L163 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -870,14 +898,17 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L164-L210 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code payload_access: not_applicable evidence_level: verified_nativeaot - primary_owner: QylAutoInstrumentationGenerator + primary_owner: QylAutoInstrumentationGenerator; dependency-specific SqlClient listener fallback when no interceptor is emitted evidence: - src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs + - src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedDbCommand.cs + - src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientDiagnosticListener.cs - tools/verify-source-interceptor-consumer.py - demos/Qyl.RealSqlClientDemo - tools/verify-real-sqlclient-demo.py @@ -912,6 +943,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L165 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -955,6 +987,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L166-L213 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -997,6 +1030,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L167 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -1017,7 +1051,7 @@ contract_items: libraries: - name: CoreWCF.Primitives url: https://www.nuget.org/packages/CoreWCF.Primitives - supported_versions: '>=1.8.0' + supported_versions: CoreWCF.Primitives ==1.9.1; CoreWCF.Http ==1.9.1; server ActivitySource path only instrumentation_types: - source instrumentation_type_operator: single @@ -1029,17 +1063,17 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L168 - lane: unsupported_nativeaot - qyl_status: unsupported_nativeaot + contract_origin: upstream_otel_dotnet_auto_60 + lane: official_library_hook + qyl_status: implemented call_site_visibility: library_internal - payload_access: reflection_required - evidence_level: none - primary_owner: unsupported by qyl NativeAOT substrate + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk CoreWCF.Primitives ActivitySource registration evidence: - - docs/coverage-matrix.md - unsupported_reason: 'Not yet implemented rather than impossible: CoreWCF >=1.8 natively emits server-side spans through - its own ActivitySource (''CoreWCF.Primitives''), so registration-lane coverage is feasible (managed); planned on the qyl - roadmap. Unsupported today because no executable owner exists yet.' + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealCoreWcfDemo + - tools/verify-real-corewcf-demo.py - kind: signal_specific_instrumentation_promise key: signals.traces.WCFSERVICE signal: traces @@ -1061,6 +1095,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L169 + contract_origin: upstream_otel_dotnet_auto_60 lane: unsupported_nativeaot qyl_status: unsupported_nativeaot call_site_visibility: library_internal @@ -1097,6 +1132,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L223 + contract_origin: upstream_otel_dotnet_auto_60 lane: unsupported_nativeaot qyl_status: unsupported_nativeaot call_site_visibility: user_code @@ -1130,6 +1166,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L224 + contract_origin: upstream_otel_dotnet_auto_60 lane: runtime_public_telemetry qyl_status: implemented call_site_visibility: library_internal @@ -1183,6 +1220,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L225 + contract_origin: upstream_otel_dotnet_auto_60 lane: runtime_public_telemetry qyl_status: implemented call_site_visibility: both @@ -1224,6 +1262,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L226 + contract_origin: upstream_otel_dotnet_auto_60 lane: runtime_public_telemetry qyl_status: implemented call_site_visibility: library_internal @@ -1275,6 +1314,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L227 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -1319,6 +1359,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L228 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -1353,6 +1394,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L229 + contract_origin: upstream_otel_dotnet_auto_60 lane: runtime_public_telemetry qyl_status: implemented call_site_visibility: library_internal @@ -1413,6 +1455,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L230-L236 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -1421,6 +1464,7 @@ contract_items: primary_owner: QylAutoInstrumentationGenerator + QylDbClientMetrics evidence: - src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs + - src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedDbCommand.cs - src/Qyl.OpenTelemetry.AutoInstrumentation/QylDbClientMetrics.cs - tools/verify-source-interceptor-consumer.py - demos/Qyl.RealSqlClientDemo @@ -1457,6 +1501,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L244-L251 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -1498,6 +1543,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L245-L254 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -1530,6 +1576,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L246-L257 + contract_origin: upstream_otel_dotnet_auto_60 lane: source_interceptor qyl_status: implemented call_site_visibility: user_code @@ -1561,6 +1608,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L128 + contract_origin: upstream_otel_dotnet_auto_60 lane: environment_control qyl_status: control_bound call_site_visibility: not_applicable @@ -1587,6 +1635,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L129 + contract_origin: upstream_otel_dotnet_auto_60 lane: environment_control qyl_status: control_bound call_site_visibility: not_applicable @@ -1618,6 +1667,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L120-L130 + contract_origin: upstream_otel_dotnet_auto_60 lane: environment_control qyl_status: control_bound call_site_visibility: not_applicable @@ -1644,6 +1694,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L131 + contract_origin: upstream_otel_dotnet_auto_60 lane: environment_control qyl_status: control_bound call_site_visibility: not_applicable @@ -1675,6 +1726,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L120-L132 + contract_origin: upstream_otel_dotnet_auto_60 lane: environment_control qyl_status: control_bound call_site_visibility: not_applicable @@ -1701,6 +1753,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L133 + contract_origin: upstream_otel_dotnet_auto_60 lane: environment_control qyl_status: control_bound call_site_visibility: not_applicable @@ -1732,6 +1785,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L120-L134 + contract_origin: upstream_otel_dotnet_auto_60 lane: environment_control qyl_status: control_bound call_site_visibility: not_applicable @@ -1765,6 +1819,7 @@ contract_items: url: https://github.com/open-telemetry/opentelemetry.io/blob/db8337edbbac824aebbb330acea18a7042b38806/content/en/docs/zero-code/dotnet/instrumentations.md#L156 - label: current removal note url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/CHANGELOG.md#L414-L418 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -1795,6 +1850,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L263 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -1825,6 +1881,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L264 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -1861,6 +1918,7 @@ contract_items: url: https://github.com/open-telemetry/opentelemetry.io/blob/db8337edbbac824aebbb330acea18a7042b38806/content/en/docs/zero-code/dotnet/instrumentations.md#L159 - label: current removal note url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/CHANGELOG.md#L416-L418 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -1889,6 +1947,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L265 + contract_origin: upstream_otel_dotnet_auto_60 lane: unsupported_nativeaot qyl_status: unsupported_nativeaot call_site_visibility: not_applicable @@ -1917,6 +1976,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L266 + contract_origin: upstream_otel_dotnet_auto_60 lane: unsupported_nativeaot qyl_status: unsupported_nativeaot call_site_visibility: not_applicable @@ -1943,6 +2003,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L267 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -1971,6 +2032,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L268 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -1999,6 +2061,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L269 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -2027,6 +2090,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L270 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -2055,6 +2119,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L271 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -2082,6 +2147,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L272 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -2108,6 +2174,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L273 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -2135,6 +2202,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L274 + contract_origin: upstream_otel_dotnet_auto_60 lane: instrumentation_option qyl_status: option_bound call_site_visibility: not_applicable @@ -2161,6 +2229,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L275 + contract_origin: upstream_otel_dotnet_auto_60 lane: unsupported_nativeaot qyl_status: unsupported_nativeaot call_site_visibility: not_applicable @@ -2193,6 +2262,7 @@ contract_items: upstream_sources: - label: current config url: https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/blob/611b815a62a66b5ece634337328757444fb9c2e9/docs/config.md#L276-L284 + contract_origin: upstream_otel_dotnet_auto_60 lane: unsupported_nativeaot qyl_status: unsupported_nativeaot call_site_visibility: not_applicable @@ -2203,3 +2273,260 @@ contract_items: unsupported_reason: 'The option''s entire semantic is CLR-profiler IL rewriting of System.Data.SqlClient on .NET Framework; runtime IL rewriting is excluded from this package family by definition. Devil''s-advocate check (2026-07-18): no substrate-legal mechanism honors the option''s meaning.' +- kind: signal_specific_instrumentation_promise + key: signals.traces.MICROSOFTEXTENSIONSAI + signal: traces + instrumentation_id: MICROSOFTEXTENSIONSAI + environment_toggle: OTEL_DOTNET_AUTO_TRACES_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Extensions.AI + url: https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0 + supported_versions: Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.Microsoft.Extensions.AI; the application explicitly installs the official UseOpenTelemetry + wrapper. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 61 + contract_item_id: QYL_NATIVE_CONTRACT_001 + upstream_sources: + - label: Microsoft.Extensions.AI 10.8.0 + url: https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0 + - label: official UseOpenTelemetry documentation + url: https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk source registration plus Microsoft.Extensions.AI UseOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py + contract_origin: qyl_native +- kind: signal_specific_instrumentation_promise + key: signals.metrics.MICROSOFTEXTENSIONSAI + signal: metrics + instrumentation_id: MICROSOFTEXTENSIONSAI + environment_toggle: OTEL_DOTNET_AUTO_METRICS_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Extensions.AI + url: https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0 + supported_versions: Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.Microsoft.Extensions.AI; the application explicitly installs the official UseOpenTelemetry + wrapper. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 62 + contract_item_id: QYL_NATIVE_CONTRACT_002 + upstream_sources: + - label: Microsoft.Extensions.AI 10.8.0 + url: https://www.nuget.org/packages/Microsoft.Extensions.AI/10.8.0 + - label: official UseOpenTelemetry documentation + url: https://learn.microsoft.com/dotnet/ai/microsoft-extensions-ai + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk meter registration plus Microsoft.Extensions.AI UseOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py + contract_origin: qyl_native +- kind: signal_specific_instrumentation_promise + key: signals.traces.MICROSOFTAGENTSAI + signal: traces + instrumentation_id: MICROSOFTAGENTSAI + environment_toggle: OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Agents.AI + url: https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0 + supported_versions: Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.Microsoft.Agents.AI; the application explicitly installs the official UseOpenTelemetry + wrapper. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 63 + contract_item_id: QYL_NATIVE_CONTRACT_003 + upstream_sources: + - label: Microsoft.Agents.AI 1.13.0 + url: https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0 + - label: official UseOpenTelemetry API + url: https://learn.microsoft.com/dotnet/api/microsoft.agents.ai.opentelemetryagentbuilderextensions.useopentelemetry + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk source registration plus Microsoft.Agents.AI UseOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py + contract_origin: qyl_native +- kind: signal_specific_instrumentation_promise + key: signals.metrics.MICROSOFTAGENTSAI + signal: metrics + instrumentation_id: MICROSOFTAGENTSAI + environment_toggle: OTEL_DOTNET_AUTO_METRICS_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Agents.AI + url: https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0 + supported_versions: Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.Microsoft.Agents.AI; the application explicitly installs the official UseOpenTelemetry + wrapper. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 64 + contract_item_id: QYL_NATIVE_CONTRACT_004 + upstream_sources: + - label: Microsoft.Agents.AI 1.13.0 + url: https://www.nuget.org/packages/Microsoft.Agents.AI/1.13.0 + - label: official UseOpenTelemetry API + url: https://learn.microsoft.com/dotnet/api/microsoft.agents.ai.opentelemetryagentbuilderextensions.useopentelemetry + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk meter registration plus Microsoft.Agents.AI UseOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py + contract_origin: qyl_native +- kind: signal_specific_instrumentation_promise + key: signals.traces.MICROSOFTAGENTSAIWORKFLOWS + signal: traces + instrumentation_id: MICROSOFTAGENTSAIWORKFLOWS + environment_toggle: OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAIWORKFLOWS_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: Microsoft.Agents.AI.Workflows + url: https://www.nuget.org/packages/Microsoft.Agents.AI.Workflows/1.13.0 + supported_versions: Microsoft.Agents.AI.Workflows ==1.13.0; WorkflowBuilder.WithOpenTelemetry() path only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Microsoft.Agents.AI.Workflows; the application explicitly calls WithOpenTelemetry. + - The 8.0.0 promise is traces only because this exact workflow hook exposes an ActivitySource, not a corresponding meter. + - This is an exact managed path/version claim, not a provider-wide or NativeAOT claim. + index: 65 + contract_item_id: QYL_NATIVE_CONTRACT_005 + upstream_sources: + - label: Microsoft.Agents.AI.Workflows 1.13.0 + url: https://www.nuget.org/packages/Microsoft.Agents.AI.Workflows/1.13.0 + - label: official Agent Framework repository + url: https://github.com/microsoft/agent-framework + lane: official_library_hook + qyl_status: implemented + call_site_visibility: user_code + payload_access: typed_public + evidence_level: verified_managed + primary_owner: Qyl.Sdk source registration plus Microsoft.Agents.AI.Workflows WithOpenTelemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealGenAiDemo + - tools/verify-real-genai-demo.py + contract_origin: qyl_native +- kind: signal_specific_instrumentation_promise + key: signals.traces.MCP + signal: traces + instrumentation_id: MCP + environment_toggle: OTEL_DOTNET_AUTO_TRACES_MCP_INSTRUMENTATION_ENABLED + environment_toggle_origin: qyl_native_signal_specific_pattern + libraries: + - name: ModelContextProtocol + url: https://www.nuget.org/packages/ModelContextProtocol/1.4.1 + supported_versions: ModelContextProtocol ==1.4.1; automatic official client/server ActivitySource path only + instrumentation_types: + - source + instrumentation_type_operator: single + status: experimental + notes: + - Qyl.Sdk registers Experimental.ModelContextProtocol; the official SDK emits the client/server spans automatically. + - MCP metrics are deliberately outside the 8.0.0 contract because the official instruments attach dynamic tool and resource + names as dimensions, violating qyl's bounded-cardinality policy. + - This exact path and version is verified in managed and NativeAOT execution; it is not a protocol-wide claim. + index: 66 + contract_item_id: QYL_NATIVE_CONTRACT_006 + upstream_sources: + - label: ModelContextProtocol 1.4.1 + url: https://www.nuget.org/packages/ModelContextProtocol/1.4.1 + - label: official MCP C# SDK + url: https://github.com/modelcontextprotocol/csharp-sdk + lane: official_library_hook + qyl_status: implemented + call_site_visibility: library_internal + payload_access: typed_public + evidence_level: verified_nativeaot + primary_owner: Qyl.Sdk source registration plus automatic ModelContextProtocol telemetry + evidence: + - src/Qyl.Sdk/QylTelemetrySources.cs + - demos/Qyl.RealMcpDemo + - tools/verify-real-mcp-demo.py + conformance_signals: + - kind: span + name: mcp.initialize + required_attributes: + - mcp.method.name + - mcp.session.id + - network.transport + recommended_attributes: + - session.id + opt_in_attributes: [] + - kind: span + name: mcp.notifications.initialized + required_attributes: + - mcp.method.name + - mcp.session.id + - network.transport + recommended_attributes: + - session.id + opt_in_attributes: [] + - kind: span + name: mcp.tools.list + required_attributes: + - mcp.method.name + - mcp.session.id + - network.transport + recommended_attributes: + - session.id + opt_in_attributes: [] + - kind: span + name: mcp.tools.call + required_attributes: + - gen_ai.operation.name + - gen_ai.tool.name + - mcp.method.name + - mcp.protocol.version + - mcp.session.id + - network.transport + recommended_attributes: + - session.id + opt_in_attributes: [] + contract_origin: qyl_native diff --git a/docs/generated/qyl-aot-contract.schema.json b/docs/generated/qyl-aot-contract.schema.json index 332bbb0..35ec2af 100644 --- a/docs/generated/qyl-aot-contract.schema.json +++ b/docs/generated/qyl-aot-contract.schema.json @@ -8,7 +8,7 @@ "schema_id", "schema_version", "generated_at", - "source", + "sources", "generated_from", "contract_items" ], @@ -17,25 +17,42 @@ "const": "qyl-aot-autoinstrumentation-resolved-contract" }, "schema_version": { - "type": "string" + "const": "2.0.0" }, "generated_at": { "type": "string" }, - "source": { - "type": "object" + "sources": { + "type": "object", + "additionalProperties": false, + "required": [ + "upstream_otel_dotnet_auto_60", + "qyl_native" + ], + "properties": { + "upstream_otel_dotnet_auto_60": { + "type": "object" + }, + "qyl_native": { + "type": "object" + } + } }, "generated_from": { "type": "object", "additionalProperties": false, "required": [ "upstream_contract", + "qyl_native_contract", "qyl_ownership" ], "properties": { "upstream_contract": { "const": "docs/contracts/otel-dotnet-auto-60.upstream.yaml" }, + "qyl_native_contract": { + "const": "docs/contracts/qyl-native-instrumentations.yaml" + }, "qyl_ownership": { "const": "docs/contracts/qyl-aot-ownership.yaml" } @@ -43,8 +60,8 @@ }, "contract_items": { "type": "array", - "minItems": 60, - "maxItems": 60, + "minItems": 66, + "maxItems": 66, "items": { "$ref": "#/$defs/contract_item" } @@ -75,17 +92,23 @@ "key": { "type": "string" }, + "contract_origin": { + "enum": [ + "upstream_otel_dotnet_auto_60", + "qyl_native" + ] + }, "status": { "type": "string" }, "index": { "type": "integer", "minimum": 1, - "maximum": 60 + "maximum": 66 }, "contract_item_id": { "type": "string", - "pattern": "^OTEL_DOTNET_AUTO_CONTRACT_[0-9]{3}$" + "pattern": "^(?:OTEL_DOTNET_AUTO_CONTRACT|QYL_NATIVE_CONTRACT)_[0-9]{3}$" }, "upstream_sources": { "type": "array", @@ -164,6 +187,7 @@ "required": [ "kind", "key", + "contract_origin", "status", "index", "contract_item_id", @@ -200,7 +224,13 @@ }, "contract_item_id": { "type": "string", - "pattern": "^OTEL_DOTNET_AUTO_CONTRACT_[0-9]{3}$" + "pattern": "^(?:OTEL_DOTNET_AUTO_CONTRACT|QYL_NATIVE_CONTRACT)_[0-9]{3}$" + }, + "contract_origin": { + "enum": [ + "upstream_otel_dotnet_auto_60", + "qyl_native" + ] }, "documentation": { "type": "object" @@ -229,7 +259,7 @@ "index": { "type": "integer", "minimum": 1, - "maximum": 60 + "maximum": 66 }, "instrumentation_id": { "type": "string" @@ -380,6 +410,7 @@ "required": [ "kind", "key", + "contract_origin", "status", "index", "contract_item_id", @@ -412,7 +443,13 @@ }, "contract_item_id": { "type": "string", - "pattern": "^OTEL_DOTNET_AUTO_CONTRACT_[0-9]{3}$" + "pattern": "^(?:OTEL_DOTNET_AUTO_CONTRACT|QYL_NATIVE_CONTRACT)_[0-9]{3}$" + }, + "contract_origin": { + "enum": [ + "upstream_otel_dotnet_auto_60", + "qyl_native" + ] }, "default_value": {}, "description": { @@ -439,7 +476,7 @@ "index": { "type": "integer", "minimum": 1, - "maximum": 60 + "maximum": 66 }, "is_pattern": { "type": "boolean" @@ -575,6 +612,7 @@ "required": [ "kind", "key", + "contract_origin", "status", "index", "contract_item_id", @@ -615,7 +653,13 @@ }, "contract_item_id": { "type": "string", - "pattern": "^OTEL_DOTNET_AUTO_CONTRACT_[0-9]{3}$" + "pattern": "^(?:OTEL_DOTNET_AUTO_CONTRACT|QYL_NATIVE_CONTRACT)_[0-9]{3}$" + }, + "contract_origin": { + "enum": [ + "upstream_otel_dotnet_auto_60", + "qyl_native" + ] }, "default_behavior": { "type": "string" @@ -645,7 +689,7 @@ "index": { "type": "integer", "minimum": 1, - "maximum": 60 + "maximum": 66 }, "key": { "type": "string" diff --git a/docs/qyl-aot-autoinstrumentation.conformance-plan.json b/docs/qyl-aot-autoinstrumentation.conformance-plan.json index 6fbde70..2cc07c2 100644 --- a/docs/qyl-aot-autoinstrumentation.conformance-plan.json +++ b/docs/qyl-aot-autoinstrumentation.conformance-plan.json @@ -353,6 +353,67 @@ } ] }, + { + "service_name": "qyl-mcp-aot-demo", + "profile_id": "qyl-aot-mcp", + "expected_signals": [ + { + "kind": "span", + "name": "mcp.initialize", + "required_attributes": [ + "mcp.method.name", + "mcp.session.id", + "network.transport" + ], + "recommended_attributes": [ + "session.id" + ], + "opt_in_attributes": [] + }, + { + "kind": "span", + "name": "mcp.notifications.initialized", + "required_attributes": [ + "mcp.method.name", + "mcp.session.id", + "network.transport" + ], + "recommended_attributes": [ + "session.id" + ], + "opt_in_attributes": [] + }, + { + "kind": "span", + "name": "mcp.tools.list", + "required_attributes": [ + "mcp.method.name", + "mcp.session.id", + "network.transport" + ], + "recommended_attributes": [ + "session.id" + ], + "opt_in_attributes": [] + }, + { + "kind": "span", + "name": "mcp.tools.call", + "required_attributes": [ + "gen_ai.operation.name", + "gen_ai.tool.name", + "mcp.method.name", + "mcp.protocol.version", + "mcp.session.id", + "network.transport" + ], + "recommended_attributes": [ + "session.id" + ], + "opt_in_attributes": [] + } + ] + }, { "service_name": "qyl-metrics-aot-demo", "profile_id": "qyl-aot-metrics", diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs index c2c60fe..1dcb620 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/AspNetCore/AspNetCoreDiagnosticListener.cs @@ -22,11 +22,9 @@ internal sealed class AspNetCoreDiagnosticListener : QylDiagnosticListenerSubscr /// protected override void OnEvent(string name, object? payload) { - if (!StringComparer.Ordinal.Equals(name, "qyl.http.server") && - !StringComparer.Ordinal.Equals(name, "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop")) - { + if (!StringComparer.Ordinal.Equals(name, "Microsoft.AspNetCore.Hosting.HttpRequestIn.Stop") || + QylAspNetCoreOwnership.MiddlewareRegistered) return; - } var method = HttpSemantics.NormalizeMethod( AspNetCorePayloadReader.GetMethod(payload) ?? @@ -48,7 +46,7 @@ protected override void OnEvent(string name, object? payload) SemanticTagWriter.Set(activity, SemanticAttributes.HttpRoute, route); SemanticTagWriter.Set(activity, SemanticAttributes.UrlPath, path); - // Option parity with the interceptor lane: url.query obeys the ASP.NET Core + // Option parity with the explicit middleware lane: url.query obeys the ASP.NET Core // redaction control; header capture obeys the configured capture lists. if (activity is not null) { diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs deleted file mode 100644 index 61f1fbc..0000000 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Diagnostics; -using Qyl.OpenTelemetry.AutoInstrumentation; -using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.Semantics; - -namespace Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.EntityFrameworkCore; - -/// -/// Subscribes to the synthetic qyl.db.efcore event used by the shared semantic demo. -/// Real EFCore command events live in Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore so -/// non-EFCore apps do not inherit EFCore package dependencies or NativeAOT warnings. -/// -internal sealed class EntityFrameworkCoreDiagnosticListener : QylDiagnosticListenerSubscriber -{ - /// - protected override string ListenerName => "Microsoft.EntityFrameworkCore"; - - /// - protected override QylAutoInstrumentationSignal Signal => QylAutoInstrumentationSignal.Traces; - - /// - protected override string InstrumentationId => QylAutoInstrumentationIds.EntityFrameworkCore; - - /// - protected override void OnEvent(string name, object? payload) - { - if (!StringComparer.Ordinal.Equals(name, "qyl.db.efcore")) - { - return; - } - - var system = DiagnosticPayloadReader.GetString(payload, QylSemanticAttributes.DbSystemName, "db.system"); - var namespaceName = DiagnosticPayloadReader.GetString(payload, "db.namespace", "db.name"); - var queryText = DiagnosticPayloadReader.GetString(payload, "db.query.text", "db.statement"); - var operation = DatabaseSemantics.NormalizeOperation( - DiagnosticPayloadReader.GetString(payload, "db.operation.name", "db.operation"), - queryText); - var querySummary = DiagnosticPayloadReader.GetString(payload, "db.query.summary"); - var errorType = DiagnosticPayloadReader.GetString(payload, "error.type", "exception.type"); - - using var activity = QylActivitySource.Source.StartActivity(QylActivityNames.DbCommand(operation), ActivityKind.Client); - - SemanticTagWriter.Set(activity, SemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.DbEfCore); - SemanticTagWriter.Set(activity, SemanticAttributes.DbSystem, system); - SemanticTagWriter.Set(activity, SemanticAttributes.DbNamespace, namespaceName); - SemanticTagWriter.Set(activity, SemanticAttributes.DbOperationName, operation); - SemanticTagWriter.Set(activity, SemanticAttributes.DbQuerySummary, querySummary); - if (DatabaseSemantics.ShouldWriteQueryText( - queryText, - operation, - QylAutoInstrumentationOptions.Current.EntityFrameworkCoreSetDbStatementForText)) - { - SemanticTagWriter.Set(activity, SemanticAttributes.DbQueryText, queryText); - } - - DatabaseSemantics.SetError(activity, errorType); - } -} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/QylDiagnosticListenerSubscriber.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/QylDiagnosticListenerSubscriber.cs index 51eea0b..8f82d40 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/QylDiagnosticListenerSubscriber.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/QylDiagnosticListenerSubscriber.cs @@ -6,8 +6,8 @@ namespace Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners; /// /// Base subscriber for built-in events. DiagnosticSource is a /// managed BCL primitive, so this layer emits spans without IL rewriting or runtime code generation. -/// Concrete subscribers react on completion (*.Stop) and use -/// QylActivitySource.StartAtAmbientStart so emitted duration reflects the real operation. +/// Completion-driven subscribers use QylActivitySource.StartAtAmbientStart so emitted +/// duration reflects the real operation. /// public abstract class QylDiagnosticListenerSubscriber : IObserver>, IDisposable { diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/SqlClient/SqlClientDiagnosticListener.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/SqlClient/SqlClientDiagnosticListener.cs deleted file mode 100644 index 24a0519..0000000 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/SqlClient/SqlClientDiagnosticListener.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Diagnostics; -using Qyl.OpenTelemetry.AutoInstrumentation; -using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.Semantics; - -namespace Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.SqlClient; - -/// -/// Subscribes to the synthetic qyl.db.sqlclient event used by the shared semantic demo. -/// Real Microsoft.Data.SqlClient command events live in Qyl.OpenTelemetry.AutoInstrumentation.SqlClient -/// so non-SqlClient apps do not inherit SqlClient package dependencies or NativeAOT warnings. -/// -internal sealed class SqlClientDiagnosticListener : QylDiagnosticListenerSubscriber -{ - /// - protected override string ListenerName => "SqlClientDiagnosticListener"; - - /// - protected override QylAutoInstrumentationSignal Signal => QylAutoInstrumentationSignal.Traces; - - /// - protected override string InstrumentationId => QylAutoInstrumentationIds.SqlClient; - - /// - protected override void OnEvent(string name, object? payload) - { - if (!StringComparer.Ordinal.Equals(name, "qyl.db.sqlclient")) - { - return; - } - - var namespaceName = DiagnosticPayloadReader.GetString(payload, "db.namespace", "db.name"); - var queryText = DiagnosticPayloadReader.GetString(payload, "db.query.text", "db.statement"); - var operation = DatabaseSemantics.NormalizeOperation( - DiagnosticPayloadReader.GetString(payload, "db.operation.name", "db.operation"), - queryText); - var querySummary = DiagnosticPayloadReader.GetString(payload, "db.query.summary"); - var serverAddress = DiagnosticPayloadReader.GetString(payload, "server.address", "peer.hostname"); - var errorType = DiagnosticPayloadReader.GetString(payload, "error.type", "exception.type"); - - using var activity = QylActivitySource.Source.StartActivity(QylActivityNames.SqlClientCommand(operation), ActivityKind.Client); - - SemanticTagWriter.Set(activity, SemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.DbSqlClient); - SemanticTagWriter.Set(activity, SemanticAttributes.DbSystem, QylSemanticAttributes.DbSystemMicrosoftSqlServer); - SemanticTagWriter.Set(activity, SemanticAttributes.DbNamespace, namespaceName); - SemanticTagWriter.Set(activity, SemanticAttributes.DbOperationName, operation); - SemanticTagWriter.Set(activity, SemanticAttributes.DbQuerySummary, querySummary); - if (DatabaseSemantics.ShouldWriteQueryText( - queryText, - operation, - QylAutoInstrumentationOptions.Current.SqlClientSetDbStatementForText)) - { - SemanticTagWriter.Set(activity, SemanticAttributes.DbQueryText, queryText); - } - - SemanticTagWriter.Set(activity, SemanticAttributes.ServerAddress, serverAddress); - DatabaseSemantics.SetError(activity, errorType); - } -} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreAutoInstrumentationBootstrap.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreAutoInstrumentationBootstrap.cs index 204c080..63b351e 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreAutoInstrumentationBootstrap.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreAutoInstrumentationBootstrap.cs @@ -1,9 +1,10 @@ -namespace Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore; +namespace Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; /// Package bootstrap surface used by the build-transitive consumer initializer. +[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static class EntityFrameworkCoreAutoInstrumentationBootstrap { /// Boot qyl EFCore auto-instrumentation for the current process. public static void Boot() - => EntityFrameworkCoreModuleInitializerBoot.Boot(); + => global::Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore.EntityFrameworkCoreModuleInitializerBoot.Boot(); } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs index 571e7a6..07f86d1 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs @@ -31,7 +31,11 @@ protected override void OnEvent(string name, object? payload) if (!EntityFrameworkCorePayloadReader.TryRead(payload, out var command)) return; - using var activity = QylActivitySource.Source.StartActivity(QylActivityNames.DbCommand(command.Operation), ActivityKind.Client); + using var activity = QylActivitySource.StartAt( + QylActivityNames.DbCommand(command.Operation), + ActivityKind.Client, + command.StartTime); + activity?.SetEndTime((command.StartTime + command.Duration).UtcDateTime); SemanticTagWriter.Set(activity, SemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.DbEfCore); SemanticTagWriter.Set(activity, SemanticAttributes.DbSystem, command.DbSystem); diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCorePayloadReader.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCorePayloadReader.cs index 9a4189a..d8ea8ff 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCorePayloadReader.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCorePayloadReader.cs @@ -25,7 +25,9 @@ public static bool TryRead(object? payload, out EntityFrameworkCoreCommand comma QueryText: commandEvent.Command.CommandText, ErrorType: payload is CommandErrorEventData errorEvent ? errorEvent.Exception.GetType().FullName - : null); + : null, + StartTime: commandEvent is CommandEndEventData endEvent ? endEvent.StartTime : TimeProvider.System.GetUtcNow(), + Duration: commandEvent is CommandEndEventData timedEvent ? timedEvent.Duration : TimeSpan.Zero); return true; } @@ -53,4 +55,6 @@ internal readonly record struct EntityFrameworkCoreCommand( string? Operation, string? QuerySummary, string? QueryText, - string? ErrorType); + string? ErrorType, + DateTimeOffset StartTime, + TimeSpan Duration); diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Shipped.txt b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Shipped.txt index 3beacf6..e8ca69d 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Shipped.txt +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/PublicAPI.Shipped.txt @@ -1,3 +1,3 @@ #nullable enable -Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore.EntityFrameworkCoreAutoInstrumentationBootstrap -static Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore.EntityFrameworkCoreAutoInstrumentationBootstrap.Boot() -> void +Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.EntityFrameworkCoreAutoInstrumentationBootstrap +static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.EntityFrameworkCoreAutoInstrumentationBootstrap.Boot() -> void diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/buildTransitive/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore.Bootstrap.g.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/buildTransitive/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore.Bootstrap.g.cs index 633a208..37e1358 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/buildTransitive/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore.Bootstrap.g.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/buildTransitive/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore.Bootstrap.g.cs @@ -8,6 +8,6 @@ internal static class QylEntityFrameworkCoreAutoInstrumentationConsumerBootstrap { [ModuleInitializer] internal static void Initialize() - => EntityFrameworkCoreAutoInstrumentationBootstrap.Boot(); + => global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.EntityFrameworkCoreAutoInstrumentationBootstrap.Boot(); } #endif diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/ModuleInitializerBoot.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/ModuleInitializerBoot.cs index 5115f10..5d2af67 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/ModuleInitializerBoot.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/ModuleInitializerBoot.cs @@ -2,10 +2,8 @@ using System.Runtime.CompilerServices; using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners; using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.AspNetCore; -using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.EntityFrameworkCore; using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.GrpcClient; using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.HttpClient; -using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.SqlClient; namespace Qyl.OpenTelemetry.AutoInstrumentation.Hosting; @@ -29,8 +27,6 @@ internal static class ModuleInitializerBoot [ new HttpClientDiagnosticListener(), new AspNetCoreDiagnosticListener(), - new EntityFrameworkCoreDiagnosticListener(), - new SqlClientDiagnosticListener(), new GrpcClientDiagnosticListener(), ]; diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/PublicAPI.Unshipped.txt b/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/PublicAPI.Unshipped.txt index b942773..7dc5c58 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/PublicAPI.Unshipped.txt +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/PublicAPI.Unshipped.txt @@ -1,6 +1 @@ #nullable enable -*REMOVED*Qyl.OpenTelemetry.AutoInstrumentation.Hosting.QylAutoInstrumentationHostingOptions -*REMOVED*Qyl.OpenTelemetry.AutoInstrumentation.Hosting.QylAutoInstrumentationHostingOptions.EnableConformanceProcessor.get -> bool -*REMOVED*Qyl.OpenTelemetry.AutoInstrumentation.Hosting.QylAutoInstrumentationHostingOptions.EnableConformanceProcessor.set -> void -*REMOVED*Qyl.OpenTelemetry.AutoInstrumentation.Hosting.QylAutoInstrumentationHostingOptions.QylAutoInstrumentationHostingOptions() -> void -*REMOVED*static Qyl.OpenTelemetry.AutoInstrumentation.Hosting.QylAutoInstrumentationServiceCollectionExtensions.AddQylAutoInstrumentation(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/Qyl.OpenTelemetry.AutoInstrumentation.Hosting.csproj b/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/Qyl.OpenTelemetry.AutoInstrumentation.Hosting.csproj index 0d8a817..151f4b2 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/Qyl.OpenTelemetry.AutoInstrumentation.Hosting.csproj +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/Qyl.OpenTelemetry.AutoInstrumentation.Hosting.csproj @@ -22,7 +22,9 @@ - + diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Descriptors.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Descriptors.cs index 4ca41d4..64ab4cc 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Descriptors.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Descriptors.cs @@ -10,10 +10,6 @@ public sealed partial class QylAutoInstrumentationGenerator private enum InterceptorKind { HttpClient, - HttpWebRequest, - AspNetCoreEndpointMap, - MeterProviderBuilderAddMeter, - AzureClient, ElasticsearchClient, ElasticTransport, WcfClient, @@ -34,8 +30,6 @@ private enum InterceptorKind ILoggerLog, NLogLogger, Log4NetLogger, - EntityFrameworkCoreDbContext, - EntityFrameworkCoreQueryable, DbCommand, } @@ -127,29 +121,6 @@ public override void Emit(StringBuilder builder, in InterceptedInvocation invoca => EmitDbCommandInterceptor(builder, in invocation, index, this); } - private sealed record HttpWebRequestBodyDescriptor( - string MethodPrefix, - string ReceiverName, - string RequestType, - string HelperType, - string GetStartTimeUtcMethod, - string StartActivityMethod, - string RecordResultMethod, - string RecordExceptionMethod) : InterceptorBodyDescriptor - { - public override void Emit(StringBuilder builder, in InterceptedInvocation invocation, int index) - => EmitHttpWebRequestInterceptor(builder, in invocation, index, this); - } - - private sealed record MeterProviderBuilderBodyDescriptor( - string MethodPrefix, - string ReceiverName, - string EnabledMeterNamesExpression) : InterceptorBodyDescriptor - { - public override void Emit(StringBuilder builder, in InterceptedInvocation invocation, int index) - => EmitMeterProviderBuilderAddMeterInterceptor(builder, in invocation, index, this); - } - private sealed record LoggerBodyDescriptor( LoggerInterceptorBodyKind Kind, string MethodPrefix, @@ -353,12 +324,6 @@ private enum TelemetrySignal Logs, } - // Signal and AdditionalMetricIds are intentionally never read by C#: their - // consumer is tools/verify-contract-invariants.py, which parses the generator - // source at InterceptorTarget construction sites and composes contract keys - // from TelemetrySignal + InstrumentationId (69429cb made freeform keys - // unrepresentable). They also participate in record value equality. Do not - // delete them to satisfy an unused-member inspection. private readonly record struct InterceptorTarget( InterceptorKind Kind, TelemetrySignal Signal, diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Detection.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Detection.cs index 7da2e83..064dd08 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Detection.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Detection.cs @@ -94,44 +94,20 @@ private static bool TryGetHttpClientInvocation(IMethodSymbol symbol, out Interce return false; } - private static bool TryGetHttpWebRequestInvocation(IMethodSymbol symbol, ITypeSymbol? receiverType, out InterceptorTarget target) - { - target = default; - var effectiveReceiverType = IsType(symbol.ContainingType, "global::System.Net.HttpWebRequest") - ? symbol.ContainingType - : receiverType; - - if (effectiveReceiverType is null || - !IsType(effectiveReceiverType, "global::System.Net.HttpWebRequest") || - !TryGetNoParameters(symbol, out var parameters)) - { - return false; - } - - var methodName = symbol.Name; - var isAsync = methodName.EndsWithOrdinal("Async"); - if (!TryGetHttpWebRequestReturn(symbol, methodName, isAsync, out var returnType)) - return false; - - target = new InterceptorTarget( - InterceptorKind.HttpWebRequest, - TelemetrySignal.Traces, - "HTTPCLIENT", - CleanTypeName(symbol.ContainingType), - methodName, - returnType, - parameters, - isAsync, - AdditionalMetricIds: MetricIds("HTTPCLIENT")); - return true; - } - - private static bool TryGetDbCommandInvocation(IMethodSymbol symbol, out InterceptorTarget target) + private static bool TryGetDbCommandInvocation( + IMethodSymbol symbol, + ITypeSymbol? receiverType, + out InterceptorTarget target) { target = default; if (!InheritsFromOrIs(symbol.ContainingType, "global::System.Data.Common.DbCommand")) return false; + var effectiveReceiverType = receiverType is not null && + InheritsFromOrIs(receiverType, "global::System.Data.Common.DbCommand") + ? receiverType + : symbol.ContainingType; + var methodName = symbol.Name; var isAsync = methodName.EndsWithOrdinal("Async"); if (!TryGetDbCommandParameters(symbol, methodName, out var parameters)) @@ -140,7 +116,7 @@ private static bool TryGetDbCommandInvocation(IMethodSymbol symbol, out Intercep if (!TryGetDbCommandReturn(symbol, methodName, isAsync, out var returnType)) return false; - var instrumentationId = GetDbInstrumentationId(symbol.ContainingType); + var instrumentationId = GetDbInstrumentationId(effectiveReceiverType); target = new InterceptorTarget( InterceptorKind.DbCommand, TelemetrySignal.Traces, @@ -154,171 +130,6 @@ private static bool TryGetDbCommandInvocation(IMethodSymbol symbol, out Intercep return true; } - private static bool TryGetAspNetCoreEndpointMapInvocation(IMethodSymbol symbol, out InterceptorTarget target) - { - target = default; - var original = symbol.ReducedFrom; - - if (original is null || - !IsSupportedAspNetCoreMapMethod(symbol.Name) || - !IsType(original.ContainingType, "global::Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions") || - !TryGetAspNetCoreEndpointMapReturnType(symbol, out var returnType)) - return false; - - target = new InterceptorTarget( - InterceptorKind.AspNetCoreEndpointMap, - TelemetrySignal.Traces, - "ASPNETCORE", - "global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder", - symbol.Name, - returnType, - BuildParameters(symbol), - false); - return true; - } - - private static bool TryGetAspNetCoreEndpointMapReturnType(IMethodSymbol symbol, out string returnType) - { - returnType = string.Empty; - - var handlerIndex = 1; - if (string.Equals(symbol.Name, "MapMethods", StringComparison.Ordinal)) - { - if (symbol.Parameters.Length is not 3 || - !IsType(symbol.Parameters[0].Type, "global::System.String") || - !IsConstructedFrom(symbol.Parameters[1].Type, "global::System.Collections.Generic.IEnumerable")) - { - return false; - } - - handlerIndex = 2; - } - else if (symbol.Parameters.Length is not 2 || - !IsType(symbol.Parameters[0].Type, "global::System.String")) - { - return false; - } - - if (IsType(symbol.Parameters[handlerIndex].Type, "global::Microsoft.AspNetCore.Http.RequestDelegate") && - IsType(symbol.ReturnType, "global::Microsoft.AspNetCore.Builder.IEndpointConventionBuilder")) - { - returnType = "global::Microsoft.AspNetCore.Builder.IEndpointConventionBuilder"; - return true; - } - - return false; - } - - private static bool IsSupportedAspNetCoreMapMethod(string name) - => name is "MapGet" or "MapPost" or "MapPut" or "MapDelete" or "MapPatch" or "MapMethods"; - - private static bool TryGetMeterProviderBuilderAddMeterInvocation(IMethodSymbol symbol, out InterceptorTarget target) - { - target = default; - ITypeSymbol receiverType = symbol.ContainingType; - var extensionContainingType = string.Empty; - if (symbol.ReducedFrom is { Parameters.Length: > 0 } original) - { - receiverType = original.Parameters[0].Type; - extensionContainingType = CleanTypeName(original.ContainingType); - } - else if (symbol.IsStatic) - { - return false; - } - - if (!string.Equals(symbol.Name, "AddMeter", StringComparison.Ordinal) || - !IsType(receiverType, "global::OpenTelemetry.Metrics.MeterProviderBuilder") || - !IsType(symbol.ReturnType, "global::OpenTelemetry.Metrics.MeterProviderBuilder") || - symbol.Parameters.Length is not 1 || - !IsArrayOf(symbol.Parameters[0].Type, "global::System.String")) - { - return false; - } - - target = new InterceptorTarget( - InterceptorKind.MeterProviderBuilderAddMeter, - TelemetrySignal.Metrics, - "ASPNETCORE", - CleanTypeName(receiverType), - "AddMeter", - CleanTypeName(symbol.ReturnType, symbol), - BuildParameters(symbol), - false, - ExtensionContainingType: extensionContainingType, - AdditionalMetricIds: MetricIds( - "HTTPCLIENT", - "NETRUNTIME", - "NPGSQL", - "NSERVICEBUS", - "PROCESS", - "SQLCLIENT")); - return true; - } - - private static bool TryGetAzureClientInvocation(IMethodSymbol symbol, out InterceptorTarget target) - { - target = default; - if (symbol.IsStatic || - symbol.MethodKind is not MethodKind.Ordinary || - !CanEmitByValueOrInParameters(symbol) || - !IsAzureClientType(symbol.ContainingType) || - !TryGetAzureClientOperationReturn(symbol.ReturnType, out var isAsync)) - { - return false; - } - - target = new InterceptorTarget( - InterceptorKind.AzureClient, - TelemetrySignal.Traces, - "AZURE", - CleanTypeName(symbol.ContainingType), - symbol.Name, - CleanTypeName(symbol.ReturnType, symbol), - BuildParameters(symbol), - isAsync); - return true; - } - - private static bool IsAzureClientType(ITypeSymbol? symbol) - { - if (symbol is not INamedTypeSymbol named || - !named.Name.EndsWithOrdinal("Client")) - { - return false; - } - - // Require the client to come from a real Azure SDK assembly (Azure.Storage.Blobs, - // Azure.Messaging.*, …) — not merely any user type parked in an `Azure.*` namespace. - // Together with the Azure.Response return-type gate this stops a false-positive Azure span - // on a user-authored *Client placed in the reserved Azure root namespace. - var namespaceName = named.ContainingNamespace.ToDisplayString(); - return namespaceName.StartsWithOrdinal("Azure.") && - !namespaceName.StartsWithOrdinal("Azure.Core") && - named.ContainingAssembly?.Name is { } assemblyName && - assemblyName.StartsWithOrdinal("Azure."); - } - - private static bool TryGetAzureClientOperationReturn(ITypeSymbol returnType, out bool isAsync) - { - isAsync = false; - if (IsAzureResponseType(returnType)) - return true; - - if (TryGetTaskResult(returnType, out var resultType) && IsAzureResponseType(resultType)) - { - isAsync = true; - return true; - } - - return false; - } - - private static bool IsAzureResponseType(ITypeSymbol? symbol) - => symbol is INamedTypeSymbol named && - (IsTypeByMetadata(named, "Azure", "Response") || - IsConstructedGeneric(named, "Azure", "Response`1")); - private static bool TryGetElasticInvocation(IMethodSymbol symbol, out InterceptorTarget target) { if (TryGetElasticsearchClientInvocation(symbol, out target)) @@ -1158,99 +969,4 @@ private static bool TryGetKafkaConsumerInvocation(IMethodSymbol symbol, out Inte return true; } - private static bool TryGetEntityFrameworkCoreDbContextInvocation(IMethodSymbol symbol, out InterceptorTarget target) - { - target = default; - if (!InheritsFromOrIs(symbol.ContainingType, "global::Microsoft.EntityFrameworkCore.DbContext")) - return false; - - if (string.Equals(symbol.Name, "SaveChanges", StringComparison.Ordinal) && - symbol.ReturnType.SpecialType is SpecialType.System_Int32 && - TryGetEfCoreSaveChangesParameters(symbol, allowCancellationToken: false, out var parameters)) - { - target = new InterceptorTarget( - InterceptorKind.EntityFrameworkCoreDbContext, - TelemetrySignal.Traces, - "ENTITYFRAMEWORKCORE", - CleanTypeName(symbol.ContainingType), - "SaveChanges", - "int", - parameters, - false); - return true; - } - - if (string.Equals(symbol.Name, "SaveChangesAsync", StringComparison.Ordinal) && - IsTaskOf(symbol.ReturnType, "global::System.Int32") && - TryGetEfCoreSaveChangesParameters(symbol, allowCancellationToken: true, out parameters)) - { - target = new InterceptorTarget( - InterceptorKind.EntityFrameworkCoreDbContext, - TelemetrySignal.Traces, - "ENTITYFRAMEWORKCORE", - CleanTypeName(symbol.ContainingType), - "SaveChangesAsync", - "global::System.Threading.Tasks.Task", - parameters, - true); - return true; - } - - return false; - } - - private static bool TryGetEntityFrameworkCoreQueryableInvocation(IMethodSymbol symbol, out InterceptorTarget target) - { - target = default; - var original = symbol.ReducedFrom; - if (original is null || - !IsType(original.ContainingType, "global::Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions") || - !IsSupportedEntityFrameworkCoreQueryableMethod(symbol.Name) || - !CanEmitByValueOrInParameters(symbol) || - original.Parameters.Length is 0 || - !TryGetEntityFrameworkCoreQueryableReturn(symbol.ReturnType)) - { - return false; - } - - target = new InterceptorTarget( - InterceptorKind.EntityFrameworkCoreQueryable, - TelemetrySignal.Traces, - "ENTITYFRAMEWORKCORE", - CleanTypeName(original.Parameters[0].Type), - symbol.Name, - CleanTypeName(symbol.ReturnType, symbol), - BuildParameters(symbol), - true, - GetTypeParameterList(symbol), - GetConstraintClauses(symbol), - ExtensionContainingType: "global::Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions"); - return true; - } - - private static bool IsSupportedEntityFrameworkCoreQueryableMethod(string methodName) - => methodName is "ToListAsync" or - "ToArrayAsync" or - "FirstAsync" or - "FirstOrDefaultAsync" or - "SingleAsync" or - "SingleOrDefaultAsync" or - "LastAsync" or - "LastOrDefaultAsync" or - "AnyAsync" or - "AllAsync" or - "CountAsync" or - "LongCountAsync" or - "MinAsync" or - "MaxAsync" or - "SumAsync" or - "AverageAsync" or - "ContainsAsync" or - "LoadAsync" or - "ForEachAsync" or - "ExecuteDeleteAsync" or - "ExecuteUpdateAsync"; - - private static bool TryGetEntityFrameworkCoreQueryableReturn(ITypeSymbol returnType) - => IsTask(returnType) || TryGetTaskResult(returnType, out _); } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Shapes.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Shapes.cs index 07942dd..6631576 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Shapes.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Shapes.cs @@ -55,32 +55,6 @@ private static bool TryGetDbCommandReturn(IMethodSymbol symbol, string methodNam return false; } - private static bool TryGetHttpWebRequestReturn(IMethodSymbol symbol, string methodName, bool isAsync, out string returnType) - { - returnType = CleanTypeName(symbol.ReturnType, symbol); - if (!isAsync) - { - if (string.Equals(methodName, "GetResponse", StringComparison.Ordinal)) - return InheritsFromOrIs(symbol.ReturnType, "global::System.Net.WebResponse"); - - if (string.Equals(methodName, "GetRequestStream", StringComparison.Ordinal)) - return InheritsFromOrIs(symbol.ReturnType, "global::System.IO.Stream"); - - return false; - } - - if (!TryGetTaskResult(symbol.ReturnType, out var taskResult)) - return false; - - if (string.Equals(methodName, "GetResponseAsync", StringComparison.Ordinal)) - return InheritsFromOrIs(taskResult, "global::System.Net.WebResponse"); - - if (string.Equals(methodName, "GetRequestStreamAsync", StringComparison.Ordinal)) - return InheritsFromOrIs(taskResult, "global::System.IO.Stream"); - - return false; - } - private static InterceptorTarget HttpTarget(IMethodSymbol symbol, string methodName, string returnType, EquatableArray parameters) => new( InterceptorKind.HttpClient, @@ -383,37 +357,6 @@ private static bool TryGetRedisStringGetParameters(IMethodSymbol symbol, out Equ return true; } - private static bool TryGetEfCoreSaveChangesParameters(IMethodSymbol symbol, bool allowCancellationToken, out EquatableArray parameters) - { - if (symbol.Parameters.Length is 0) - { - parameters = default; - return true; - } - - if (symbol.Parameters.Length is 1) - { - if (symbol.Parameters[0].Type.SpecialType is SpecialType.System_Boolean || - (allowCancellationToken && IsType(symbol.Parameters[0].Type, "global::System.Threading.CancellationToken"))) - { - parameters = BuildParameters(symbol); - return true; - } - } - - if (allowCancellationToken && - symbol.Parameters.Length is 2 && - symbol.Parameters[0].Type.SpecialType is SpecialType.System_Boolean && - IsType(symbol.Parameters[1].Type, "global::System.Threading.CancellationToken")) - { - parameters = BuildParameters(symbol); - return true; - } - - parameters = default; - return false; - } - private static bool TryGetRabbitMqBasicPublishParameters(IMethodSymbol symbol, out EquatableArray parameters) { parameters = default; @@ -869,7 +812,41 @@ private static string GetGrpcStreamReaderHelperType(InterceptedInvocation[] invo private static void AppendStringLiteral(StringBuilder builder, string value) { builder.Append('"'); - builder.Append(value.Replace("\\", @"\\").Replace("\"", "\\\"")); + foreach (var character in value) + { + switch (character) + { + case '"': + builder.Append("\\\""); + break; + case '\\': + builder.Append("\\\\"); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + case < ' ': + builder.Append("\\u"); + builder.Append(((int)character).ToString("x4", System.Globalization.CultureInfo.InvariantCulture)); + break; + default: + builder.Append(character); + break; + } + } + builder.Append('"'); } } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs index d391f0a..11b7658 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.cs @@ -154,14 +154,16 @@ private static void EmitInterceptors( builder.AppendLine("{"); builder.AppendLine(" internal static class QylGeneratedInterceptors"); builder.AppendLine(" {"); - builder.AppendLine(" private const int RequiredQylGeneratedCodeAbi = global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylGeneratedCodeAbi.V6;"); + builder.AppendLine(" private const int RequiredQylGeneratedCodeAbi = global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylGeneratedCodeAbi.V8;"); builder.AppendLine(); for (var index = 0; index < invocations.Length; index++) { context.CancellationToken.ThrowIfCancellationRequested(); var invocation = invocations[index]; - var descriptor = GetEmissionDescriptor(invocation.Target); + var target = invocation.Target; + var descriptor = GetEmissionDescriptor(in target); + EmitInterceptorManifest(builder, in target); descriptor.Body.Emit(builder, in invocation, index); } @@ -176,6 +178,45 @@ private static void EmitInterceptors( SourceText.From(builder.ToString(), Encoding.UTF8)); } + private static void EmitInterceptorManifest(StringBuilder builder, in InterceptorTarget target) + { + builder.Append(" // qyl-interceptor-manifest: {\"interceptorKind\":"); + AppendStringLiteral(builder, target.Kind.ToString()); + builder.Append(",\"signal\":"); + AppendStringLiteral(builder, GetManifestSignalName(target.Signal)); + builder.Append(",\"instrumentationId\":"); + AppendStringLiteral(builder, target.InstrumentationId); + builder.Append(",\"additionalMetricIds\":["); + for (var index = 0; index < target.AdditionalMetricIds.Length; index++) + { + if (index > 0) + builder.Append(','); + AppendStringLiteral(builder, target.AdditionalMetricIds[index]); + } + + builder.Append("],\"contractKeys\":["); + AppendStringLiteral(builder, GetContractKey(target.Signal, target.InstrumentationId)); + for (var index = 0; index < target.AdditionalMetricIds.Length; index++) + { + builder.Append(','); + AppendStringLiteral(builder, GetContractKey(TelemetrySignal.Metrics, target.AdditionalMetricIds[index])); + } + + builder.AppendLine("]}"); + } + + private static string GetContractKey(TelemetrySignal signal, string instrumentationId) + => "signals." + GetManifestSignalName(signal) + "." + instrumentationId; + + private static string GetManifestSignalName(TelemetrySignal signal) + => signal switch + { + TelemetrySignal.Traces => "traces", + TelemetrySignal.Metrics => "metrics", + TelemetrySignal.Logs => "logs", + _ => throw new InvalidOperationException("Unknown telemetry signal: " + signal), + }; + private static void EmitInterceptsLocationAttribute(StringBuilder builder) { builder.AppendLine("namespace System.Runtime.CompilerServices"); @@ -417,79 +458,6 @@ private static void AppendTraceStartActivityArguments( } } - private static void EmitHttpWebRequestInterceptor( - StringBuilder builder, - in InterceptedInvocation invocation, - int index, - HttpWebRequestBodyDescriptor descriptor) - { - var target = invocation.Target; - EmitAttributeAndSignature(builder, invocation.Location, target.ReturnType, - descriptor.MethodPrefix + "_" + target.MethodName, index, target.ReceiverType, descriptor.ReceiverName, - target.Parameters, target.IsAsync); - builder.AppendLine(" {"); - builder.Append(" var httpWebRequest = ("); - builder.Append(descriptor.RequestType); - builder.Append(')'); - builder.Append(descriptor.ReceiverName); - builder.AppendLine(";"); - builder.Append(" var metricStartTimeUtc = "); - builder.Append(descriptor.HelperType); - builder.Append('.'); - builder.Append(descriptor.GetStartTimeUtcMethod); - builder.AppendLine("();"); - builder.Append(" var activity = "); - builder.Append(descriptor.HelperType); - builder.Append('.'); - builder.Append(descriptor.StartActivityMethod); - builder.Append("(httpWebRequest, "); - AppendStringLiteral(builder, target.MethodName); - builder.AppendLine(");"); - builder.AppendLine(" try"); - builder.AppendLine(" {"); - - if (target.IsAsync) - { - builder.Append(" var result = await "); - builder.Append(descriptor.ReceiverName); - builder.Append('.'); - builder.Append(target.MethodName); - builder.Append('('); - AppendArgumentList(builder, target.Parameters, includeLeadingComma: false); - builder.AppendLine(").ConfigureAwait(false);"); - } - else - { - builder.Append(" var result = "); - builder.Append(descriptor.ReceiverName); - builder.Append('.'); - builder.Append(target.MethodName); - builder.Append('('); - AppendArgumentList(builder, target.Parameters, includeLeadingComma: false); - builder.AppendLine(");"); - } - - builder.Append(" "); - builder.Append(descriptor.HelperType); - builder.Append('.'); - builder.Append(descriptor.RecordResultMethod); - builder.AppendLine("(activity, metricStartTimeUtc, httpWebRequest.Method, result);"); - builder.AppendLine(" return result;"); - builder.AppendLine(" }"); - builder.AppendLine(" catch (global::System.Exception exception)"); - builder.AppendLine(" {"); - builder.Append(" "); - builder.Append(descriptor.HelperType); - builder.Append('.'); - builder.Append(descriptor.RecordExceptionMethod); - builder.AppendLine("(activity, metricStartTimeUtc, httpWebRequest.Method, exception);"); - builder.AppendLine(" throw;"); - builder.AppendLine(" }"); - EmitActivityDisposeFinally(builder); - builder.AppendLine(" }"); - builder.AppendLine(); - } - private static void EmitDbCommandInterceptor( StringBuilder builder, in InterceptedInvocation invocation, @@ -585,41 +553,6 @@ private static void EmitDbCommandInterceptor( builder.AppendLine(); } - private static void EmitMeterProviderBuilderAddMeterInterceptor( - StringBuilder builder, - in InterceptedInvocation invocation, - int index, - MeterProviderBuilderBodyDescriptor descriptor) - { - var target = invocation.Target; - EmitAttributeAndSignature(builder, invocation.Location, target.ReturnType, - descriptor.MethodPrefix + "_" + target.MethodName, index, target.ReceiverType, descriptor.ReceiverName, - target.Parameters, isAsync: false); - builder.AppendLine(" {"); - builder.Append(" var result = "); - AppendInvocationCall(builder, in target, descriptor.ReceiverName); - builder.AppendLine(";"); - builder.Append(" var qylMeters = "); - builder.Append(descriptor.EnabledMeterNamesExpression); - builder.AppendLine(";"); - if (string.IsNullOrEmpty(target.ExtensionContainingType)) - { - builder.AppendLine(" return qylMeters.Length is 0 ? result : result.AddMeter(qylMeters);"); - } - else - { - builder.Append(" return qylMeters.Length is 0 ? result : "); - builder.Append(target.ExtensionContainingType); - builder.Append('.'); - builder.Append(target.MethodName); - builder.AppendLine("(result, qylMeters);"); - } - - builder.AppendLine(); - builder.AppendLine(" }"); - builder.AppendLine(); - } - private static void EmitGrpcNetClientInterceptor( StringBuilder builder, in InterceptedInvocation invocation, diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylGeneratedSourceInterceptorCatalog.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylGeneratedSourceInterceptorCatalog.cs index da5e566..6df16dc 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylGeneratedSourceInterceptorCatalog.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylGeneratedSourceInterceptorCatalog.cs @@ -7,10 +7,6 @@ public sealed partial class QylAutoInstrumentationGenerator private static ImmutableArray CreateGeneratedMatcherDescriptors() => ImmutableArray.Create( new InterceptorMatcherDescriptor(TryGetHttpClientInvocation), - new InterceptorMatcherDescriptor(TryGetHttpWebRequestInvocation), - new InterceptorMatcherDescriptor(TryGetAspNetCoreEndpointMapInvocation), - new InterceptorMatcherDescriptor(TryGetMeterProviderBuilderAddMeterInvocation), - new InterceptorMatcherDescriptor(TryGetAzureClientInvocation), new InterceptorMatcherDescriptor(TryGetElasticInvocation), new InterceptorMatcherDescriptor(TryGetWcfClientInvocation), new InterceptorMatcherDescriptor(TryGetGrpcNetClientAsyncUnaryInvocation), @@ -21,8 +17,6 @@ private static ImmutableArray CreateGeneratedMatch new InterceptorMatcherDescriptor(TryGetQuartzInvocation), new InterceptorMatcherDescriptor(TryGetStackExchangeRedisInvocation), new InterceptorMatcherDescriptor(TryGetGraphQlInvocation), - new InterceptorMatcherDescriptor(TryGetEntityFrameworkCoreDbContextInvocation), - new InterceptorMatcherDescriptor(TryGetEntityFrameworkCoreQueryableInvocation), new InterceptorMatcherDescriptor(TryGetMongoDbInvocation), new InterceptorMatcherDescriptor(TryGetDbCommandInvocation), new InterceptorMatcherDescriptor(TryGetRabbitMqInvocation), @@ -34,10 +28,6 @@ private static ImmutableArray CreateGeneratedMatch private static ImmutableArray CreateGeneratedEmissionDescriptors() => ImmutableArray.Create( new InterceptorEmissionDescriptor(InterceptorKind.HttpClient, new ForwardingInterceptorBodyDescriptor("HttpClient", "client", "global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpClient", ReceiverTypeOverride: "global::System.Net.Http.HttpClient")), - new InterceptorEmissionDescriptor(InterceptorKind.HttpWebRequest, new HttpWebRequestBodyDescriptor("HttpWebRequest", "request", "global::System.Net.HttpWebRequest", "global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpWebRequest", "GetStartTimeUtc", "StartActivity", "RecordResult", "RecordException")), - new InterceptorEmissionDescriptor(InterceptorKind.AspNetCoreEndpointMap, new ForwardingInterceptorBodyDescriptor("AspNetCoreEndpointMap", "endpoints", "global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAspNetCore")), - new InterceptorEmissionDescriptor(InterceptorKind.MeterProviderBuilderAddMeter, new MeterProviderBuilderBodyDescriptor("MeterProviderBuilder", "builder", "global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.GetEnabledMeterNames()")), - new InterceptorEmissionDescriptor(InterceptorKind.AzureClient, new TraceInterceptorBodyDescriptor("AzureClient", "client", RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAzure", "StartActivity", "RecordException", TraceStartActivityArgumentKind.TargetMethodName))), new InterceptorEmissionDescriptor(InterceptorKind.ElasticsearchClient, new TraceInterceptorBodyDescriptor("Elastic", "client", MethodPrefixKind: TraceMethodPrefixKind.InstrumentationIdAndTargetMethodName, AsyncObservation: new TraceAsyncObservationDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedElastic.ObserveAsync", TraceAsyncObservationCondition.AsyncWithByRefParameters), RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedElastic", "StartActivity", "RecordException", TraceStartActivityArgumentKind.InstrumentationIdAndTargetMethodName))), new InterceptorEmissionDescriptor(InterceptorKind.ElasticTransport, new TraceInterceptorBodyDescriptor("Elastic", "client", MethodPrefixKind: TraceMethodPrefixKind.InstrumentationIdAndTargetMethodName, AsyncObservation: new TraceAsyncObservationDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedElastic.ObserveAsync", TraceAsyncObservationCondition.AsyncWithByRefParameters), RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedElastic", "StartActivity", "RecordException", TraceStartActivityArgumentKind.InstrumentationIdAndTargetMethodName))), new InterceptorEmissionDescriptor(InterceptorKind.WcfClient, new TraceInterceptorBodyDescriptor("WcfClient", "client", RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedWcfClient", "StartActivity", "RecordException", TraceStartActivityArgumentKind.ReceiverTypeAndTargetMethodName))), @@ -52,8 +42,6 @@ private static ImmutableArray CreateGeneratedEmis new InterceptorEmissionDescriptor(InterceptorKind.QuartzJobExecute, new TraceInterceptorBodyDescriptor("Quartz", "job", AsyncObservation: new TraceAsyncObservationDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedQuartz.ObserveAsync"), RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedQuartz", "StartActivity", "RecordException"))), new InterceptorEmissionDescriptor(InterceptorKind.StackExchangeRedisCommandAsync, new TraceInterceptorBodyDescriptor("StackExchangeRedis", "database", RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedRedis", "StartCommandActivity", "RecordException", TraceStartActivityArgumentKind.RedisOperationName))), new InterceptorEmissionDescriptor(InterceptorKind.GraphQlDocumentExecuter, new TraceInterceptorBodyDescriptor("GraphQl", "executer", AsyncObservation: new TraceAsyncObservationDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedGraphQl.ObserveAsync"), ActivityEnrichment: new TraceActivityEnrichmentDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedGraphQl", "RecordExecutionOptions", TraceActivityEnrichmentArgumentKind.GraphQlExecutionOptions), RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedGraphQl", "StartActivity", "RecordException"))), - new InterceptorEmissionDescriptor(InterceptorKind.EntityFrameworkCoreDbContext, new TraceInterceptorBodyDescriptor("EntityFrameworkCoreDbContext", "dbContext", RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedEntityFrameworkCore", "StartActivity", "RecordException", TraceStartActivityArgumentKind.TargetMethodName))), - new InterceptorEmissionDescriptor(InterceptorKind.EntityFrameworkCoreQueryable, new TraceInterceptorBodyDescriptor("EntityFrameworkCoreQueryable", "query", RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedEntityFrameworkCore", "StartActivity", "RecordException", TraceStartActivityArgumentKind.TargetMethodName))), new InterceptorEmissionDescriptor(InterceptorKind.MongoDbCollection, new TraceInterceptorBodyDescriptor("MongoDb", "collection", AsyncObservation: new TraceAsyncObservationDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedMongoDb.ObserveAsync"), RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedMongoDb", "StartActivity", "RecordException", TraceStartActivityArgumentKind.TargetMethodName))), new InterceptorEmissionDescriptor(InterceptorKind.DbCommand, new DbCommandBodyDescriptor("DbCommand", "command", "global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedDbCommand", "global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedDbCommand", "GetTimestamp", "StartActivity", "ObserveAsync", "RecordException", "RecordDuration")), new InterceptorEmissionDescriptor(InterceptorKind.RabbitMqBasicPublish, new TraceInterceptorBodyDescriptor("RabbitMq", "channel", RuntimeHelper: new TraceRuntimeHelperDescriptor("global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedRabbitMq", "StartPublishActivity", "RecordException", TraceStartActivityArgumentKind.RabbitMqExchange))), diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/PublicAPI.Shipped.txt b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/PublicAPI.Shipped.txt index b7aade2..1c0ae62 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/PublicAPI.Shipped.txt +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/PublicAPI.Shipped.txt @@ -1,3 +1,3 @@ #nullable enable -Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.SqlClientAutoInstrumentationBootstrap -static Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.SqlClientAutoInstrumentationBootstrap.Boot() -> void +Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.SqlClientAutoInstrumentationBootstrap +static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.SqlClientAutoInstrumentationBootstrap.Boot() -> void diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.csproj b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.csproj index e091259..e2aa992 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.csproj +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.csproj @@ -22,7 +22,9 @@ - + diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientAutoInstrumentationBootstrap.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientAutoInstrumentationBootstrap.cs index 35212a5..709b158 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientAutoInstrumentationBootstrap.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientAutoInstrumentationBootstrap.cs @@ -1,9 +1,10 @@ -namespace Qyl.OpenTelemetry.AutoInstrumentation.SqlClient; +namespace Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; /// Package bootstrap surface used by the build-transitive consumer initializer. +[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static class SqlClientAutoInstrumentationBootstrap { /// Boot qyl Microsoft.Data.SqlClient auto-instrumentation for the current process. public static void Boot() - => SqlClientModuleInitializerBoot.Boot(); + => global::Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.SqlClientModuleInitializerBoot.Boot(); } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientDiagnosticListener.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientDiagnosticListener.cs index c7e74ef..1644859 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientDiagnosticListener.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientDiagnosticListener.cs @@ -2,6 +2,7 @@ using Qyl.OpenTelemetry.AutoInstrumentation; using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners; using Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.Semantics; +using Qyl.OpenTelemetry.AutoInstrumentation.Internal; namespace Qyl.OpenTelemetry.AutoInstrumentation.SqlClient; @@ -20,9 +21,27 @@ internal sealed class SqlClientDiagnosticListener : QylDiagnosticListenerSubscri /// protected override string InstrumentationId => QylAutoInstrumentationIds.SqlClient; + /// In-flight command start timestamps, keyed by the diagnostic OperationId. Entries are + /// removed on the matching After/Error event; the size cap guards against events whose completion + /// never fires (killed connections) from accumulating forever. + private static readonly System.Collections.Concurrent.ConcurrentDictionary PendingOperations = new(); + + private const int PendingOperationsCap = 10_000; + /// protected override void OnEvent(string name, object? payload) { + if (StringComparer.Ordinal.Equals(name, "Microsoft.Data.SqlClient.WriteCommandBefore")) + { + if (SqlClientPayloadReader.TryReadOperationStart(payload, out var operationId, out var startTimestamp) && + PendingOperations.Count < PendingOperationsCap) + { + PendingOperations[operationId] = startTimestamp; + } + + return; + } + var isSuccess = StringComparer.Ordinal.Equals(name, "Microsoft.Data.SqlClient.WriteCommandAfter"); var isError = StringComparer.Ordinal.Equals(name, "Microsoft.Data.SqlClient.WriteCommandError"); @@ -32,7 +51,25 @@ protected override void OnEvent(string name, object? payload) if (!SqlClientPayloadReader.TryRead(payload, isError, out var command)) return; - using var activity = QylActivitySource.Source.StartActivity(QylActivityNames.SqlClientCommand(command.Operation), ActivityKind.Client); + var duration = TimeSpan.Zero; + if (command.OperationId is { } operationKey && + PendingOperations.TryRemove(operationKey, out var beforeTimestamp) && + command.Timestamp is { } afterTimestamp && + afterTimestamp > beforeTimestamp) + { + duration = TimeSpan.FromTicks( + (long)((afterTimestamp - beforeTimestamp) * ((double)TimeSpan.TicksPerSecond / Stopwatch.Frequency))); + } + + if (QylDbActivityPolicy.HasCurrentActivityFor(command.Command)) + return; + + var endTime = TimeProvider.System.GetUtcNow(); + using var activity = QylActivitySource.StartAt( + QylActivityNames.SqlClientCommand(command.Operation), + ActivityKind.Client, + endTime - duration); + activity?.SetEndTime(endTime.UtcDateTime); SemanticTagWriter.Set(activity, SemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.DbSqlClient); SemanticTagWriter.Set(activity, SemanticAttributes.DbSystem, QylSemanticAttributes.DbSystemMicrosoftSqlServer); @@ -51,4 +88,5 @@ protected override void OnEvent(string name, object? payload) SemanticTagWriter.Set(activity, SemanticAttributes.ServerPort, command.ServerPort); DatabaseSemantics.SetError(activity, command.ErrorType); } + } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientPayloadReader.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientPayloadReader.cs index 75e7151..98babfb 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientPayloadReader.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientPayloadReader.cs @@ -9,6 +9,8 @@ internal static class SqlClientPayloadReader { private const string CommandKey = "Command"; private const string ExceptionKey = "Exception"; + private const string OperationIdKey = "OperationId"; + private const string TimestampKey = "Timestamp"; public static bool TryRead(object? payload, bool isError, out SqlClientCommand command) { @@ -25,17 +27,47 @@ public static bool TryRead(object? payload, bool isError, out SqlClientCommand c var endpoint = ParseDataSource(sqlCommand.Connection?.DataSource); command = new SqlClientCommand( + Command: sqlCommand, Namespace: NormalizeEmpty(sqlCommand.Connection?.Database), Operation: operation, QuerySummary: DatabaseSemantics.CreateSummary(operation, sqlCommand.CommandType.ToString()), QueryText: sqlCommand.CommandText, ServerAddress: endpoint.Address, ServerPort: endpoint.Port, - ErrorType: isError ? GetErrorType(exception) : null); + ErrorType: isError ? GetErrorType(exception) : null, + OperationId: TryGetPayloadStruct(payload, OperationIdKey, out var operationId) ? operationId : null, + Timestamp: TryGetPayloadStruct(payload, TimestampKey, out var timestamp) ? timestamp : null); return true; } + public static bool TryReadOperationStart(object? payload, out Guid operationId, out long timestamp) + { + timestamp = 0; + return TryGetPayloadStruct(payload, OperationIdKey, out operationId) && + TryGetPayloadStruct(payload, TimestampKey, out timestamp); + } + + private static bool TryGetPayloadStruct(object? payload, string key, out T value) + where T : struct + { + if (payload is IEnumerable> entries) + { + foreach (var entry in entries) + { + if (StringComparer.Ordinal.Equals(entry.Key, key) && + entry.Value is T matched) + { + value = matched; + return true; + } + } + } + + value = default; + return false; + } + private static bool TryGetPayloadValue(object? payload, string key, out T? value) where T : class { @@ -120,12 +152,15 @@ private static string StripProtocolPrefix(string source) } internal readonly record struct SqlClientCommand( + SqlCommand Command, string? Namespace, string? Operation, string? QuerySummary, string? QueryText, string? ServerAddress, int? ServerPort, - string? ErrorType); + string? ErrorType, + Guid? OperationId, + long? Timestamp); internal readonly record struct SqlServerEndpoint(string? Address, int? Port); diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/buildTransitive/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.Bootstrap.g.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/buildTransitive/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.Bootstrap.g.cs index ee05320..f3ec863 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/buildTransitive/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.Bootstrap.g.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/buildTransitive/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.Bootstrap.g.cs @@ -8,6 +8,6 @@ internal static class QylSqlClientAutoInstrumentationConsumerBootstrap { [ModuleInitializer] internal static void Initialize() - => SqlClientAutoInstrumentationBootstrap.Boot(); + => global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.SqlClientAutoInstrumentationBootstrap.Boot(); } #endif diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylActivityStatus.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylActivityStatus.cs index 3860835..b1db938 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylActivityStatus.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylActivityStatus.cs @@ -10,7 +10,7 @@ public static void RecordException(Activity? activity, Exception exception) if (activity is null) return; - activity.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().Name); + activity.SetTag(QylSemanticAttributes.ErrorType, exception.GetType().FullName); activity.SetStatus(ActivityStatusCode.Error); } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylCaptureHelpers.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylCaptureHelpers.cs index 1091128..bab6e1f 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylCaptureHelpers.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylCaptureHelpers.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Net; using System.Net.Http.Headers; using System.Text; using Grpc.Core; @@ -43,38 +42,6 @@ public static void SetHttpHeaders( } } - public static void SetRequestHeaders( - Activity? activity, - QylCapturedNameMap configuredHeaders, - IHeaderDictionary headers) - { - if (activity is null || configuredHeaders.Count is 0) - return; - - for (var index = 0; index < configuredHeaders.Count; index++) - { - var lookupName = configuredHeaders.GetLookupName(index); - if (headers.TryGetValue(lookupName, out var values) && values.Count > 0) - activity.SetTag(configuredHeaders.GetTagName(index), ToTagValues(values)); - } - } - - public static void SetRequestHeaders( - Activity? activity, - QylCapturedNameMap configuredHeaders, - WebHeaderCollection headers) - { - if (activity is null || configuredHeaders.Count is 0) - return; - - for (var index = 0; index < configuredHeaders.Count; index++) - { - var values = headers.GetValues(configuredHeaders.GetLookupName(index)); - if (values is { Length: > 0 }) - activity.SetTag(configuredHeaders.GetTagName(index), values); - } - } - public static void SetMetadataHeaders( Activity? activity, QylCapturedNameMap configuredMetadata, @@ -100,6 +67,22 @@ public static void SetMetadataHeaders( } } + public static void SetRequestHeaders( + Activity? activity, + QylCapturedNameMap configuredHeaders, + IHeaderDictionary headers) + { + if (activity is null || configuredHeaders.Count is 0) + return; + + for (var index = 0; index < configuredHeaders.Count; index++) + { + var lookupName = configuredHeaders.GetLookupName(index); + if (headers.TryGetValue(lookupName, out var values) && values.Count > 0) + activity.SetTag(configuredHeaders.GetTagName(index), ToTagValues(values)); + } + } + public static string RedactQuery(string url) { var queryStart = url.IndexOf('?', StringComparison.Ordinal); diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylDbActivityPolicy.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylDbActivityPolicy.cs index e5fd22b..eddf87e 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylDbActivityPolicy.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylDbActivityPolicy.cs @@ -1,24 +1,16 @@ using System.Data; using System.Data.Common; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace Qyl.OpenTelemetry.AutoInstrumentation.Internal; internal static class QylDbActivityPolicy { - public static Activity? StartEntityFrameworkCoreActivity(string operationName) - { - var activity = QylActivityFactory.StartTraceActivity( - QylAutoInstrumentationIds.EntityFrameworkCore, - QylActivityNames.DbCommand(operationName), - ActivityKind.Client, - QylInstrumentationDomains.DbEfCore); - if (activity is null) - return null; - - QylActivityTags.SetDbOperation(activity, operationName, operationName); - return activity; - } + /// Commands with an in-flight interceptor-lane activity. Weak on the command so neither + /// side outlives its natural lifetime: a command in an exporter queue is never pinned by its span, + /// and a collected command drops its entry automatically. + private static readonly ConditionalWeakTable InFlightCommands = new(); public static Activity? StartDbCommandActivity(DbCommand command, string instrumentationId, string operationName) { @@ -31,6 +23,8 @@ internal static class QylDbActivityPolicy if (activity is null) return null; + InFlightCommands.AddOrUpdate(command, activity); + QylActivityTags.SetDb( activity, GetDbSystemName(instrumentationId), @@ -45,6 +39,20 @@ internal static class QylDbActivityPolicy return activity; } + internal static bool HasCurrentActivityFor(DbCommand command) + { + if (!InFlightCommands.TryGetValue(command, out var activity)) + return false; + + if (activity.IsStopped) + { + InFlightCommands.Remove(command); + return false; + } + + return true; + } + private static string NormalizeOperation(string operationName, DbCommand command) { if (command.CommandType is CommandType.StoredProcedure) diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylDurationMetrics.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylDurationMetrics.cs deleted file mode 100644 index 2ec4445..0000000 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylDurationMetrics.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Qyl.OpenTelemetry.AutoInstrumentation.Internal; - -internal static class QylDurationMetrics -{ - public static DateTime GetHttpClientStartTimeUtc() - => QylHttpClientMetrics.IsRecordingEnabled ? TimeProvider.System.GetUtcNow().UtcDateTime : default; - - public static DateTime GetHttpClientStartTimeUtc(bool metricsEnabled) - => metricsEnabled ? TimeProvider.System.GetUtcNow().UtcDateTime : default; - - public static void RecordHttpClientDuration(DateTime startTimeUtc, string? method, int? statusCode) - => QylHttpClientMetrics.RecordRequestDuration(startTimeUtc, method, statusCode); - - public static void RecordHttpClientDurationUnchecked(DateTime startTimeUtc, string? method, int? statusCode) - => QylHttpClientMetrics.RecordRequestDurationUnchecked(startTimeUtc, method, statusCode); - - public static long GetDbClientStartTimestamp() - => QylDbClientMetrics.GetTimestamp(); - - public static bool IsDbClientRecordingEnabled(string instrumentationId) - => QylDbClientMetrics.IsRecordingEnabled(instrumentationId); - - public static void RecordDbClientDuration(long startTimestamp, string instrumentationId) - => QylDbClientMetrics.RecordDuration(startTimestamp, instrumentationId); - - public static long GetNServiceBusStartTimestamp() - => QylNServiceBusMetrics.GetTimestamp(); - - public static void RecordNServiceBusDuration(long startTimestamp, string operationName) - => QylNServiceBusMetrics.RecordDuration(startTimestamp, operationName); -} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylHttpActivityPolicy.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylHttpActivityPolicy.cs index 80ca71b..cbf5b06 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylHttpActivityPolicy.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylHttpActivityPolicy.cs @@ -70,14 +70,11 @@ public static void BackfillServerRoute(Activity activity, string method, string? public static void SetResponseStatus(Activity activity, int statusCode, int errorStatusCodeFloor) { - SetResponseStatus(activity, statusCode); + activity.SetTag(QylSemanticAttributes.HttpResponseStatusCode, statusCode); if (statusCode >= errorStatusCodeFloor) QylActivityStatus.RecordError(activity, statusCode); } - public static void SetResponseStatus(Activity activity, int statusCode) - => activity.SetTag(QylSemanticAttributes.HttpResponseStatusCode, statusCode); - private static void SetRequestMethod(Activity activity, string method, string? methodOriginal) { activity.SetTag(QylSemanticAttributes.HttpRequestMethod, method); diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylRpcActivityPolicy.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylRpcActivityPolicy.cs index ee4e942..e38a851 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylRpcActivityPolicy.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylRpcActivityPolicy.cs @@ -27,42 +27,6 @@ internal static class QylRpcActivityPolicy return activity; } - public static Activity? StartWcfClientActivity(string clientType, string methodName) - { - var activity = QylActivityFactory.StartTraceActivity( - QylAutoInstrumentationIds.WcfClient, - QylActivityNames.WcfClient, - ActivityKind.Client, - QylInstrumentationDomains.RpcWcfClient); - if (activity is null) - return null; - - QylActivityTags.SetRpc( - activity, - QylSemanticAttributes.RpcSystemDotNetWcf, - clientType, - methodName); - return activity; - } - - public static Activity? StartWcfCoreActivity(string serviceName, string contractName, string operationName) - { - var activity = QylActivityFactory.StartTraceActivity( - QylAutoInstrumentationIds.WcfCore, - QylActivityNames.CoreWcfServer, - ActivityKind.Server, - QylInstrumentationDomains.RpcWcfCore); - if (activity is null) - return null; - - QylActivityTags.SetRpc( - activity, - QylSemanticAttributes.RpcSystemDotNetWcf, - string.IsNullOrEmpty(contractName) ? serviceName : contractName, - operationName); - return activity; - } - public static void SetGrpcOkStatus(Activity activity) => activity.SetTag(QylSemanticAttributes.RpcGrpcStatusCode, QylSemanticAttributes.RpcGrpcStatusCodeOk); diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylSensitiveCapturePolicy.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylSensitiveCapturePolicy.cs index 30ed9bb..46778e8 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylSensitiveCapturePolicy.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/Internal/QylSensitiveCapturePolicy.cs @@ -51,7 +51,6 @@ private static bool ShouldCaptureDbQueryText(DbCommand command, string instrumen return instrumentationId switch { QylAutoInstrumentationIds.SqlClient => options.SqlClientSetDbStatementForText, - QylAutoInstrumentationIds.EntityFrameworkCore => options.EntityFrameworkCoreSetDbStatementForText, QylAutoInstrumentationIds.OracleMda => options.OracleMdaSetDbStatementForText, _ => false, }; diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/PublicAPI.Shipped.txt b/src/Qyl.OpenTelemetry.AutoInstrumentation/PublicAPI.Shipped.txt index 9e056bb..39f4115 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/PublicAPI.Shipped.txt +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/PublicAPI.Shipped.txt @@ -1,15 +1,11 @@ #nullable enable Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylGeneratedCodeAbi -Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAspNetCore -Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAzure Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedDbCommand Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedElastic -Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedEntityFrameworkCore Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedExternalLogger Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedGraphQl Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedGrpcNetClient Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpClient -Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpWebRequest Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedKafka Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedLogger Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedMassTransit @@ -19,43 +15,12 @@ Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedQuartz Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedRabbitMq Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedRedis Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedWcfClient -Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedWcfCore -Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters Qyl.OpenTelemetry.AutoInstrumentation.QylAspNetCoreInstrumentationServiceCollectionExtensions Qyl.OpenTelemetry.AutoInstrumentation.QylAutoInstrumentationSignal Qyl.OpenTelemetry.AutoInstrumentation.QylAutoInstrumentationSignal.Logs = 2 -> Qyl.OpenTelemetry.AutoInstrumentation.QylAutoInstrumentationSignal Qyl.OpenTelemetry.AutoInstrumentation.QylAutoInstrumentationSignal.Metrics = 1 -> Qyl.OpenTelemetry.AutoInstrumentation.QylAutoInstrumentationSignal Qyl.OpenTelemetry.AutoInstrumentation.QylAutoInstrumentationSignal.Traces = 0 -> Qyl.OpenTelemetry.AutoInstrumentation.QylAutoInstrumentationSignal -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylGeneratedCodeAbi.V6 = 6 -> int -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreAuthenticationMeterName = "Microsoft.AspNetCore.Authentication" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreAuthorizationMeterName = "Microsoft.AspNetCore.Authorization" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreComponentsLifecycleMeterName = "Microsoft.AspNetCore.Components.Lifecycle" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreComponentsMeterName = "Microsoft.AspNetCore.Components" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreComponentsServerCircuitsMeterName = "Microsoft.AspNetCore.Components.Server.Circuits" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreDiagnosticsMeterName = "Microsoft.AspNetCore.Diagnostics" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreHeaderParsingMeterName = "Microsoft.AspNetCore.HeaderParsing" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreHostingMeterName = "Microsoft.AspNetCore.Hosting" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreHttpConnectionsMeterName = "Microsoft.AspNetCore.Http.Connections" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreRateLimitingMeterName = "Microsoft.AspNetCore.RateLimiting" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreRoutingMeterName = "Microsoft.AspNetCore.Routing" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.AspNetCoreServerKestrelMeterName = "Microsoft.AspNetCore.Server.Kestrel" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.DatabaseMeterName = "Qyl.OpenTelemetry.AutoInstrumentation.Database" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.HttpClientMeterName = "System.Net.Http" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.NServiceBusIncomingPipelineMeterName = "NServiceBus.Core.Pipeline.Incoming" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.NServiceBusMeterName = "NServiceBus.Core" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.NameResolutionMeterName = "System.Net.NameResolution" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.NetRuntimeMeterName = "System.Runtime" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.NpgsqlMeterName = "Npgsql" -> string! -const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.ProcessMeterName = "System.Runtime" -> string! -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAspNetCore.InvokeAsync(Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate, Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAspNetCore.MapDelete(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAspNetCore.MapGet(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.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.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAspNetCore.MapPatch(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAspNetCore.MapPost(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAspNetCore.MapPut(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, string! pattern, Microsoft.AspNetCore.Http.RequestDelegate! requestDelegate) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAzure.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedAzure.StartActivity(string! methodName) -> System.Diagnostics.Activity? +const Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylGeneratedCodeAbi.V8 = 8 -> int static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedDbCommand.GetTimestamp() -> long static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedDbCommand.ObserveAsync(System.Threading.Tasks.Task! task, System.Diagnostics.Activity? activity, long metricStart, string! instrumentationId) -> System.Threading.Tasks.Task! static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedDbCommand.RecordDuration(long startTimestamp, string! instrumentationId) -> void @@ -65,8 +30,6 @@ static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedElastic static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedElastic.ObserveAsync(System.Threading.Tasks.Task? task, System.Diagnostics.Activity? activity) -> System.Threading.Tasks.Task! static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedElastic.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedElastic.StartActivity(string! instrumentationId, string! methodName) -> System.Diagnostics.Activity? -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedEntityFrameworkCore.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedEntityFrameworkCore.StartActivity(string! operationName) -> System.Diagnostics.Activity? static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedExternalLogger.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedExternalLogger.StartActivity(string! instrumentationId, string! domain, string! methodName, string? severityName) -> System.Diagnostics.Activity? static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedGraphQl.ObserveAsync(System.Threading.Tasks.Task? task, System.Diagnostics.Activity? activity) -> System.Threading.Tasks.Task! @@ -124,10 +87,6 @@ static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpCli static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpClient.SendAsync(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request, System.Net.Http.HttpCompletionOption completionOption) -> System.Threading.Tasks.Task! static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.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.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpClient.SendAsync(System.Net.Http.HttpClient! client, System.Net.Http.HttpRequestMessage! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpWebRequest.GetStartTimeUtc() -> System.DateTime -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpWebRequest.RecordException(System.Diagnostics.Activity? activity, System.DateTime startTimeUtc, string? method, System.Exception! exception) -> void -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpWebRequest.RecordResult(System.Diagnostics.Activity? activity, System.DateTime startTimeUtc, string? method, object? result) -> void -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpWebRequest.StartActivity(System.Net.HttpWebRequest! request, string! methodName) -> System.Diagnostics.Activity? static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedKafka.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedKafka.StartConsumerActivity() -> System.Diagnostics.Activity? static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedKafka.StartProducerActivity() -> System.Diagnostics.Activity? @@ -152,7 +111,4 @@ static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedRedis.R static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedRedis.StartCommandActivity(string! operationName) -> System.Diagnostics.Activity? static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedWcfClient.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedWcfClient.StartActivity(string! clientType, string! methodName) -> System.Diagnostics.Activity? -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedWcfCore.RecordException(System.Diagnostics.Activity? activity, System.Exception! exception) -> void -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedWcfCore.StartActivity(string! serviceName, string! contractName, string! operationName) -> System.Diagnostics.Activity? -static Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylMetricMeters.GetEnabledMeterNames() -> string![]! static Qyl.OpenTelemetry.AutoInstrumentation.QylAspNetCoreInstrumentationServiceCollectionExtensions.AddQylAspNetCoreInstrumentation(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivityNames.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivityNames.cs index efb3a79..435164e 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivityNames.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivityNames.cs @@ -11,9 +11,6 @@ internal static class QylActivityNames private const string SqlFallback = "SQL CLIENT"; /// Fixed span name for Azure operations: Azure SDK. - internal const string AzureSdk = "Azure SDK"; - /// Fixed span name for CoreWCF operations: CoreWCF SERVER. - internal const string CoreWcfServer = "CoreWCF SERVER"; /// Fixed span name for GraphQL operations: GraphQL execute. internal const string GraphQlExecute = "GraphQL execute"; /// Fixed span name for Kafka operations: Kafka message. diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs index ce9b37d..493966e 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylActivitySource.cs @@ -42,4 +42,19 @@ internal static bool IsRecordingEnabled ? Source.StartActivity(operationName, activityKind) : Source.StartActivity(operationName, activityKind, ambient.Context, tags: null, links: null, startTime: ambient.StartTimeUtc); } + + /// + /// Starts a qyl span at the operation's REAL start time, for DiagnosticListener bridges whose + /// completion payload carries the operation's own timing (EF Core CommandExecuted, + /// SqlClient before/after timestamps). Parents to the current activity for trace correlation; + /// callers stamp the matching end via before disposal. + /// + internal static Activity? StartAt(string operationName, ActivityKind activityKind, DateTimeOffset startTime) + { + if (!Source.HasListeners()) + return null; + + var ambient = Activity.Current; + return Source.StartActivity(operationName, activityKind, ambient?.Context ?? default, tags: null, links: null, startTime: startTime); + } } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreInstrumentationServiceCollectionExtensions.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreInstrumentationServiceCollectionExtensions.cs index 9440363..f998882 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreInstrumentationServiceCollectionExtensions.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreInstrumentationServiceCollectionExtensions.cs @@ -29,7 +29,7 @@ public static IServiceCollection AddQylAspNetCoreInstrumentation(this IServiceCo ArgumentNullException.ThrowIfNull(services); // Claim the ASP.NET Core signal for the middleware lane so the DiagnosticListener lane (if the // Hosting package is also referenced) defers and the server span is emitted exactly once. - QylSignalOwnership.Register(QylAutoInstrumentationIds.AspNetCore, QylSignalOwnership.GeneratedMiddleware); + QylAspNetCoreOwnership.RegisterMiddleware(); // IStartupFilters compose in registration order and this one must stay outermost so the server // span wraps the whole pipeline — call this before registering other pipeline-wrapping filters. services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreOwnership.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreOwnership.cs new file mode 100644 index 0000000..02fb187 --- /dev/null +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreOwnership.cs @@ -0,0 +1,12 @@ +namespace Qyl.OpenTelemetry.AutoInstrumentation; + +internal static class QylAspNetCoreOwnership +{ + private static int _middlewareRegistered; + + internal static bool MiddlewareRegistered + => Volatile.Read(ref _middlewareRegistered) is not 0; + + internal static void RegisterMiddleware() + => Interlocked.Exchange(ref _middlewareRegistered, 1); +} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreStartupFilter.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreStartupFilter.cs index 48e8f7f..c3870f0 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreStartupFilter.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAspNetCoreStartupFilter.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; - -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Qyl.OpenTelemetry.AutoInstrumentation.Internal; namespace Qyl.OpenTelemetry.AutoInstrumentation; @@ -19,13 +21,85 @@ internal sealed class QylAspNetCoreStartupFilter : IStartupFilter public Action Configure(Action next) => app => { - // Middleware lane (priority 90). Defers to the endpoint interceptor lane (95) when it owns the - // ASP.NET Core signal, so a consumer that has both intercepted endpoints and this middleware - // still emits exactly one server span. Wins over the DiagnosticListener lane (70). + // The middleware lane owns the ASP.NET Core signal when registered, so a DiagnosticListener + // subscriber defers and each request emits one server span. app.Use(static (context, requestDelegate) => - QylSignalOwnership.ShouldEmit(QylAutoInstrumentationIds.AspNetCore, QylSignalOwnership.GeneratedMiddleware) - ? QylInterceptedAspNetCore.InvokeAsync(requestDelegate, context) - : requestDelegate(context)); + InvokeAsync(requestDelegate, context)); next(app); }; + + private static Task InvokeAsync(RequestDelegate requestDelegate, HttpContext context) + { + var activity = StartRequestActivity(context); + try + { + return ObserveAsync(requestDelegate(context), context, activity); + } + catch (Exception exception) + { + QylActivityStatus.RecordException(activity, exception); + activity?.Dispose(); + throw; + } + } + + private static Activity? StartRequestActivity(HttpContext context) + { + var options = QylAutoInstrumentationOptions.Current; + if (!options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.AspNetCore)) + return null; + + var method = QylHttpMethod.Normalize(context.Request.Method, out var methodOriginal); + var activity = QylHttpActivityPolicy.StartServerActivity( + method, + methodOriginal, + GetRoute(context), + context.Request.Path.Value, + context.Request.QueryString.HasValue ? context.Request.QueryString.Value![1..] : null, + context.Request.Scheme); + if (activity is null) + return null; + + QylCaptureHelpers.SetRequestHeaders(activity, options.AspNetCoreCapturedRequestHeaderMap, context.Request.Headers); + return activity; + } + + private static async Task ObserveAsync(Task originalTask, HttpContext context, Activity? activity) + { + if (activity is null) + { + await originalTask.ConfigureAwait(false); + return; + } + + try + { + await originalTask.ConfigureAwait(false); + RecordResponse(activity, context); + } + catch (Exception exception) + { + QylActivityStatus.RecordException(activity, exception); + throw; + } + finally + { + activity.Dispose(); + } + } + + private static void RecordResponse(Activity activity, HttpContext context) + { + QylHttpActivityPolicy.BackfillServerRoute(activity, QylHttpMethod.Normalize(context.Request.Method), GetRoute(context)); + QylHttpActivityPolicy.SetResponseStatus(activity, context.Response.StatusCode, 500); + QylCaptureHelpers.SetRequestHeaders( + activity, + QylAutoInstrumentationOptions.Current.AspNetCoreCapturedResponseHeaderMap, + context.Response.Headers); + } + + private static string? GetRoute(HttpContext context) + => context.GetEndpoint() is RouteEndpoint routeEndpoint + ? routeEndpoint.RoutePattern.RawText + : null; } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAutoInstrumentationIds.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAutoInstrumentationIds.cs index 03c8c1b..699c75f 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAutoInstrumentationIds.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAutoInstrumentationIds.cs @@ -18,8 +18,6 @@ internal 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. @@ -40,6 +38,14 @@ internal static class QylAutoInstrumentationIds public const string Kafka = "KAFKA"; /// Well-known Mass Transit value used by qyl auto-instrumentation. public const string MassTransit = "MASSTRANSIT"; + /// Well-known Microsoft Agents AI value used by qyl auto-instrumentation. + public const string MicrosoftAgentsAi = "MICROSOFTAGENTSAI"; + /// Well-known Microsoft Agents AI Workflows value used by qyl auto-instrumentation. + public const string MicrosoftAgentsAiWorkflows = "MICROSOFTAGENTSAIWORKFLOWS"; + /// Well-known Microsoft Extensions AI value used by qyl auto-instrumentation. + public const string MicrosoftExtensionsAi = "MICROSOFTEXTENSIONSAI"; + /// Well-known Model Context Protocol value used by qyl auto-instrumentation. + public const string ModelContextProtocol = "MCP"; /// 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. @@ -70,8 +76,6 @@ internal static class QylAutoInstrumentationIds 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. diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAutoInstrumentationOptions.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAutoInstrumentationOptions.cs index cc55c01..7252515 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAutoInstrumentationOptions.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylAutoInstrumentationOptions.cs @@ -167,7 +167,6 @@ public bool HasAnyActivityInstrumentationEnabled() private static readonly string[] TraceInstrumentationIds = [ QylAutoInstrumentationIds.AdoNet, - QylAutoInstrumentationIds.AspNet, QylAutoInstrumentationIds.AspNetCore, QylAutoInstrumentationIds.Azure, QylAutoInstrumentationIds.Elasticsearch, @@ -178,6 +177,10 @@ public bool HasAnyActivityInstrumentationEnabled() QylAutoInstrumentationIds.HttpClient, QylAutoInstrumentationIds.Kafka, QylAutoInstrumentationIds.MassTransit, + QylAutoInstrumentationIds.MicrosoftAgentsAi, + QylAutoInstrumentationIds.MicrosoftAgentsAiWorkflows, + QylAutoInstrumentationIds.MicrosoftExtensionsAi, + QylAutoInstrumentationIds.ModelContextProtocol, QylAutoInstrumentationIds.MongoDb, QylAutoInstrumentationIds.MySqlConnector, QylAutoInstrumentationIds.MySqlData, @@ -191,14 +194,14 @@ public bool HasAnyActivityInstrumentationEnabled() QylAutoInstrumentationIds.StackExchangeRedis, QylAutoInstrumentationIds.WcfClient, QylAutoInstrumentationIds.WcfCore, - QylAutoInstrumentationIds.WcfService, ]; private static readonly string[] MetricInstrumentationIds = [ - QylAutoInstrumentationIds.AspNet, QylAutoInstrumentationIds.AspNetCore, QylAutoInstrumentationIds.HttpClient, + QylAutoInstrumentationIds.MicrosoftAgentsAi, + QylAutoInstrumentationIds.MicrosoftExtensionsAi, QylAutoInstrumentationIds.NetRuntime, QylAutoInstrumentationIds.Npgsql, QylAutoInstrumentationIds.NServiceBus, diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylGeneratedCodeAbi.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylGeneratedCodeAbi.cs index 5ba3547..acd41ac 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylGeneratedCodeAbi.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylGeneratedCodeAbi.cs @@ -9,6 +9,6 @@ namespace Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public static class QylGeneratedCodeAbi { - /// The 6.x generated-code ABI anchor. - public const int V6 = 6; + /// The 8.x generated-code ABI anchor. + public const int V8 = 8; } diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylHttpClientMetrics.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylHttpClientMetrics.cs deleted file mode 100644 index 2945f87..0000000 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylHttpClientMetrics.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.Metrics; - -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; - -namespace Qyl.OpenTelemetry.AutoInstrumentation; - -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 (!IsRecordingEnabled) - return; - - RecordRequestDurationUnchecked(startTimeUtc, method, statusCode); - } - - internal static void RecordRequestDurationUnchecked(DateTime startTimeUtc, string? method, int? statusCode) - { - var elapsed = TimeProvider.System.GetUtcNow().UtcDateTime - startTimeUtc; - if (elapsed.TotalSeconds >= 0) - { - var tags = new TagList - { - { QylSemanticAttributes.HttpRequestMethod, QylHttpMethod.Normalize(method) }, - }; - - if (statusCode is { } code) - tags.Add(QylSemanticAttributes.HttpResponseStatusCode, code); - - RequestDuration.Record(elapsed.TotalSeconds, in tags); - } - } -} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentation.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentation.cs index 7ad08a1..df42c60 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentation.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentation.cs @@ -36,6 +36,13 @@ public static bool Activate() // owns telemetry in a zero-code app, so take the raw URI and redact it itself. AppContext.SetSwitch("System.Net.Http.DisableUriRedaction", true); + if (QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled( + QylAutoInstrumentationSignal.Traces, + QylAutoInstrumentationIds.Azure)) + { + AppContext.SetSwitch("Azure.Experimental.EnableActivitySource", true); + } + QylRuntimeProcessMetrics.Initialize(); return true; diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentationDomains.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentationDomains.cs index 6c8310a..fbeab88 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentationDomains.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInstrumentationDomains.cs @@ -28,8 +28,6 @@ internal static class QylInstrumentationDomains 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. @@ -50,6 +48,4 @@ internal static class QylInstrumentationDomains 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.OpenTelemetry.AutoInstrumentation/QylInterceptedAspNetCore.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAspNetCore.cs deleted file mode 100644 index 7013be5..0000000 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAspNetCore.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Diagnostics; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Qyl.OpenTelemetry.AutoInstrumentation.Internal; - -namespace Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; - -/// 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. -[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] -public static class QylInterceptedAspNetCore -{ - /// 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) - throw new NullReferenceException(); - - if (context is null) - return requestDelegate(context!); - - var activity = StartRequestActivity(context); - try - { - return ObserveAsync(requestDelegate(context), context, activity); - } - catch (Exception exception) - { - RecordException(activity, exception); - activity?.Dispose(); - throw; - } - } - - private static Activity? StartRequestActivity(HttpContext context) - { - var options = QylAutoInstrumentationOptions.Current; - if (!options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.AspNetCore)) - return null; - - var method = QylHttpMethod.Normalize(context.Request.Method, out var methodOriginal); - var route = GetRoute(context); - var activity = QylHttpActivityPolicy.StartServerActivity( - method, - methodOriginal, - route, - context.Request.Path.Value, - context.Request.QueryString.HasValue ? context.Request.QueryString.Value![1..] : null, - context.Request.Scheme); - if (activity is null) - return null; - - QylCaptureHelpers.SetRequestHeaders(activity, options.AspNetCoreCapturedRequestHeaderMap, context.Request.Headers); - return activity; - } - - private static async Task ObserveAsync(Task originalTask, HttpContext context, Activity? activity) - { - if (activity is null) - { - await originalTask.ConfigureAwait(false); - return; - } - - try - { - await originalTask.ConfigureAwait(false); - RecordResponse(activity, context); - } - catch (Exception exception) - { - RecordException(activity, exception); - throw; - } - finally - { - activity.Dispose(); - } - } - - private static void RecordException(Activity? activity, Exception exception) - { - QylActivityStatus.RecordException(activity, exception); - } - - private static void RecordResponse(Activity? activity, HttpContext context) - { - if (activity is null) - return; - - // The server-span middleware can run outside routing (registered via IStartupFilter); the route - // template is only known once the pipeline has resolved the endpoint, so backfill it here. - QylHttpActivityPolicy.BackfillServerRoute(activity, QylHttpMethod.Normalize(context.Request.Method), GetRoute(context)); - QylHttpActivityPolicy.SetResponseStatus(activity, context.Response.StatusCode, 500); - QylCaptureHelpers.SetRequestHeaders( - activity, - QylAutoInstrumentationOptions.Current.AspNetCoreCapturedResponseHeaderMap, - context.Response.Headers); - } - - private static RequestDelegate Observe(RequestDelegate requestDelegate) - { - // The endpoint interceptor lane owns the ASP.NET Core server signal (priority 95). Registered here - // at endpoint-mapping time (before requests) so the middleware (90) and DiagnosticListener (70) - // lanes defer — exactly one server span per request. Registration is NOT done from InvokeAsync, - // which the middleware also calls; only actually-intercepted endpoints claim the interceptor lane. - QylSignalOwnership.Register(QylAutoInstrumentationIds.AspNetCore, QylSignalOwnership.Interceptor); - return requestDelegate is null ? null! : context => InvokeAsync(requestDelegate, context); - } - - private static string? GetRoute(HttpContext context) - => context.GetEndpoint() is RouteEndpoint routeEndpoint - ? routeEndpoint.RoutePattern.RawText - : null; -} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAzure.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAzure.cs deleted file mode 100644 index ea6ba21..0000000 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedAzure.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Diagnostics; -using Qyl.OpenTelemetry.AutoInstrumentation.Internal; - -namespace Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; - -/// 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. -[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] -public static class QylInterceptedAzure -{ - - /// Runs the Start Activity runtime helper used by source-generated qyl interceptors. - public static Activity? StartActivity(string methodName) - { - var activity = QylActivityFactory.StartTraceActivity( - QylAutoInstrumentationIds.Azure, - QylActivityNames.AzureSdk, - ActivityKind.Client, - QylInstrumentationDomains.AzureSdk); - // The span name stays the stable low-cardinality "Azure SDK"; the specific method goes - // on code.function.name. - if (activity is not null && !string.IsNullOrEmpty(methodName)) - activity.SetTag(QylSemanticAttributes.CodeFunctionName, methodName); - return activity; - } - - /// Runs the Record Exception runtime helper used by source-generated qyl interceptors. - public static void RecordException(Activity? activity, Exception exception) - { - QylActivityStatus.RecordException(activity, exception); - } -} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedDbCommand.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedDbCommand.cs index c0c4114..db368af 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedDbCommand.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedDbCommand.cs @@ -22,11 +22,11 @@ public static class QylInterceptedDbCommand /// Runs the Get Timestamp runtime helper used by source-generated qyl interceptors. public static long GetTimestamp() - => QylDurationMetrics.GetDbClientStartTimestamp(); + => QylDbClientMetrics.GetTimestamp(); /// Runs the Record Duration runtime helper used by source-generated qyl interceptors. public static void RecordDuration(long startTimestamp, string instrumentationId) - => QylDurationMetrics.RecordDbClientDuration(startTimestamp, instrumentationId); + => QylDbClientMetrics.RecordDuration(startTimestamp, 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) @@ -34,7 +34,7 @@ public static Task ObserveAsync(Task task, Activity? activity, long met ArgumentNullException.ThrowIfNull(task); ArgumentNullException.ThrowIfNull(instrumentationId); - if (activity is null && !QylDurationMetrics.IsDbClientRecordingEnabled(instrumentationId)) + if (activity is null && !QylDbClientMetrics.IsRecordingEnabled(instrumentationId)) return task; return ObserveSlowAsync(task, activity, metricStart, instrumentationId); @@ -45,13 +45,13 @@ private static async Task ObserveSlowAsync(Task task, Activity? activit try { var result = await task.ConfigureAwait(false); - QylDurationMetrics.RecordDbClientDuration(metricStart, instrumentationId); + QylDbClientMetrics.RecordDuration(metricStart, instrumentationId); return result; } catch (Exception exception) { RecordException(activity, exception); - QylDurationMetrics.RecordDbClientDuration(metricStart, instrumentationId); + QylDbClientMetrics.RecordDuration(metricStart, instrumentationId); throw; } finally diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedEntityFrameworkCore.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedEntityFrameworkCore.cs deleted file mode 100644 index 16e1085..0000000 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedEntityFrameworkCore.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Diagnostics; -using Qyl.OpenTelemetry.AutoInstrumentation.Internal; - -namespace Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; - -/// 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. -[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] -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); - - return QylDbActivityPolicy.StartEntityFrameworkCoreActivity(operationName); - } - - /// Runs the Record Exception runtime helper used by source-generated qyl interceptors. - public static void RecordException(Activity? activity, Exception exception) - { - QylActivityStatus.RecordException(activity, exception); - } -} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpClient.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpClient.cs index ffd5056..6ecc9d1 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpClient.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpClient.cs @@ -477,7 +477,6 @@ private static Task ObserveValueAsync(Task originalTask, HttpClientObse if (!originalTask.IsCompletedSuccessfully) return ObserveValueSlowAsync(originalTask, observation); - RecordSuccess(observation); observation.Dispose(); return originalTask; } @@ -486,9 +485,7 @@ private static async Task ObserveValueSlowAsync(Task originalTask, Http { try { - var result = await originalTask.ConfigureAwait(false); - RecordSuccess(observation); - return result; + return await originalTask.ConfigureAwait(false); } catch (Exception exception) { @@ -503,14 +500,12 @@ private static async Task ObserveValueSlowAsync(Task originalTask, Http private static HttpClientObservation StartHttpClientObservation(HttpClient client, HttpRequestMessage request) { - if (!TryGetHttpClientObservationOptions(out var options, out var traceEnabled, out var metricsEnabled)) + if (!TryGetHttpClientObservationOptions(out var options, out var traceEnabled)) return default; var observation = StartHttpClientObservation( client, - options, traceEnabled, - metricsEnabled, request.Method.Method, request.RequestUri, rawRequestUri: null); @@ -530,14 +525,14 @@ private static HttpClientObservation StartHttpClientObservation( string? requestUri, HttpContent? content = null) { - if (!TryGetHttpClientObservationOptions(out var options, out var traceEnabled, out var metricsEnabled)) + if (!TryGetHttpClientObservationOptions(out var options, out var traceEnabled)) return default; Uri? uri = null; if (traceEnabled && !string.IsNullOrWhiteSpace(requestUri)) Uri.TryCreate(requestUri, UriKind.RelativeOrAbsolute, out uri); - var observation = StartHttpClientObservation(client, options, traceEnabled, metricsEnabled, method, uri, requestUri); + var observation = StartHttpClientObservation(client, traceEnabled, method, uri, requestUri); SetConvenienceRequestHeaders(observation.Activity, options, client, content); return observation; } @@ -556,37 +551,34 @@ private static HttpClientObservation StartHttpClientObservation( string? rawRequestUri, HttpContent? content = null) { - if (!TryGetHttpClientObservationOptions(out var options, out var traceEnabled, out var metricsEnabled)) + if (!TryGetHttpClientObservationOptions(out var options, out var traceEnabled)) return default; - var observation = StartHttpClientObservation(client, options, traceEnabled, metricsEnabled, method, requestUri, rawRequestUri); + var observation = StartHttpClientObservation(client, traceEnabled, method, requestUri, rawRequestUri); SetConvenienceRequestHeaders(observation.Activity, options, client, content); return observation; } + // The forwarded call reaches the real HttpClient, whose native System.Net.Http meter owns + // http.client.request.duration; the interceptor lane records no duplicate qyl instrument. private static bool TryGetHttpClientObservationOptions( out QylAutoInstrumentationOptions options, - out bool traceEnabled, - out bool metricsEnabled) + out bool traceEnabled) { options = QylAutoInstrumentationOptions.Current; traceEnabled = QylActivitySource.IsRecordingEnabled && options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.HttpClient); - metricsEnabled = QylHttpClientMetrics.IsRecordingEnabledFor(options); - return traceEnabled || metricsEnabled; + return traceEnabled; } private static HttpClientObservation StartHttpClientObservation( HttpClient client, - QylAutoInstrumentationOptions options, bool traceEnabled, - bool metricsEnabled, string method, Uri? requestUri, string? rawRequestUri) { method = QylHttpMethod.Normalize(method, out var methodOriginal); - var startTimeUtc = QylDurationMetrics.GetHttpClientStartTimeUtc(metricsEnabled); Activity? activity = null; if (traceEnabled) @@ -600,7 +592,7 @@ private static HttpClientObservation StartHttpClientObservation( rawRequestUri); } - return new HttpClientObservation(activity, startTimeUtc, method, metricsEnabled); + return new HttpClientObservation(activity); } private static Uri? ResolveRequestUri(HttpClient client, Uri? requestUri) @@ -639,13 +631,6 @@ private static void RecordResponse(HttpClientObservation observation, HttpRespon response.Content?.Headers); } - - RecordDuration(observation, statusCode); - } - - private static void RecordSuccess(HttpClientObservation observation) - { - RecordDuration(observation, null); } private static void ThrowIfInvalidCallTarget(HttpClient client, HttpRequestMessage request) @@ -670,7 +655,6 @@ private static void RecordException(HttpClientObservation observation, Exception var activity = observation.Activity; QylActivityStatus.RecordException(activity, exception); - RecordDuration(observation, null); } private static void RecordResponseStatusException(HttpClientObservation observation, int statusCode) @@ -680,29 +664,12 @@ private static void RecordResponseStatusException(HttpClientObservation observat { QylHttpActivityPolicy.SetResponseStatus(activity, statusCode, 400); } - - RecordDuration(observation, statusCode); - } - - private static void RecordDuration(HttpClientObservation observation, int? statusCode) - { - if (!observation.RecordMetrics) - return; - - QylDurationMetrics.RecordHttpClientDurationUnchecked( - observation.StartTimeUtc, - observation.Method, - statusCode); } - private readonly record struct HttpClientObservation( - Activity? Activity, - DateTime StartTimeUtc, - string? Method, - bool RecordMetrics) : IDisposable + private readonly record struct HttpClientObservation(Activity? Activity) : IDisposable { /// Well-known Is Enabled value used by qyl auto-instrumentation. - public bool IsEnabled => Activity is not null || RecordMetrics; + public bool IsEnabled => Activity is not null; /// Runs the Dispose runtime helper used by source-generated qyl interceptors. public void Dispose() diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpWebRequest.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpWebRequest.cs deleted file mode 100644 index bf1f40c..0000000 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedHttpWebRequest.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Diagnostics; -using System.Net; -using Qyl.OpenTelemetry.AutoInstrumentation.Internal; - -namespace Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; - -/// 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. -[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] -public static class QylInterceptedHttpWebRequest -{ - - /// Runs the Get Start Time Utc runtime helper used by source-generated qyl interceptors. - public static DateTime GetStartTimeUtc() - => QylDurationMetrics.GetHttpClientStartTimeUtc(); - - /// Runs the Start Activity runtime helper used by source-generated qyl interceptors. - public static Activity? StartActivity(HttpWebRequest request, string methodName) - { - var options = QylAutoInstrumentationOptions.Current; - if (!options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Traces, QylAutoInstrumentationIds.HttpClient)) - return null; - - var method = QylHttpMethod.Normalize(request.Method, out var methodOriginal); - var activity = QylHttpActivityPolicy.StartClientActivity( - QylInstrumentationDomains.HttpWebRequest, - method, - methodOriginal, - request.RequestUri, - request.RequestUri?.ToString()); - if (activity is null) - return null; - - QylCaptureHelpers.SetRequestHeaders(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; - var response = result as HttpWebResponse; - if (response is not null) - statusCode = RecordResponse(activity, response, markErrorForStatus: true); - - 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; - if (exception is WebException { Response: HttpWebResponse response }) - statusCode = RecordResponse(activity, response, markErrorForStatus: false); - - QylActivityStatus.RecordException(activity, exception); - RecordDuration(startTimeUtc, method, statusCode); - } - - private static int RecordResponse(Activity? activity, HttpWebResponse response, bool markErrorForStatus) - { - var statusCode = (int)response.StatusCode; - if (activity is not null) - { - QylHttpActivityPolicy.SetResponseStatus(activity, statusCode, markErrorForStatus ? 400 : int.MaxValue); - QylCaptureHelpers.SetRequestHeaders( - activity, - QylAutoInstrumentationOptions.Current.HttpClientCapturedResponseHeaderMap, - response.Headers); - } - - return statusCode; - } - - private static void RecordDuration(DateTime startTimeUtc, string? method, int? statusCode) - { - if (startTimeUtc == default) - return; - - QylDurationMetrics.RecordHttpClientDuration( - startTimeUtc, - method, - statusCode); - } - -} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedNServiceBus.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedNServiceBus.cs index 0be16bb..cb9edd7 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedNServiceBus.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedNServiceBus.cs @@ -15,11 +15,11 @@ public static class QylInterceptedNServiceBus /// Runs the Get Timestamp runtime helper used by source-generated qyl interceptors. public static long GetTimestamp() - => QylDurationMetrics.GetNServiceBusStartTimestamp(); + => QylNServiceBusMetrics.GetTimestamp(); /// Runs the Record Duration runtime helper used by source-generated qyl interceptors. public static void RecordDuration(long startTimestamp, string operationName) - => QylDurationMetrics.RecordNServiceBusDuration(startTimestamp, operationName); + => QylNServiceBusMetrics.RecordDuration(startTimestamp, operationName); /// Runs the Record Exception runtime helper used by source-generated qyl interceptors. public static void RecordException(Activity? activity, Exception exception) diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedWcfClient.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedWcfClient.cs index 69034b6..f6c85c7 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedWcfClient.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedWcfClient.cs @@ -11,7 +11,22 @@ public static class QylInterceptedWcfClient /// Runs the Start Activity runtime helper used by source-generated qyl interceptors. public static Activity? StartActivity(string clientType, string methodName) - => QylRpcActivityPolicy.StartWcfClientActivity(clientType, methodName); + { + var activity = QylActivityFactory.StartTraceActivity( + QylAutoInstrumentationIds.WcfClient, + QylActivityNames.WcfClient, + ActivityKind.Client, + QylInstrumentationDomains.RpcWcfClient); + if (activity is null) + return null; + + QylActivityTags.SetRpc( + activity, + QylSemanticAttributes.RpcSystemDotNetWcf, + clientType, + methodName); + return activity; + } /// Runs the Record Exception runtime helper used by source-generated qyl interceptors. public static void RecordException(Activity? activity, Exception exception) diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedWcfCore.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedWcfCore.cs deleted file mode 100644 index ec801b7..0000000 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylInterceptedWcfCore.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Diagnostics; -using Qyl.OpenTelemetry.AutoInstrumentation.Internal; - -namespace Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; - -/// 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. -[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] -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) - => QylRpcActivityPolicy.StartWcfCoreActivity(serviceName, contractName, operationName); - - /// Runs the Record Exception runtime helper used by source-generated qyl interceptors. - public static void RecordException(Activity? activity, Exception exception) - { - QylActivityStatus.RecordException(activity, exception); - } -} diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricMeters.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricMeters.cs index 90d4ae9..a09d12f 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricMeters.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricMeters.cs @@ -1,56 +1,51 @@ -namespace Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; +namespace Qyl.OpenTelemetry.AutoInstrumentation; -/// -/// Meter names required by the metrics side of the auto-instrumentation contract. -/// These are registration targets for source-visible MeterProviderBuilder.AddMeter(...) -/// interception; they do not create instruments by themselves. -/// -[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] -public static class QylMetricMeters +internal static class QylMetricMeters { /// Well-known ASP.NET Core Hosting Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreHostingMeterName = "Microsoft.AspNetCore.Hosting"; + internal const string AspNetCoreHostingMeterName = "Microsoft.AspNetCore.Hosting"; /// Well-known ASP.NET Core Routing Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreRoutingMeterName = "Microsoft.AspNetCore.Routing"; + internal const string AspNetCoreRoutingMeterName = "Microsoft.AspNetCore.Routing"; /// Well-known ASP.NET Core Diagnostics Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreDiagnosticsMeterName = "Microsoft.AspNetCore.Diagnostics"; + internal const string AspNetCoreDiagnosticsMeterName = "Microsoft.AspNetCore.Diagnostics"; /// Well-known ASP.NET Core Rate Limiting Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreRateLimitingMeterName = "Microsoft.AspNetCore.RateLimiting"; + internal const string AspNetCoreRateLimitingMeterName = "Microsoft.AspNetCore.RateLimiting"; /// Well-known ASP.NET Core Header Parsing Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreHeaderParsingMeterName = "Microsoft.AspNetCore.HeaderParsing"; + internal const string AspNetCoreHeaderParsingMeterName = "Microsoft.AspNetCore.HeaderParsing"; /// Well-known ASP.NET Core Kestrel Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreServerKestrelMeterName = "Microsoft.AspNetCore.Server.Kestrel"; + internal const string AspNetCoreServerKestrelMeterName = "Microsoft.AspNetCore.Server.Kestrel"; /// Well-known ASP.NET Core SignalR HTTP Connections Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreHttpConnectionsMeterName = "Microsoft.AspNetCore.Http.Connections"; + internal const string AspNetCoreHttpConnectionsMeterName = "Microsoft.AspNetCore.Http.Connections"; /// Well-known ASP.NET Core Authorization Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreAuthorizationMeterName = "Microsoft.AspNetCore.Authorization"; + internal const string AspNetCoreAuthorizationMeterName = "Microsoft.AspNetCore.Authorization"; /// Well-known ASP.NET Core Authentication Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreAuthenticationMeterName = "Microsoft.AspNetCore.Authentication"; + internal const string AspNetCoreAuthenticationMeterName = "Microsoft.AspNetCore.Authentication"; /// Well-known ASP.NET Core Components Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreComponentsMeterName = "Microsoft.AspNetCore.Components"; + internal const string AspNetCoreComponentsMeterName = "Microsoft.AspNetCore.Components"; /// Well-known ASP.NET Core Components Lifecycle Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreComponentsLifecycleMeterName = "Microsoft.AspNetCore.Components.Lifecycle"; + internal const string AspNetCoreComponentsLifecycleMeterName = "Microsoft.AspNetCore.Components.Lifecycle"; /// Well-known ASP.NET Core Components Server Circuits Meter Name value used by qyl auto-instrumentation. - public const string AspNetCoreComponentsServerCircuitsMeterName = "Microsoft.AspNetCore.Components.Server.Circuits"; + internal const string AspNetCoreComponentsServerCircuitsMeterName = "Microsoft.AspNetCore.Components.Server.Circuits"; /// Well-known HTTP Client Meter Name value used by qyl auto-instrumentation. - public const string HttpClientMeterName = "System.Net.Http"; + internal const string HttpClientMeterName = "System.Net.Http"; /// Well-known System.Net DNS name resolution Meter Name value used by qyl auto-instrumentation. - public const string NameResolutionMeterName = "System.Net.NameResolution"; + internal const string NameResolutionMeterName = "System.Net.NameResolution"; /// Well-known Database Meter Name value used by qyl auto-instrumentation. - public const string DatabaseMeterName = "Qyl.OpenTelemetry.AutoInstrumentation.Database"; - /// Well-known Npgsql Meter Name value used by qyl auto-instrumentation. - public const string NpgsqlMeterName = "Npgsql"; + internal const string DatabaseMeterName = "Qyl.OpenTelemetry.AutoInstrumentation.Database"; + /// Npgsql's library-native Meter, carrying its connection-pool and command instruments. + internal const string NpgsqlNativeMeterName = "Npgsql"; /// Well-known N Service Bus Meter Name value used by qyl auto-instrumentation. - public const string NServiceBusMeterName = "NServiceBus.Core"; - /// Well-known NServiceBus incoming pipeline Meter Name value used by qyl auto-instrumentation. - public const string NServiceBusIncomingPipelineMeterName = "NServiceBus.Core.Pipeline.Incoming"; + internal const string NServiceBusMeterName = "Qyl.OpenTelemetry.AutoInstrumentation.NServiceBus"; + /// NServiceBus's library-native core Meter. + internal const string NServiceBusNativeMeterName = "NServiceBus.Core"; + /// NServiceBus's library-native incoming-pipeline Meter. + internal const string NServiceBusNativeIncomingPipelineMeterName = "NServiceBus.Core.Pipeline.Incoming"; /// Well-known Net Runtime Meter Name value used by qyl auto-instrumentation. - public const string NetRuntimeMeterName = "System.Runtime"; + internal const string NetRuntimeMeterName = "System.Runtime"; /// Well-known Process Meter Name value used by qyl auto-instrumentation. - public const string ProcessMeterName = "System.Runtime"; + internal const string ProcessMeterName = "System.Runtime"; - /// Runs the Get Enabled Meter Names runtime helper used by source-generated qyl interceptors. - public static string[] GetEnabledMeterNames() + internal static string[] GetEnabledMeterNames() { var options = QylAutoInstrumentationOptions.Current; var names = new List(20); @@ -79,10 +74,7 @@ public static string[] GetEnabledMeterNames() var databaseMeterEnabled = false; if (options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.Npgsql)) - { - names.Add(NpgsqlMeterName); databaseMeterEnabled = true; - } if (options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.SqlClient)) databaseMeterEnabled = true; @@ -91,10 +83,7 @@ public static string[] GetEnabledMeterNames() names.Add(DatabaseMeterName); if (options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.NServiceBus)) - { names.Add(NServiceBusMeterName); - names.Add(NServiceBusIncomingPipelineMeterName); - } if (options.IsInstrumentationEnabled(QylAutoInstrumentationSignal.Metrics, QylAutoInstrumentationIds.NetRuntime)) names.Add(NetRuntimeMeterName); diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricNames.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricNames.cs index be2a393..7ffc5ed 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricNames.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricNames.cs @@ -4,25 +4,8 @@ namespace Qyl.OpenTelemetry.AutoInstrumentation; /// This runtime surface is NativeAOT-compatible and is consumed by source-generated interceptors without runtime IL rewriting, profiler attach, or reflection discovery. internal static class QylMetricNames { - /// Well-known HTTP Server Request Duration value used by qyl auto-instrumentation. - public const string HttpServerRequestDuration = "http.server.request.duration"; - /// Well-known ASP.NET Core Components Navigate value used by qyl auto-instrumentation. - public const string AspNetCoreComponentsNavigate = "aspnetcore.components.navigate"; - /// Well-known ASP.NET Core Components Handle Event Duration value used by qyl auto-instrumentation. - public const string AspNetCoreComponentsHandleEventDuration = "aspnetcore.components.handle_event.duration"; - /// Well-known ASP.NET Core Components Update Parameters Duration value used by qyl auto-instrumentation. - public const string AspNetCoreComponentsUpdateParametersDuration = "aspnetcore.components.update_parameters.duration"; - /// Well-known ASP.NET Core Components Render Diff Duration value used by qyl auto-instrumentation. - public const string AspNetCoreComponentsRenderDiffDuration = "aspnetcore.components.render_diff.duration"; - /// Well-known ASP.NET Core Components Render Diff Size value used by qyl auto-instrumentation. - public const string AspNetCoreComponentsRenderDiffSize = "aspnetcore.components.render_diff.size"; - /// 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 DNS Lookup Duration value used by qyl auto-instrumentation. - public const string DnsLookupDuration = "dns.lookup.duration"; /// Well-known N Service Bus Messaging Operation Duration value used by qyl auto-instrumentation. public const string NServiceBusMessagingOperationDuration = "nservicebus.messaging.operation.duration"; diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylNServiceBusMetrics.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylNServiceBusMetrics.cs index 3a508e8..e192dd9 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylNServiceBusMetrics.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylNServiceBusMetrics.cs @@ -1,7 +1,5 @@ using System.Diagnostics.Metrics; -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; - namespace Qyl.OpenTelemetry.AutoInstrumentation; /// Defines the qyl auto-instrumentation surface for qyl N Service Bus Metrics. diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs index 43f07fd..78a723c 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylRuntimeProcessMetrics.cs @@ -1,8 +1,6 @@ using System.Diagnostics; using System.Diagnostics.Metrics; -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; - namespace Qyl.OpenTelemetry.AutoInstrumentation; internal static class QylRuntimeProcessMetrics diff --git a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylSemanticAttributes.cs b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylSemanticAttributes.cs index df3ea29..0845053 100644 --- a/src/Qyl.OpenTelemetry.AutoInstrumentation/QylSemanticAttributes.cs +++ b/src/Qyl.OpenTelemetry.AutoInstrumentation/QylSemanticAttributes.cs @@ -2,6 +2,7 @@ namespace Qyl.OpenTelemetry.AutoInstrumentation; using DbAttributes = Qyl.OpenTelemetry.SemanticConventions.Attributes.Db.DbAttributes; using DbIncubatingAttributes = Qyl.OpenTelemetry.SemanticConventions.Incubating.Attributes.Db.DbAttributes; +using CodeAttributes = Qyl.OpenTelemetry.SemanticConventions.Attributes.Code.CodeAttributes; using CpuAttributes = Qyl.OpenTelemetry.SemanticConventions.Incubating.Attributes.Cpu.CpuAttributes; using DotnetAttributes = Qyl.OpenTelemetry.SemanticConventions.Attributes.Dotnet.DotnetAttributes; using ErrorAttributes = Qyl.OpenTelemetry.SemanticConventions.Attributes.Error.ErrorAttributes; @@ -58,9 +59,9 @@ internal static class QylSemanticAttributes /// Well-known Url Full value used by qyl auto-instrumentation. public const string UrlFull = UrlAttributes.Full; /// Well-known Url Scheme value used by qyl auto-instrumentation. - public const string UrlScheme = "url.scheme"; + public const string UrlScheme = UrlAttributes.Scheme; /// Well-known Code Function Name value used by qyl auto-instrumentation. - public const string CodeFunctionName = "code.function.name"; + public const string CodeFunctionName = CodeAttributes.FunctionName; /// Well-known Dotnet Gc Heap Generation value used by qyl auto-instrumentation. public const string DotnetGcHeapGeneration = DotnetAttributes.GcHeapGeneration; diff --git a/src/Qyl.Sdk/Qyl.Sdk.csproj b/src/Qyl.Sdk/Qyl.Sdk.csproj index ca2eddf..d138029 100644 --- a/src/Qyl.Sdk/Qyl.Sdk.csproj +++ b/src/Qyl.Sdk/Qyl.Sdk.csproj @@ -5,7 +5,7 @@ Qyl Qyl.Sdk Qyl.Sdk - One-line qyl onboarding for .NET applications: builder.AddQyl() wires the OpenTelemetry SDK, the qyl auto-instrumentation listeners, GenAI sources, OTLP export with local collector auto-discovery, and session propagation. + One-line qyl onboarding for .NET applications: builder.AddQyl() wires the OpenTelemetry SDK, qyl listeners, version-pinned GenAI, Azure SDK, MCP and CoreWCF sources, OTLP export with local collector discovery, and session propagation. @@ -14,7 +14,9 @@ - +
diff --git a/src/Qyl.Sdk/QylAzureSpanProcessor.cs b/src/Qyl.Sdk/QylAzureSpanProcessor.cs new file mode 100644 index 0000000..6dc81dd --- /dev/null +++ b/src/Qyl.Sdk/QylAzureSpanProcessor.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; +using OpenTelemetry; +using Qyl.OpenTelemetry.AutoInstrumentation; + +namespace Qyl; + +internal sealed class QylAzureSpanProcessor : BaseProcessor +{ + public override void OnEnd(Activity data) + { + if (!data.Source.Name.StartsWith("Azure.", StringComparison.Ordinal)) + return; + + data.SetTag(QylSemanticAttributes.QylInstrumentationDomain, QylInstrumentationDomains.AzureSdk); + data.SetTag(QylSemanticAttributes.UrlFull, null); + data.SetTag(QylSemanticAttributes.UrlPath, null); + + if (data.Status is ActivityStatusCode.Error) + { + var exceptionType = data.GetTagItem(QylSemanticAttributes.ErrorType) as string + ?? FindExceptionType(data); + if (exceptionType is not null) + data.SetTag(QylSemanticAttributes.ErrorType, GetSimpleTypeName(exceptionType)); + } + } + + private static string? FindExceptionType(Activity activity) + { + foreach (var activityEvent in activity.Events) + { + foreach (var tag in activityEvent.Tags) + { + if (StringComparer.Ordinal.Equals(tag.Key, "exception.type") && tag.Value is string exceptionType) + return exceptionType; + } + } + + return null; + } + + private static string GetSimpleTypeName(string typeName) + { + var separator = typeName.LastIndexOf('.'); + return separator >= 0 ? typeName[(separator + 1)..] : typeName; + } +} diff --git a/src/Qyl.Sdk/QylSdkHostApplicationBuilderExtensions.cs b/src/Qyl.Sdk/QylSdkHostApplicationBuilderExtensions.cs index ac06e35..01924c4 100644 --- a/src/Qyl.Sdk/QylSdkHostApplicationBuilderExtensions.cs +++ b/src/Qyl.Sdk/QylSdkHostApplicationBuilderExtensions.cs @@ -8,19 +8,20 @@ using OpenTelemetry.Trace; using Qyl.OpenTelemetry.AutoInstrumentation; using Qyl.OpenTelemetry.AutoInstrumentation.Hosting; -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; namespace Qyl; /// /// The one-line qyl onboarding surface: builder.AddQyl() activates the qyl -/// auto-instrumentation listeners, wires the OpenTelemetry SDK with the qyl, ASP.NET Core, -/// HttpClient, GenAI, and MCP sources plus the full qyl meter inventory, propagates +/// auto-instrumentation listeners, wires the OpenTelemetry SDK with qyl-owned ASP.NET Core and +/// HttpClient spans, version-pinned GenAI, Azure SDK, MCP, and CoreWCF sources plus the native and +/// qyl-owned meter inventory, propagates /// session.id across traces, and exports traces, metrics, and logs over OTLP — to /// OTEL_EXPORTER_OTLP_ENDPOINT when set, otherwise to a locally discovered qyl collector. -/// The qyl DiagnosticListeners emit EF Core, SqlClient, and gRPC-client spans under the single -/// qyl ActivitySource, so those integrations need no extra source registration here (and adding -/// the libraries' native sources would double-report them). +/// The specialist EF Core and SqlClient packages and the gRPC-client listener emit spans under +/// the single qyl ActivitySource, so they need no extra source registration here. Applications +/// still install the specialist package for each dependency-heavy database integration; adding +/// competing native sources would double-report the same operations. /// public static class QylSdkHostApplicationBuilderExtensions { @@ -28,10 +29,11 @@ public static class QylSdkHostApplicationBuilderExtensions /// Activate qyl instrumentation, session propagation, and OTLP export. /// - /// GenAI telemetry additionally requires opting the agent itself in: + /// Wrapper-based GenAI telemetry additionally requires opting the library itself in: /// agent.AsBuilder().UseOpenTelemetry().Build() for Microsoft.Agents.AI, or /// chatClient.AsBuilder().UseOpenTelemetry().Build() for a bare IChatClient. The - /// sources are pre-registered here so that single line is all that's left. + /// Workflows use WorkflowBuilder.WithOpenTelemetry(). MCP and CoreWCF emit from + /// their official SDK paths without a separate telemetry wrapper. /// public static IHostApplicationBuilder AddQyl( this IHostApplicationBuilder builder, @@ -62,17 +64,14 @@ public static IHostApplicationBuilder AddQyl( .ConfigureResource(resource => resource.AddService(serviceName)) .WithTracing(tracing => { - tracing.AddSource( - QylActivitySource.Name, - "Microsoft.AspNetCore", - "System.Net.Http", - "Experimental.Microsoft.Agents.AI", - "Experimental.Microsoft.Extensions.AI", - "Experimental.ModelContextProtocol"); + tracing.AddSource(QylTelemetrySources.GetEnabledActivitySourceNames()); foreach (var source in options.AdditionalSources) tracing.AddSource(source); + if (QylTelemetrySources.IsAzureTracingEnabled()) + tracing.AddProcessor(new QylAzureSpanProcessor()); + if (options.EnableSessionPropagation) tracing.AddProcessor(new QylSessionSpanProcessor()); @@ -88,10 +87,7 @@ public static IHostApplicationBuilder AddQyl( // GenAI meters, which are emitted by the same UseOpenTelemetry() opt-in that // produces the gen_ai spans. metrics.AddMeter(QylMetricMeters.GetEnabledMeterNames()); - metrics.AddMeter( - "Experimental.Microsoft.Extensions.AI", - "Experimental.Microsoft.Agents.AI", - "Experimental.ModelContextProtocol"); + metrics.AddMeter(QylTelemetrySources.GetEnabledMeterNames()); foreach (var meter in options.AdditionalMeters) metrics.AddMeter(meter); diff --git a/src/Qyl.Sdk/QylSdkOptions.cs b/src/Qyl.Sdk/QylSdkOptions.cs index 9cf36d0..9627050 100644 --- a/src/Qyl.Sdk/QylSdkOptions.cs +++ b/src/Qyl.Sdk/QylSdkOptions.cs @@ -29,8 +29,8 @@ public sealed class QylSdkOptions public bool EnableLogExport { get; set; } = true; /// - /// Register a MeterProvider covering the qyl auto-instrumentation meter inventory (ASP.NET - /// Core, HttpClient, DNS, database, messaging, runtime) plus the GenAI meters, and export it + /// Register a MeterProvider covering the native and qyl-owned auto-instrumentation meter + /// inventory (ASP.NET Core, HttpClient, DNS, database, messaging, runtime) plus the GenAI meters, and export it /// over OTLP alongside traces. Defaults to true. /// public bool EnableMetricsExport { get; set; } = true; @@ -44,7 +44,8 @@ public sealed class QylSdkOptions /// /// Additional names to subscribe beyond the - /// qyl defaults (qyl listeners, ASP.NET Core, HttpClient, and the GenAI sources). + /// qyl defaults (qyl listener spans plus version-pinned GenAI, Azure SDK, MCP, and CoreWCF + /// sources). /// public IList AdditionalSources { get; } = []; diff --git a/src/Qyl.Sdk/QylTelemetrySources.cs b/src/Qyl.Sdk/QylTelemetrySources.cs new file mode 100644 index 0000000..266bdb1 --- /dev/null +++ b/src/Qyl.Sdk/QylTelemetrySources.cs @@ -0,0 +1,75 @@ +using Qyl.OpenTelemetry.AutoInstrumentation; + +namespace Qyl; + +internal static class QylTelemetrySources +{ + internal const string MicrosoftExtensionsAi = "Experimental.Microsoft.Extensions.AI"; + internal const string MicrosoftAgentsAi = "Experimental.Microsoft.Agents.AI"; + internal const string MicrosoftAgentsAiWorkflows = "Microsoft.Agents.AI.Workflows"; + internal const string ModelContextProtocol = "Experimental.ModelContextProtocol"; + internal const string CoreWcf = "CoreWCF.Primitives"; + internal const string Azure = "Azure.*"; + internal const string AspNetCore = "Microsoft.AspNetCore"; + internal const string HttpClient = "System.Net.Http"; + + internal static string[] GetEnabledActivitySourceNames() + { + var options = QylAutoInstrumentationOptions.Current; + var names = new List(8); + + if (options.HasAnyActivityInstrumentationEnabled()) + names.Add(QylActivitySource.Name); + + // Framework-native sources: registering them makes ASP.NET Core hosting and HttpClient + // create their activities through the sampler (proper root sampling decisions, honored + // upstream traceparent) instead of the legacy unsampled DiagnosticListener fallback. + AddIfEnabled(names, options, QylAutoInstrumentationIds.AspNetCore, AspNetCore); + AddIfEnabled(names, options, QylAutoInstrumentationIds.HttpClient, HttpClient); + + AddIfEnabled(names, options, QylAutoInstrumentationIds.MicrosoftExtensionsAi, MicrosoftExtensionsAi); + AddIfEnabled(names, options, QylAutoInstrumentationIds.MicrosoftAgentsAi, MicrosoftAgentsAi); + AddIfEnabled(names, options, QylAutoInstrumentationIds.MicrosoftAgentsAiWorkflows, MicrosoftAgentsAiWorkflows); + AddIfEnabled(names, options, QylAutoInstrumentationIds.ModelContextProtocol, ModelContextProtocol); + AddIfEnabled(names, options, QylAutoInstrumentationIds.WcfCore, CoreWcf); + AddIfEnabled(names, options, QylAutoInstrumentationIds.Azure, Azure); + + return names.ToArray(); + } + + internal static bool IsAzureTracingEnabled() + => QylAutoInstrumentationOptions.Current.IsInstrumentationEnabled( + QylAutoInstrumentationSignal.Traces, + QylAutoInstrumentationIds.Azure); + + internal static string[] GetEnabledMeterNames() + { + var options = QylAutoInstrumentationOptions.Current; + var names = new List(2); + + AddIfEnabled( + names, + options, + QylAutoInstrumentationIds.MicrosoftExtensionsAi, + MicrosoftExtensionsAi, + QylAutoInstrumentationSignal.Metrics); + AddIfEnabled( + names, + options, + QylAutoInstrumentationIds.MicrosoftAgentsAi, + MicrosoftAgentsAi, + QylAutoInstrumentationSignal.Metrics); + return names.ToArray(); + } + + private static void AddIfEnabled( + List names, + QylAutoInstrumentationOptions options, + string instrumentationId, + string telemetryName, + QylAutoInstrumentationSignal signal = QylAutoInstrumentationSignal.Traces) + { + if (options.IsInstrumentationEnabled(signal, instrumentationId)) + names.Add(telemetryName); + } +} diff --git a/tests/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.ContractManifests.verified.jsonl b/tests/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.ContractManifests.verified.jsonl new file mode 100644 index 0000000..4b41a64 --- /dev/null +++ b/tests/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.ContractManifests.verified.jsonl @@ -0,0 +1,28 @@ +{"interceptorKind":"DbCommand","signal":"traces","instrumentationId":"ADONET","additionalMetricIds":[],"contractKeys":["signals.traces.ADONET"]} +{"interceptorKind":"DbCommand","signal":"traces","instrumentationId":"MYSQLCONNECTOR","additionalMetricIds":[],"contractKeys":["signals.traces.MYSQLCONNECTOR"]} +{"interceptorKind":"DbCommand","signal":"traces","instrumentationId":"MYSQLDATA","additionalMetricIds":[],"contractKeys":["signals.traces.MYSQLDATA"]} +{"interceptorKind":"DbCommand","signal":"traces","instrumentationId":"NPGSQL","additionalMetricIds":["NPGSQL"],"contractKeys":["signals.traces.NPGSQL","signals.metrics.NPGSQL"]} +{"interceptorKind":"DbCommand","signal":"traces","instrumentationId":"ORACLEMDA","additionalMetricIds":[],"contractKeys":["signals.traces.ORACLEMDA"]} +{"interceptorKind":"DbCommand","signal":"traces","instrumentationId":"SQLCLIENT","additionalMetricIds":["SQLCLIENT"],"contractKeys":["signals.traces.SQLCLIENT","signals.metrics.SQLCLIENT"]} +{"interceptorKind":"DbCommand","signal":"traces","instrumentationId":"SQLITE","additionalMetricIds":[],"contractKeys":["signals.traces.SQLITE"]} +{"interceptorKind":"ElasticTransport","signal":"traces","instrumentationId":"ELASTICTRANSPORT","additionalMetricIds":[],"contractKeys":["signals.traces.ELASTICTRANSPORT"]} +{"interceptorKind":"ElasticsearchClient","signal":"traces","instrumentationId":"ELASTICSEARCH","additionalMetricIds":[],"contractKeys":["signals.traces.ELASTICSEARCH"]} +{"interceptorKind":"GraphQlDocumentExecuter","signal":"traces","instrumentationId":"GRAPHQL","additionalMetricIds":[],"contractKeys":["signals.traces.GRAPHQL"]} +{"interceptorKind":"GrpcNetClientAsyncClientStreamingCall","signal":"traces","instrumentationId":"GRPCNETCLIENT","additionalMetricIds":[],"contractKeys":["signals.traces.GRPCNETCLIENT"]} +{"interceptorKind":"GrpcNetClientAsyncDuplexStreamingCall","signal":"traces","instrumentationId":"GRPCNETCLIENT","additionalMetricIds":[],"contractKeys":["signals.traces.GRPCNETCLIENT"]} +{"interceptorKind":"GrpcNetClientAsyncServerStreamingCall","signal":"traces","instrumentationId":"GRPCNETCLIENT","additionalMetricIds":[],"contractKeys":["signals.traces.GRPCNETCLIENT"]} +{"interceptorKind":"GrpcNetClientAsyncUnaryCall","signal":"traces","instrumentationId":"GRPCNETCLIENT","additionalMetricIds":[],"contractKeys":["signals.traces.GRPCNETCLIENT"]} +{"interceptorKind":"HttpClient","signal":"traces","instrumentationId":"HTTPCLIENT","additionalMetricIds":["HTTPCLIENT"],"contractKeys":["signals.traces.HTTPCLIENT","signals.metrics.HTTPCLIENT"]} +{"interceptorKind":"ILoggerExtensionLog","signal":"logs","instrumentationId":"ILOGGER","additionalMetricIds":[],"contractKeys":["signals.logs.ILOGGER"]} +{"interceptorKind":"ILoggerLog","signal":"logs","instrumentationId":"ILOGGER","additionalMetricIds":[],"contractKeys":["signals.logs.ILOGGER"]} +{"interceptorKind":"KafkaConsumer","signal":"traces","instrumentationId":"KAFKA","additionalMetricIds":[],"contractKeys":["signals.traces.KAFKA"]} +{"interceptorKind":"KafkaProducer","signal":"traces","instrumentationId":"KAFKA","additionalMetricIds":[],"contractKeys":["signals.traces.KAFKA"]} +{"interceptorKind":"Log4NetLogger","signal":"logs","instrumentationId":"LOG4NET","additionalMetricIds":[],"contractKeys":["signals.logs.LOG4NET"]} +{"interceptorKind":"MassTransitMessageOperation","signal":"traces","instrumentationId":"MASSTRANSIT","additionalMetricIds":[],"contractKeys":["signals.traces.MASSTRANSIT"]} +{"interceptorKind":"MongoDbCollection","signal":"traces","instrumentationId":"MONGODB","additionalMetricIds":[],"contractKeys":["signals.traces.MONGODB"]} +{"interceptorKind":"NLogLogger","signal":"logs","instrumentationId":"NLOG","additionalMetricIds":[],"contractKeys":["signals.logs.NLOG"]} +{"interceptorKind":"NServiceBusMessageOperation","signal":"traces","instrumentationId":"NSERVICEBUS","additionalMetricIds":["NSERVICEBUS"],"contractKeys":["signals.traces.NSERVICEBUS","signals.metrics.NSERVICEBUS"]} +{"interceptorKind":"QuartzJobExecute","signal":"traces","instrumentationId":"QUARTZ","additionalMetricIds":[],"contractKeys":["signals.traces.QUARTZ"]} +{"interceptorKind":"RabbitMqBasicPublish","signal":"traces","instrumentationId":"RABBITMQ","additionalMetricIds":[],"contractKeys":["signals.traces.RABBITMQ"]} +{"interceptorKind":"StackExchangeRedisCommandAsync","signal":"traces","instrumentationId":"STACKEXCHANGEREDIS","additionalMetricIds":[],"contractKeys":["signals.traces.STACKEXCHANGEREDIS"]} +{"interceptorKind":"WcfClient","signal":"traces","instrumentationId":"WCFCLIENT","additionalMetricIds":[],"contractKeys":["signals.traces.WCFCLIENT"]} diff --git a/tests/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.Interceptors.g.verified.cs b/tests/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.Interceptors.g.verified.cs index c508c5b..28a8ca9 100644 --- a/tests/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.Interceptors.g.verified.cs +++ b/tests/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.Snapshots/verified/QylAutoInstrumentation.Interceptors.g.verified.cs @@ -16,8 +16,9 @@ namespace Qyl.OpenTelemetry.AutoInstrumentation.Generated { internal static class QylGeneratedInterceptors { - private const int RequiredQylGeneratedCodeAbi = global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylGeneratedCodeAbi.V6; + private const int RequiredQylGeneratedCodeAbi = global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylGeneratedCodeAbi.V8; + // qyl-interceptor-manifest: {"interceptorKind":"ILoggerLog","signal":"logs","instrumentationId":"ILOGGER","additionalMetricIds":[],"contractKeys":["signals.logs.ILOGGER"]} // Intercepted call at /_qyl_generator_snapshot/Program.cs(5,8) [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "BZ/VvozrZJa2e0oqXWVjZFQAAABQcm9ncmFtLmNz")] public static void ILogger_Log_0( @@ -29,6 +30,7 @@ public static void ILogger_Log_0( global::System.Func formatter) => global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedLogger.Log(logger, logLevel, eventId, state, exception, formatter); + // qyl-interceptor-manifest: {"interceptorKind":"ILoggerLog","signal":"logs","instrumentationId":"ILOGGER","additionalMetricIds":[],"contractKeys":["signals.logs.ILOGGER"]} // Intercepted call at /_qyl_generator_snapshot/Probe.cs(10,16) [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "bCVh78nW9Q7jLziddD9BAPcBAABQcm9iZS5jcw==")] public static void ILogger_Log_1( diff --git a/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/Program.cs b/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/Program.cs index 56dbd7c..8776f70 100644 --- a/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/Program.cs +++ b/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/Program.cs @@ -192,8 +192,8 @@ public static WebApiAotReport Create(CapturedActivity[] activities, int downstre var failures = new List(); var signals = new List(); - // Server span is owned by the generated-middleware lane (priority 90) which wins over the - // DiagnosticListener lane (70) via QylSignalOwnership, so exactly one server span is emitted and + // Server span is owned by the explicit middleware; the DiagnosticListener observes only the + // ambient start in this mode, so exactly one server span is emitted and // it carries the aspnetcore.server domain (with the route backfilled after routing). AddRequired(signals, failures, "aspnetcore.server", activities.FirstOrDefault(static activity => HasTag(activity, "qyl.instrumentation.domain", "aspnetcore.server") && @@ -216,7 +216,7 @@ public static WebApiAotReport Create(CapturedActivity[] activities, int downstre 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, "qyl.instrumentation.domain", "db.client") && HasTag(activity, "db.operation.name", "SELECT") && HasTag(activity, "error.type", "System.InvalidOperationException"))); diff --git a/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/verified/report.json b/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/verified/report.json index 33cff71..a43ee07 100644 --- a/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/verified/report.json +++ b/tools/Qyl.OpenTelemetry.AutoInstrumentation.WebApiAotDemo/verified/report.json @@ -8,7 +8,7 @@ "Tags": { "db.operation.name": "CREATE", "db.query.summary": "CREATE", - "db.system.name": "other_sql", + "db.system.name": "sqlite", "qyl.instrumentation.domain": "db.client" } }, @@ -32,7 +32,7 @@ "Tags": { "db.operation.name": "SELECT", "db.query.summary": "SELECT", - "db.system.name": "other_sql", + "db.system.name": "microsoft.sql_server", "error.type": "InvalidOperationException", "qyl.instrumentation.domain": "db.client" } @@ -75,19 +75,6 @@ "qyl.instrumentation.domain": "aspnetcore.server", "url.scheme": "http" } - }, - { - "Kind": "Client", - "Name": "SQL SELECT", - "Signal": "activity", - "Status": "Error", - "Tags": { - "db.operation.name": "SELECT", - "db.query.summary": "Text SELECT", - "db.system.name": "microsoft.sql_server", - "error.type": "System.InvalidOperationException", - "qyl.instrumentation.domain": "db.sqlclient" - } } ], "Failures": [], @@ -147,15 +134,15 @@ }, { "Kind": "Client", - "Name": "SQL SELECT", + "Name": "DB SELECT", "Signal": "sqlclient.command", "Status": "Error", "Tags": { "db.operation.name": "SELECT", - "db.query.summary": "Text SELECT", + "db.query.summary": "SELECT", "db.system.name": "microsoft.sql_server", - "error.type": "System.InvalidOperationException", - "qyl.instrumentation.domain": "db.sqlclient" + "error.type": "InvalidOperationException", + "qyl.instrumentation.domain": "db.client" } } ] diff --git a/tools/generate-contract-artifacts.py b/tools/generate-contract-artifacts.py index b3e5535..cc115e1 100755 --- a/tools/generate-contract-artifacts.py +++ b/tools/generate-contract-artifacts.py @@ -16,6 +16,7 @@ ROOT = Path(__file__).resolve().parents[1] UPSTREAM_CONTRACT_PATH = ROOT / "docs" / "contracts" / "otel-dotnet-auto-60.upstream.yaml" +QYL_NATIVE_CONTRACT_PATH = ROOT / "docs" / "contracts" / "qyl-native-instrumentations.yaml" OWNERSHIP_PATH = ROOT / "docs" / "contracts" / "qyl-aot-ownership.yaml" RESOLVED_CONTRACT_PATH = ROOT / "docs" / "generated" / "qyl-aot-contract.resolved.yaml" SCHEMA_PATH = ROOT / "docs" / "generated" / "qyl-aot-contract.schema.json" @@ -98,6 +99,16 @@ "nlog.log", ], }, + { + "service_name": "qyl-mcp-aot-demo", + "profile_id": "qyl-aot-mcp", + "signal_names": [ + "mcp.initialize", + "mcp.notifications.initialized", + "mcp.tools.list", + "mcp.tools.call", + ], + }, { "service_name": "qyl-metrics-aot-demo", "profile_id": "qyl-aot-metrics", @@ -128,6 +139,7 @@ "qyl-aot-graphql", "qyl-aot-grpc", "qyl-aot-logging", + "qyl-aot-mcp", "qyl-aot-messaging", "qyl-aot-metrics", "qyl-aot-search", @@ -158,6 +170,75 @@ PAYLOAD_ACCESS_VALUES = {"typed_public", "reflection_required", "not_applicable"} EVIDENCE_LEVELS = {"none", "verified_nativeaot", "verified_managed", "compile_binding_only", "option_bound"} CONFORMANCE_KINDS = {"span", "metric", "log"} +QYL_NATIVE_8_SIGNAL_CONTRACT = [ + ( + "QYL_NATIVE_CONTRACT_001", + "signals.traces.MICROSOFTEXTENSIONSAI", + "traces", + "MICROSOFTEXTENSIONSAI", + "OTEL_DOTNET_AUTO_TRACES_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED", + ), + ( + "QYL_NATIVE_CONTRACT_002", + "signals.metrics.MICROSOFTEXTENSIONSAI", + "metrics", + "MICROSOFTEXTENSIONSAI", + "OTEL_DOTNET_AUTO_METRICS_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED", + ), + ( + "QYL_NATIVE_CONTRACT_003", + "signals.traces.MICROSOFTAGENTSAI", + "traces", + "MICROSOFTAGENTSAI", + "OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED", + ), + ( + "QYL_NATIVE_CONTRACT_004", + "signals.metrics.MICROSOFTAGENTSAI", + "metrics", + "MICROSOFTAGENTSAI", + "OTEL_DOTNET_AUTO_METRICS_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED", + ), + ( + "QYL_NATIVE_CONTRACT_005", + "signals.traces.MICROSOFTAGENTSAIWORKFLOWS", + "traces", + "MICROSOFTAGENTSAIWORKFLOWS", + "OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAIWORKFLOWS_INSTRUMENTATION_ENABLED", + ), + ( + "QYL_NATIVE_CONTRACT_006", + "signals.traces.MCP", + "traces", + "MCP", + "OTEL_DOTNET_AUTO_TRACES_MCP_INSTRUMENTATION_ENABLED", + ), +] +QYL_NATIVE_8_SUPPORTED_PATHS = { + "signals.traces.MICROSOFTEXTENSIONSAI": ( + "Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only" + ), + "signals.metrics.MICROSOFTEXTENSIONSAI": ( + "Microsoft.Extensions.AI ==10.8.0; ChatClientBuilder.UseOpenTelemetry() wrapper only" + ), + "signals.traces.MICROSOFTAGENTSAI": ( + "Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only" + ), + "signals.metrics.MICROSOFTAGENTSAI": ( + "Microsoft.Agents.AI ==1.13.0; AIAgentBuilder.UseOpenTelemetry() wrapper only" + ), + "signals.traces.MICROSOFTAGENTSAIWORKFLOWS": ( + "Microsoft.Agents.AI.Workflows ==1.13.0; WorkflowBuilder.WithOpenTelemetry() path only" + ), + "signals.traces.MCP": ( + "ModelContextProtocol ==1.4.1; automatic official client/server ActivitySource path only" + ), +} +QYL_SUPPORTED_VERSION_OVERRIDES = { + "signals.traces.WCFCORE": ( + "CoreWCF.Primitives ==1.9.1; CoreWCF.Http ==1.9.1; server ActivitySource path only" + ), +} MANAGED_NATIVEAOT_BOUNDARY_SIGNAL_KEYS = { "signals.logs.LOG4NET", "signals.metrics.NSERVICEBUS", @@ -166,8 +247,23 @@ "signals.traces.NSERVICEBUS", "signals.traces.QUARTZ", "signals.traces.WCFCLIENT", + "signals.traces.WCFCORE", + "signals.traces.MICROSOFTEXTENSIONSAI", + "signals.metrics.MICROSOFTEXTENSIONSAI", + "signals.traces.MICROSOFTAGENTSAI", + "signals.metrics.MICROSOFTAGENTSAI", + "signals.traces.MICROSOFTAGENTSAIWORKFLOWS", } IMPLEMENTED_COMPILE_BINDING_ONLY_ALLOWLIST: set[str] = set() +# Signals whose contract row is owned by the runtime_public_telemetry listener/meter lane while a +# source interceptor also binds their call sites. QylSignalOwnership hands the emitted span to the +# interceptor lane when it is present; the listener lane stays the contract owner because it covers +# call sites the interceptor cannot see (raw CallInvoker, HttpMessageInvoker, DI-injected clients). +GENERATED_INTERCEPTOR_ALTERNATE_PATH_SIGNAL_ALLOWLIST: set[str] = { + "signals.traces.HTTPCLIENT", + "signals.metrics.HTTPCLIENT", + "signals.traces.GRPCNETCLIENT", +} GENERATED_INTERCEPTOR_CATALOG_REQUIRED_SIGNAL_KEYS = { "signals.logs.ILOGGER", "signals.logs.LOG4NET", @@ -198,6 +294,7 @@ COMMON_ITEM_PROPERTIES = { "kind", "key", + "contract_origin", "status", "index", "contract_item_id", @@ -274,18 +371,28 @@ def load_yaml_file(path: Path) -> dict[str, Any]: def load_contract() -> dict[str, Any]: upstream = load_yaml_file(UPSTREAM_CONTRACT_PATH) + qyl_native = load_yaml_file(QYL_NATIVE_CONTRACT_PATH) ownership = load_yaml_file(OWNERSHIP_PATH) - return resolve_contract(upstream, ownership) + return resolve_contract(upstream, qyl_native, ownership) -def resolve_contract(upstream: dict[str, Any], ownership: dict[str, Any]) -> dict[str, Any]: +def resolve_contract( + upstream: dict[str, Any], + qyl_native: dict[str, Any], + ownership: dict[str, Any], +) -> dict[str, Any]: upstream_items = upstream.get("contract_items") + qyl_native_items = qyl_native.get("contract_items") ownership_items = ownership.get("ownership_items") if not isinstance(upstream_items, list): fail("upstream contract must contain contract_items[]") + if not isinstance(qyl_native_items, list): + fail("qyl-native contract must contain contract_items[]") if not isinstance(ownership_items, list): fail("qyl ownership overlay must contain ownership_items[]") + verify_source_contract_sequences(upstream_items, qyl_native_items) + overlay_by_id: dict[str, dict[str, Any]] = {} for raw_overlay in ownership_items: if not isinstance(raw_overlay, dict): @@ -302,6 +409,7 @@ def resolve_contract(upstream: dict[str, Any], ownership: dict[str, Any]) -> dic if not isinstance(raw_item, dict): fail(f"upstream contract item must be an object: {raw_item!r}") item = dict(raw_item) + item["contract_origin"] = "upstream_otel_dotnet_auto_60" contract_item_id = str(item.get("contract_item_id", "")) overlay = overlay_by_id.pop(contract_item_id, None) if overlay is None: @@ -318,6 +426,7 @@ def resolve_contract(upstream: dict[str, Any], ownership: dict[str, Any]) -> dic "evidence", "conformance_signals", "unsupported_reason", + "supported_versions", ]: if field in overlay: item[field] = overlay[field] @@ -326,19 +435,79 @@ def resolve_contract(upstream: dict[str, Any], ownership: dict[str, Any]) -> dic if overlay_by_id: fail(f"qyl ownership overlay contains unknown contract items: {sorted(overlay_by_id)}") + for raw_item in sorted(qyl_native_items, key=lambda candidate: int(candidate["index"])): + if not isinstance(raw_item, dict): + fail(f"qyl-native contract item must be an object: {raw_item!r}") + item = dict(raw_item) + item["contract_origin"] = "qyl_native" + resolved_items.append(item) + return { "schema_id": "qyl-aot-autoinstrumentation-resolved-contract", - "schema_version": str(upstream.get("schema_version", "1.0.0")), - "generated_at": str(upstream.get("generated_at", "2026-06-04")), - "source": upstream.get("source", {}), + "schema_version": "2.0.0", + "generated_at": str(qyl_native.get("generated_at", "2026-07-19")), + "sources": { + "upstream_otel_dotnet_auto_60": upstream.get("source", {}), + "qyl_native": qyl_native.get("source", {}), + }, "generated_from": { "upstream_contract": str(UPSTREAM_CONTRACT_PATH.relative_to(ROOT)), + "qyl_native_contract": str(QYL_NATIVE_CONTRACT_PATH.relative_to(ROOT)), "qyl_ownership": str(OWNERSHIP_PATH.relative_to(ROOT)), }, "contract_items": resolved_items, } +def verify_source_contract_sequences( + upstream_items: list[Any], + qyl_native_items: list[Any], +) -> None: + if len(upstream_items) != 60: + fail(f"upstream contract must remain exactly 60 items: {len(upstream_items)}") + upstream_indexes = [int(item["index"]) for item in upstream_items] + if upstream_indexes != list(range(1, 61)): + fail(f"upstream contract indexes must remain contiguous 1..60: {upstream_indexes}") + upstream_ids = [str(item["contract_item_id"]) for item in upstream_items] + expected_upstream_ids = [f"OTEL_DOTNET_AUTO_CONTRACT_{index:03d}" for index in range(1, 61)] + if upstream_ids != expected_upstream_ids: + fail("upstream contract_item_id sequence mismatch") + + if len(qyl_native_items) != 6: + fail(f"qyl-native 8.0 contract must contain exactly 6 scoped signal items: {len(qyl_native_items)}") + qyl_native_indexes = [int(item["index"]) for item in qyl_native_items] + if qyl_native_indexes != list(range(61, 67)): + fail(f"qyl-native contract indexes must be contiguous 61..66: {qyl_native_indexes}") + qyl_native_ids = [str(item["contract_item_id"]) for item in qyl_native_items] + expected_qyl_native_ids = [f"QYL_NATIVE_CONTRACT_{index:03d}" for index in range(1, 7)] + if qyl_native_ids != expected_qyl_native_ids: + fail("qyl-native contract_item_id sequence mismatch") + actual_qyl_native_contract = [ + ( + str(item["contract_item_id"]), + str(item["key"]), + str(item["signal"]), + str(item["instrumentation_id"]), + str(item["environment_toggle"]), + ) + for item in qyl_native_items + ] + if actual_qyl_native_contract != QYL_NATIVE_8_SIGNAL_CONTRACT: + fail( + "qyl-native 8.0 signal scope mismatch: " + f"expected={QYL_NATIVE_8_SIGNAL_CONTRACT} actual={actual_qyl_native_contract}" + ) + actual_supported_paths = { + str(item["key"]): str(item["supported_versions"]) + for item in qyl_native_items + } + if actual_supported_paths != QYL_NATIVE_8_SUPPORTED_PATHS: + fail( + "qyl-native 8.0 supported path/version mismatch: " + f"expected={QYL_NATIVE_8_SUPPORTED_PATHS} actual={actual_supported_paths}" + ) + + def contract_items(contract: dict[str, Any]) -> list[dict[str, Any]]: return list(contract["contract_items"]) @@ -375,6 +544,12 @@ def contract_counts(contract: dict[str, Any]) -> dict[str, int]: items = contract_items(contract) signals = signal_items(contract) return { + "upstream_contract_items": sum( + 1 for item in items if item.get("contract_origin") == "upstream_otel_dotnet_auto_60" + ), + "qyl_native_contract_items": sum( + 1 for item in items if item.get("contract_origin") == "qyl_native" + ), "signal_specific_instrumentation_promises": len(signals), "global_environment_controls": sum(1 for item in items if item["kind"] == CONTROL_KIND), "instrumentation_options": sum(1 for item in items if item["kind"] == OPTION_KIND), @@ -388,28 +563,32 @@ def contract_counts(contract: dict[str, Any]) -> dict[str, int]: def verify_contract_model(contract: dict[str, Any]) -> None: items = contract_items(contract) - if len(items) != 60: + if len(items) != 66: fail(f"wrong contract item count: {len(items)}") indexes = [int(item["index"]) for item in items] - if indexes != list(range(1, 61)): - fail(f"contract indexes must be contiguous 1..60: {indexes}") + if indexes != list(range(1, 67)): + fail(f"resolved contract indexes must be contiguous 1..66: {indexes}") ids = [str(item["contract_item_id"]) for item in items] - expected_ids = [f"OTEL_DOTNET_AUTO_CONTRACT_{index:03d}" for index in range(1, 61)] + expected_ids = [f"OTEL_DOTNET_AUTO_CONTRACT_{index:03d}" for index in range(1, 61)] + [ + f"QYL_NATIVE_CONTRACT_{index:03d}" for index in range(1, 7) + ] if ids != expected_ids: fail("contract_item_id sequence mismatch") counts = contract_counts(contract) expected_counts = { - "signal_specific_instrumentation_promises": 37, + "upstream_contract_items": 60, + "qyl_native_contract_items": 6, + "signal_specific_instrumentation_promises": 43, "global_environment_controls": 7, "instrumentation_options": 16, - "total_contract_items": 60, - "traces_signal_specific_promises": 26, - "metrics_signal_specific_promises": 8, + "total_contract_items": 66, + "traces_signal_specific_promises": 30, + "metrics_signal_specific_promises": 10, "logs_signal_specific_promises": 3, - "unique_instrumentation_ids": 31, + "unique_instrumentation_ids": 35, } if counts != expected_counts: fail(f"contract count mismatch: expected={expected_counts} actual={counts}") @@ -422,6 +601,21 @@ def verify_contract_model(contract: dict[str, Any]) -> None: fail(f"duplicate contract key: {key}") seen_keys.add(key) + upstream_items = [ + item for item in items if item.get("contract_origin") == "upstream_otel_dotnet_auto_60" + ] + qyl_native_items = [item for item in items if item.get("contract_origin") == "qyl_native"] + verify_source_contract_sequences(upstream_items, qyl_native_items) + + items_by_key = {str(item["key"]): item for item in items} + for key, expected in QYL_SUPPORTED_VERSION_OVERRIDES.items(): + actual = str(items_by_key[key]["supported_versions"]) + if actual != expected: + fail( + f"qyl supported-version override mismatch for {key}: " + f"expected={expected!r} actual={actual!r}" + ) + verify_managed_nativeaot_boundary_semantics(contract) verify_conformance_signal_semantics(contract) @@ -454,6 +648,10 @@ def verify_contract_item(item: dict[str, Any]) -> None: evidence_level = str(item.get("evidence_level", "")) evidence = item.get("evidence") upstream_sources = item.get("upstream_sources") + contract_origin = str(item.get("contract_origin", "")) + + if contract_origin not in {"upstream_otel_dotnet_auto_60", "qyl_native"}: + fail(f"invalid contract_origin for {key}: {contract_origin}") if lane not in LANES: fail(f"invalid lane for {key}: {lane}") @@ -545,6 +743,7 @@ def verify_contract_item(item: dict[str, Any]) -> None: if lane == "runtime_public_telemetry": proof_tokens = [ "src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners", + "DiagnosticListener", "src/Qyl.OpenTelemetry.AutoInstrumentation/QylMetricMeters.cs", "ILogger", "ActivitySource", @@ -695,6 +894,7 @@ def render_schema() -> str: common_required = [ "kind", "key", + "contract_origin", "status", "index", "contract_item_id", @@ -713,25 +913,34 @@ def render_schema() -> str: "$comment": " Regenerate with tools/generate-contract-artifacts.py --write.", "type": "object", "additionalProperties": False, - "required": ["schema_id", "schema_version", "generated_at", "source", "generated_from", "contract_items"], + "required": ["schema_id", "schema_version", "generated_at", "sources", "generated_from", "contract_items"], "properties": { "schema_id": {"const": "qyl-aot-autoinstrumentation-resolved-contract"}, - "schema_version": {"type": "string"}, + "schema_version": {"const": "2.0.0"}, "generated_at": {"type": "string"}, - "source": {"type": "object"}, + "sources": { + "type": "object", + "additionalProperties": False, + "required": ["upstream_otel_dotnet_auto_60", "qyl_native"], + "properties": { + "upstream_otel_dotnet_auto_60": {"type": "object"}, + "qyl_native": {"type": "object"}, + }, + }, "generated_from": { "type": "object", "additionalProperties": False, - "required": ["upstream_contract", "qyl_ownership"], + "required": ["upstream_contract", "qyl_native_contract", "qyl_ownership"], "properties": { "upstream_contract": {"const": "docs/contracts/otel-dotnet-auto-60.upstream.yaml"}, + "qyl_native_contract": {"const": "docs/contracts/qyl-native-instrumentations.yaml"}, "qyl_ownership": {"const": "docs/contracts/qyl-aot-ownership.yaml"}, }, }, "contract_items": { "type": "array", - "minItems": 60, - "maxItems": 60, + "minItems": 66, + "maxItems": 66, "items": {"$ref": "#/$defs/contract_item"}, }, }, @@ -783,9 +992,13 @@ def common_schema_properties() -> dict[str, Any]: return { "kind": {"enum": [SIGNAL_KIND, CONTROL_KIND, OPTION_KIND]}, "key": {"type": "string"}, + "contract_origin": {"enum": ["upstream_otel_dotnet_auto_60", "qyl_native"]}, "status": {"type": "string"}, - "index": {"type": "integer", "minimum": 1, "maximum": 60}, - "contract_item_id": {"type": "string", "pattern": "^OTEL_DOTNET_AUTO_CONTRACT_[0-9]{3}$"}, + "index": {"type": "integer", "minimum": 1, "maximum": 66}, + "contract_item_id": { + "type": "string", + "pattern": "^(?:OTEL_DOTNET_AUTO_CONTRACT|QYL_NATIVE_CONTRACT)_[0-9]{3}$", + }, "upstream_sources": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/upstream_source"}}, "lane": {"enum": sorted(LANES)}, "qyl_status": {"enum": sorted(STATUSES)}, @@ -874,14 +1087,16 @@ def render_coverage_matrix(contract: dict[str, Any]) -> str: "", "", "This matrix is generated from `docs/generated/qyl-aot-contract.resolved.yaml`.", - "The raw upstream contract lives in `docs/contracts/otel-dotnet-auto-60.upstream.yaml`; qyl ownership and evidence live in `docs/contracts/qyl-aot-ownership.yaml`.", - "Every row carries a clickable upstream source anchored to the official OpenTelemetry documentation source.", + "The exact 60-row upstream contract lives in `docs/contracts/otel-dotnet-auto-60.upstream.yaml`; qyl-native promises live in `docs/contracts/qyl-native-instrumentations.yaml`; qyl ownership and evidence for upstream rows live in `docs/contracts/qyl-aot-ownership.yaml`.", + "Every row identifies its contract origin and carries a clickable authoritative source.", "", "## Counts", "", "| Count | Value |", "|---|---:|", f"| Total contract items | {counts['total_contract_items']} |", + f"| Upstream OpenTelemetry .NET auto-instrumentation items | {counts['upstream_contract_items']} |", + f"| qyl-native items | {counts['qyl_native_contract_items']} |", f"| Signal promises | {counts['signal_specific_instrumentation_promises']} |", f"| Global environment controls | {counts['global_environment_controls']} |", f"| Instrumentation options | {counts['instrumentation_options']} |", @@ -904,15 +1119,42 @@ def render_coverage_matrix(contract: dict[str, Any]) -> str: "", "## Matrix", "", - "| # | Key | Upstream source | Lane | qyl status | Call-site visibility | Payload access | Evidence | Owner |", - "|---:|---|---|---|---|---|---|---|---|", + "| # | Origin | Key | Authoritative source | Supported path/version | Lane | qyl status | Call-site visibility | Payload access | Evidence | Owner |", + "|---:|---|---|---|---|---|---|---|---|---|---|", ] ) for item in contract_items(contract): + origin = ( + "upstream 60" + if item["contract_origin"] == "upstream_otel_dotnet_auto_60" + else "qyl native" + ) lines.append( - f"| {int(item['index'])} | `{item['key']}` | {format_upstream_sources(item)} | `{item['lane']}` | `{item['qyl_status']}` | " + f"| {int(item['index'])} | {origin} | `{item['key']}` | {format_authoritative_sources(item)} | " + f"{escape_md(str(item.get('supported_versions', 'n/a')))} | `{item['lane']}` | `{item['qyl_status']}` | " f"`{item['call_site_visibility']}` | `{item['payload_access']}` | `{item['evidence_level']}` | {escape_md(str(item['primary_owner']))} |" ) + qyl_native = [ + item for item in contract_items(contract) if item.get("contract_origin") == "qyl_native" + ] + if qyl_native: + lines.extend( + [ + "", + "## qyl-native 8.0 scope boundaries", + "", + "These are exact library-hook promises. Evidence level is explicit per row; none imply provider-wide coverage.", + "", + "| Key | Exact supported path/version | Boundary notes |", + "|---|---|---|", + ] + ) + for item in qyl_native: + notes = item.get("notes", []) + formatted_notes = "
".join(escape_md(str(note)) for note in notes) if isinstance(notes, list) else "" + lines.append( + f"| `{item['key']}` | {escape_md(str(item.get('supported_versions', '')))} | {formatted_notes} |" + ) unsupported = [item for item in contract_items(contract) if item.get("qyl_status") == "unsupported_nativeaot"] if unsupported: lines.extend( @@ -937,7 +1179,7 @@ def escape_md(value: str) -> str: return value.replace("|", "\\|") -def format_upstream_sources(item: dict[str, Any]) -> str: +def format_authoritative_sources(item: dict[str, Any]) -> str: sources = item.get("upstream_sources", []) if not isinstance(sources, list): return "" @@ -1017,7 +1259,9 @@ def write_generated_files(contract: dict[str, Any]) -> None: def main() -> None: - parser = argparse.ArgumentParser(description="Generate or verify qyl AOT contract artifacts from upstream + qyl ownership YAML.") + parser = argparse.ArgumentParser( + description="Generate or verify qyl AOT contract artifacts from upstream, qyl-native, and ownership YAML." + ) group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--write", action="store_true", help="Regenerate tracked contract artifacts.") group.add_argument("--check", action="store_true", help="Fail if tracked contract artifacts are stale.") diff --git a/tools/generator_manifest_coverage.py b/tools/generator_manifest_coverage.py new file mode 100644 index 0000000..a66f886 --- /dev/null +++ b/tools/generator_manifest_coverage.py @@ -0,0 +1,296 @@ +from __future__ import annotations + +import argparse +import importlib.util +import json +import os +import re +import subprocess +import tempfile +from pathlib import Path +from types import ModuleType +from typing import Any, Iterable + + +ROOT = Path(__file__).resolve().parents[1] +ARTIFACTS_PATH = ROOT / "tools" / "generate-contract-artifacts.py" +MANIFEST_PREFIX = "// qyl-interceptor-manifest: " +GENERATED_INTERCEPTOR_FILE = "QylAutoInstrumentation.Interceptors.g.cs" +GENERATOR_OUTPUT_SEGMENT = "Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators" +MANIFEST_FIELDS = ( + "interceptorKind", + "signal", + "instrumentationId", + "additionalMetricIds", + "contractKeys", +) + + +class ManifestCoverageError(RuntimeError): + pass + + +def fail(message: str) -> None: + raise ManifestCoverageError(message) + + +def load_artifacts() -> ModuleType: + spec = importlib.util.spec_from_file_location("qyl_contract_artifacts_for_manifests", ARTIFACTS_PATH) + if spec is None or spec.loader is None: + fail(f"cannot load contract artifact generator: {ARTIFACTS_PATH}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def strict_json_object(payload: str, context: str) -> dict[str, Any]: + def reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate JSON key {key!r}") + result[key] = value + return result + + try: + value = json.loads(payload, object_pairs_hook=reject_duplicate_keys) + except (json.JSONDecodeError, ValueError) as error: + fail(f"invalid interceptor manifest JSON in {context}: {error}") + if not isinstance(value, dict): + fail(f"interceptor manifest must be a JSON object in {context}") + return value + + +def verify_manifest_shape(manifest: dict[str, Any], context: str) -> None: + actual_fields = tuple(manifest) + if actual_fields != MANIFEST_FIELDS: + fail( + f"interceptor manifest fields are not canonical in {context}: " + f"expected={MANIFEST_FIELDS} actual={actual_fields}" + ) + + kind = manifest["interceptorKind"] + signal = manifest["signal"] + instrumentation_id = manifest["instrumentationId"] + additional_metric_ids = manifest["additionalMetricIds"] + contract_keys = manifest["contractKeys"] + if not isinstance(kind, str) or not kind: + fail(f"interceptor manifest kind must be a non-empty string in {context}") + if signal not in {"traces", "metrics", "logs"}: + fail(f"interceptor manifest signal is invalid in {context}: {signal!r}") + if not isinstance(instrumentation_id, str) or re.fullmatch(r"[A-Z0-9]+", instrumentation_id) is None: + fail(f"interceptor manifest instrumentation id is invalid in {context}: {instrumentation_id!r}") + if not isinstance(additional_metric_ids, list) or any( + not isinstance(item, str) or re.fullmatch(r"[A-Z0-9]+", item) is None + for item in additional_metric_ids + ): + fail(f"interceptor manifest additional metric ids are invalid in {context}") + if len(set(additional_metric_ids)) != len(additional_metric_ids): + fail(f"interceptor manifest additional metric ids contain duplicates in {context}") + if not isinstance(contract_keys, list) or any(not isinstance(item, str) for item in contract_keys): + fail(f"interceptor manifest contract keys are invalid in {context}") + + expected_contract_keys = [f"signals.{signal}.{instrumentation_id}"] + expected_contract_keys.extend(f"signals.metrics.{item}" for item in additional_metric_ids) + if contract_keys != expected_contract_keys: + fail( + f"interceptor manifest contract keys are not canonically derived in {context}: " + f"expected={expected_contract_keys} actual={contract_keys!r}" + ) + if len(set(contract_keys)) != len(contract_keys): + fail(f"interceptor manifest contract keys contain duplicates in {context}") + + +def canonical_manifest_line(manifest: dict[str, Any]) -> str: + verify_manifest_shape(manifest, "canonical manifest") + ordered = {field: manifest[field] for field in MANIFEST_FIELDS} + return json.dumps(ordered, ensure_ascii=False, separators=(",", ":")) + + +def parse_generated_interceptor_source(text: str, source: Path) -> list[dict[str, Any]]: + lines = text.splitlines() + manifest_lines = [ + (index, line.strip()[len(MANIFEST_PREFIX):]) + for index, line in enumerate(lines) + if line.strip().startswith(MANIFEST_PREFIX) + ] + interceptor_lines = [ + index + for index, line in enumerate(lines) + if line.strip().startswith("// Intercepted call at ") + ] + if not interceptor_lines: + fail(f"generated interceptor source contains no intercepted calls: {source}") + if [index for index, _ in manifest_lines] != [index - 1 for index in interceptor_lines]: + fail(f"every generated interceptor must have exactly one adjacent manifest: {source}") + + manifests: list[dict[str, Any]] = [] + for line_index, payload in manifest_lines: + context = f"{source}:{line_index + 1}" + manifest = strict_json_object(payload, context) + verify_manifest_shape(manifest, context) + manifests.append(manifest) + return manifests + + +def parse_verified_manifest_artifact(text: str, source: Path) -> list[dict[str, Any]]: + if not text.endswith("\n"): + fail(f"verified interceptor manifest artifact must end with a newline: {source}") + lines = text.splitlines() + if not lines or any(not line for line in lines): + fail(f"verified interceptor manifest artifact must contain only non-empty JSON lines: {source}") + + manifests: list[dict[str, Any]] = [] + canonical_lines: list[str] = [] + for line_index, line in enumerate(lines, start=1): + context = f"{source}:{line_index}" + manifest = strict_json_object(line, context) + verify_manifest_shape(manifest, context) + manifests.append(manifest) + canonical_lines.append(canonical_manifest_line(manifest)) + + if lines != sorted(set(canonical_lines)): + fail(f"verified interceptor manifest artifact is not unique and ordinal-sorted: {source}") + return manifests + + +def render_manifest_artifact(manifests: Iterable[dict[str, Any]]) -> str: + lines = sorted({canonical_manifest_line(manifest) for manifest in manifests}) + if not lines: + fail("live generator builds emitted no interceptor manifests") + return "\n".join(lines) + "\n" + + +def implemented_signal_demo_projects() -> tuple[Path, ...]: + artifacts = load_artifacts() + contract = artifacts.load_contract() + projects: set[Path] = set() + for item in artifacts.implemented_signal_items(contract): + evidence_dirs = [ + ROOT / entry + for entry in item.get("evidence", []) + if isinstance(entry, str) and entry.startswith("demos/") + ] + if not evidence_dirs: + fail(f"implemented signal item has no demo evidence: {item['key']}") + item_projects: set[Path] = set() + for evidence_dir in evidence_dirs: + candidates = sorted(evidence_dir.glob("*.csproj")) if evidence_dir.is_dir() else [] + if len(candidates) != 1: + fail( + f"implemented signal demo evidence must contain exactly one project for {item['key']}: " + f"{evidence_dir.relative_to(ROOT)}" + ) + item_projects.add(candidates[0]) + projects.update(item_projects) + return tuple(sorted(projects, key=lambda path: path.as_posix())) + + +def clean_build_environment() -> dict[str, str]: + environment = dict(os.environ) + for key in list(environment): + if key.startswith("OTEL_") or key.startswith("QYL_"): + del environment[key] + environment["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1" + environment["DOTNET_NOLOGO"] = "1" + environment["MSBUILDDISABLENODEREUSE"] = "1" + return environment + + +def collect_live_manifest_artifact() -> str: + projects = implemented_signal_demo_projects() + if not projects: + fail("the implemented signal contract selected no demo projects") + + manifests: list[dict[str, Any]] = [] + environment = clean_build_environment() + with tempfile.TemporaryDirectory(prefix="qyl-generator-manifests-") as temp: + temp_root = Path(temp) + artifacts_root = temp_root / "artifacts" + for project in projects: + generated_root = temp_root / "generated" / project.stem + command = [ + "dotnet", + "build", + str(project), + "-c", + "Release", + "--disable-build-servers", + "--artifacts-path", + str(artifacts_root), + "-m:1", + "/nr:false", + "-v", + "quiet", + "-p:NuGetAudit=false", + "-p:RestoreIgnoreFailedSources=true", + "-p:UseSharedCompilation=false", + "-p:WarningsNotAsErrors=NU1801", + "-p:EmitCompilerGeneratedFiles=true", + f"-p:CompilerGeneratedFilesOutputPath={generated_root}", + ] + completed = subprocess.run( + command, + cwd=ROOT, + env=environment, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + if completed.returncode != 0: + fail( + f"generator manifest demo build failed: {project.relative_to(ROOT)}\n" + f"exit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}" + ) + + generated_files = sorted( + path + for path in generated_root.rglob(GENERATED_INTERCEPTOR_FILE) + if GENERATOR_OUTPUT_SEGMENT in path.parts + ) + if len(generated_files) > 1: + fail( + f"expected at most one generated interceptor source for {project.relative_to(ROOT)}, " + f"found {len(generated_files)}" + ) + if not generated_files: + continue + generated_file = generated_files[0] + manifests.extend( + parse_generated_interceptor_source( + generated_file.read_text(encoding="utf-8"), + generated_file, + ) + ) + + return render_manifest_artifact(manifests) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Collect canonical interceptor manifests from every implemented signal evidence demo." + ) + parser.add_argument( + "--output", + type=Path, + help="write the JSONL artifact to this path instead of standard output", + ) + args = parser.parse_args() + + try: + artifact = collect_live_manifest_artifact() + except ManifestCoverageError as error: + raise SystemExit(str(error)) from error + + if args.output is None: + print(artifact, end="") + return + + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(artifact, encoding="utf-8") + print(f"generator-manifest-coverage-ok: {args.output}") + + +if __name__ == "__main__": + main() diff --git a/tools/smoketest.sh b/tools/smoketest.sh index e69f90e..1305aa7 100755 --- a/tools/smoketest.sh +++ b/tools/smoketest.sh @@ -2,12 +2,14 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -WORK="${TMPDIR:-/tmp}/qyl-smoke" +SMOKE_TEMP_ROOT="$(cd "${TMPDIR:-/tmp}" && pwd -P)" +WORK="$SMOKE_TEMP_ROOT/qyl-smoke" FEED="$WORK/feed" PACKAGES="$WORK/packages" NUGET_ORG="https://api.nuget.org/v3/index.json" MODE="local" VERSION="$(sed -n 's:.*\(.*\).*:\1:p' "$ROOT/Directory.Build.props" | head -n 1)" +INMEMORY_EXPORTER_VERSION="$(sed -n 's:.*&2 @@ -41,6 +43,10 @@ if [[ -z "$VERSION" ]]; then echo "Directory.Build.props does not contain a package " >&2 exit 2 fi +if [[ -z "$INMEMORY_EXPORTER_VERSION" ]]; then + echo "Directory.Packages.props does not contain an OpenTelemetry.Exporter.InMemory package version" >&2 + exit 2 +fi rm -rf "$WORK" mkdir -p "$FEED" "$PACKAGES" @@ -49,18 +55,93 @@ if [[ "$MODE" == "local" ]]; then dotnet build "$ROOT/src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.csproj" -c Release -v quiet dotnet pack "$ROOT/src/Qyl.OpenTelemetry.AutoInstrumentation/Qyl.OpenTelemetry.AutoInstrumentation.csproj" -c Release -o "$FEED" -v quiet dotnet pack "$ROOT/src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.csproj" -c Release -o "$FEED" -v quiet + dotnet pack "$ROOT/src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient.csproj" -c Release -o "$FEED" -v quiet dotnet pack "$ROOT/src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/Qyl.OpenTelemetry.AutoInstrumentation.Hosting.csproj" -c Release -o "$FEED" -v quiet + dotnet pack "$ROOT/src/Qyl.Sdk/Qyl.Sdk.csproj" -c Release -o "$FEED" -v quiet fi write_program() { local dir="$1" cat > "$dir/Program.cs" <<'EOF' using System.Diagnostics; +#if QYL_SDK_PACKAGE_SMOKE +using System.Diagnostics.Metrics; +#endif using System.Net; using System.Net.Sockets; using System.Text; +#if QYL_SDK_PACKAGE_SMOKE +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +#endif using Microsoft.Extensions.Logging; +#if QYL_SDK_PACKAGE_SMOKE +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Qyl; +#endif using Qyl.OpenTelemetry.AutoInstrumentation; +#if QYL_PROJECTREFERENCE_SMOKE +using Qyl.OpenTelemetry.AutoInstrumentation.Hosting; +#endif + +#if QYL_PROJECTREFERENCE_SMOKE +QylAutoInstrumentationBootstrap.Boot(); +#endif + +#if QYL_SDK_PACKAGE_SMOKE +const string qylActivitySourceName = "Qyl.OpenTelemetry.AutoInstrumentation"; +const string httpClientMeterName = "System.Net.Http"; +const string httpClientDurationMetricName = "http.client.request.duration"; + +var exportedActivities = new List(); +var exportedMetrics = new List(); +var inMemoryMetricReader = new BaseExportingMetricReader(new InMemoryExporter(exportedMetrics)); +var rawHttpClientDurationMeasurementCount = 0; +var applicationServerPort = 0; + +var qylBuilder = new HostApplicationBuilder(new HostApplicationBuilderSettings +{ + ApplicationName = "qyl-package-smoke", + DisableDefaults = true, +}); +qylBuilder.AddQyl(options => +{ + options.ServiceName = "qyl-package-smoke"; + options.EnableCollectorDiscovery = false; + options.EnableMetricsExport = true; + options.EnableLogExport = false; + options.EnableSessionPropagation = false; +}); +qylBuilder.Services.AddOpenTelemetry() + .WithTracing(tracing => tracing.AddInMemoryExporter(exportedActivities)) + .WithMetrics(metrics => metrics.AddReader(inMemoryMetricReader)); + +using var meterListener = new MeterListener +{ + InstrumentPublished = (instrument, listener) => + { + if (StringComparer.Ordinal.Equals(instrument.Meter.Name, httpClientMeterName) && + StringComparer.Ordinal.Equals(instrument.Name, httpClientDurationMetricName)) + { + listener.EnableMeasurementEvents(instrument); + } + }, +}; +meterListener.SetMeasurementEventCallback((instrument, measurement, tags, state) => +{ + _ = instrument; + _ = measurement; + _ = state; + if (IsRawApplicationMeasurement(tags, Volatile.Read(ref applicationServerPort))) + Interlocked.Increment(ref rawHttpClientDurationMeasurementCount); +}); +meterListener.Start(); + +using var qylHost = qylBuilder.Build(); +await qylHost.StartAsync(); +#endif var captured = new List(); using var listener = new ActivityListener @@ -73,6 +154,9 @@ using var listener = new ActivityListener ActivitySource.AddActivityListener(listener); await using var server = LoopbackHttpServer.Start(); +#if QYL_SDK_PACKAGE_SMOKE +Volatile.Write(ref applicationServerPort, server.Uri.Port); +#endif using var http = new HttpClient(); var response = await http.GetAsync(server.Uri + "probe?secret=redacted"); await server.RequestCompleted; @@ -87,6 +171,60 @@ logger.Log( exception: null, static (state, exception) => exception is null ? state : state + ":" + exception.GetType().Name); +#if QYL_SDK_PACKAGE_SMOKE +if (!inMemoryMetricReader.Collect(5_000)) + throw new InvalidOperationException("The in-memory metric reader did not complete collection."); + +var exportedQylHttpClientSpanCount = exportedActivities.Count(activity => + StringComparer.Ordinal.Equals(activity.Source.Name, qylActivitySourceName) && + activity.GetTagItem("qyl.instrumentation.domain") is string domain && + StringComparer.Ordinal.Equals(domain, "http.client") && + IsApplicationActivity(activity.TagObjects, applicationServerPort)); +var exportedNativeHttpClientSpanCount = exportedActivities.Count(activity => + StringComparer.Ordinal.Equals(activity.Source.Name, httpClientMeterName) && + IsApplicationActivity(activity.TagObjects, applicationServerPort)); +var exportedHttpClientDurationMetricCount = 0; +var exportedHttpClientDurationSeriesCount = 0; +long exportedHttpClientDurationMeasurementCount = 0; +foreach (var metric in exportedMetrics) +{ + if (!StringComparer.Ordinal.Equals(metric.MeterName, httpClientMeterName) || + !StringComparer.Ordinal.Equals(metric.Name, httpClientDurationMetricName)) + { + continue; + } + + exportedHttpClientDurationMetricCount++; + foreach (ref readonly var point in metric.GetMetricPoints()) + { + if (!IsApplicationMetricSeries(point.Tags, applicationServerPort)) + continue; + + exportedHttpClientDurationSeriesCount++; + exportedHttpClientDurationMeasurementCount += point.GetHistogramCount(); + } +} + +if (exportedQylHttpClientSpanCount != 1 || + exportedNativeHttpClientSpanCount != 0 || + rawHttpClientDurationMeasurementCount != 1 || + exportedHttpClientDurationMetricCount != 1 || + exportedHttpClientDurationSeriesCount != 1 || + exportedHttpClientDurationMeasurementCount != 1) +{ + throw new InvalidOperationException( + "Unexpected Qyl.Sdk telemetry composition: " + + $"qylHttpClientSpans={exportedQylHttpClientSpanCount}, " + + $"nativeHttpClientSpans={exportedNativeHttpClientSpanCount}, " + + $"rawHttpClientDurationMeasurements={rawHttpClientDurationMeasurementCount}, " + + $"exportedHttpClientDurationMetrics={exportedHttpClientDurationMetricCount}, " + + $"exportedHttpClientDurationSeries={exportedHttpClientDurationSeriesCount}, " + + $"exportedHttpClientDurationMeasurements={exportedHttpClientDurationMeasurementCount}."); +} + +await qylHost.StopAsync(); +#endif + Console.WriteLine("logger.calls=" + concreteLogger.Calls.ToString(System.Globalization.CultureInfo.InvariantCulture)); Console.WriteLine("logger.last=" + concreteLogger.Last); @@ -109,6 +247,59 @@ Console.WriteLine("activity.count=" + captured.Count.ToString(System.Globalizati return captured.Count == 2 ? 0 : 3; +#if QYL_SDK_PACKAGE_SMOKE +static bool IsApplicationActivity( + IEnumerable> tags, + int expectedPort) +{ + var addressMatches = false; + var portMatches = false; + foreach (var tag in tags) + { + if (StringComparer.Ordinal.Equals(tag.Key, "server.address")) + addressMatches = StringComparer.Ordinal.Equals(Convert.ToString(tag.Value, System.Globalization.CultureInfo.InvariantCulture), "127.0.0.1"); + else if (StringComparer.Ordinal.Equals(tag.Key, "server.port")) + portMatches = Convert.ToInt32(tag.Value, System.Globalization.CultureInfo.InvariantCulture) == expectedPort; + } + + return addressMatches && portMatches; +} + +static bool IsRawApplicationMeasurement( + ReadOnlySpan> tags, + int expectedPort) +{ + var addressMatches = false; + var portMatches = false; + foreach (var tag in tags) + { + if (StringComparer.Ordinal.Equals(tag.Key, "server.address")) + addressMatches = StringComparer.Ordinal.Equals(Convert.ToString(tag.Value, System.Globalization.CultureInfo.InvariantCulture), "127.0.0.1"); + else if (StringComparer.Ordinal.Equals(tag.Key, "server.port")) + portMatches = Convert.ToInt32(tag.Value, System.Globalization.CultureInfo.InvariantCulture) == expectedPort; + } + + return addressMatches && portMatches; +} + +static bool IsApplicationMetricSeries( + OpenTelemetry.ReadOnlyTagCollection tags, + int expectedPort) +{ + var addressMatches = false; + var portMatches = false; + foreach (var tag in tags) + { + if (StringComparer.Ordinal.Equals(tag.Key, "server.address")) + addressMatches = StringComparer.Ordinal.Equals(Convert.ToString(tag.Value, System.Globalization.CultureInfo.InvariantCulture), "127.0.0.1"); + else if (StringComparer.Ordinal.Equals(tag.Key, "server.port")) + portMatches = Convert.ToInt32(tag.Value, System.Globalization.CultureInfo.InvariantCulture) == expectedPort; + } + + return addressMatches && portMatches; +} +#endif + internal sealed class LoopbackHttpServer : IAsyncDisposable { private readonly TcpListener _listener; @@ -197,12 +388,14 @@ write_package_consumer() { true true Generated + \$(DefineConstants);QYL_SDK_PACKAGE_SMOKE - - - + + + +
@@ -225,17 +418,18 @@ write_projectreference_consumer() { true true Generated + \$(DefineConstants);QYL_PROJECTREFERENCE_SMOKE - + - + @@ -245,6 +439,39 @@ EOF write_program "$dir" } +write_sqlclient_package_consumer() { + local dir="$1" + mkdir -p "$dir" + cat > "$dir/Consumer.csproj" < + + Exe + net10.0 + enable + enable + $PACKAGE_SOURCES + $PACKAGES/sqlclient + true + true + Generated + + + + + + +
+EOF + cat > "$dir/Program.cs" <<'EOF' +using Microsoft.Data.SqlClient; + +Console.WriteLine(typeof(SqlCommand).FullName); +return 0; + +static int CompileTimeProbe(SqlCommand command) => command.ExecuteNonQuery(); +EOF +} + assert_generated_interceptor() { local dir="$1" local count @@ -256,6 +483,21 @@ assert_generated_interceptor() { fi } +assert_sqlclient_package_assets() { + local dir="$1" + local generated + + dotnet restore "$dir/Consumer.csproj" --no-cache --force-evaluate -v quiet + dotnet build "$dir/Consumer.csproj" -c Release --no-restore -v quiet + assert_generated_interceptor "$dir" + generated="$(find "$dir/Generated" -name 'QylAutoInstrumentation.Interceptors.g.cs' -print -quit)" + if ! grep -q 'signals.traces.SQLCLIENT' "$generated" || ! grep -q 'signals.metrics.SQLCLIENT' "$generated"; then + echo "SqlClient-only package consumer did not receive the SQLCLIENT trace+metric generator manifest" >&2 + exit 6 + fi + echo "sqlclient-package-assets-ok" +} + assert_no_aot_warnings() { local log="$1" local consumer="$2" @@ -327,6 +569,8 @@ run_consumer() { write_package_consumer "$WORK/pkg-consumer" run_consumer "package-reference" "$WORK/pkg-consumer" +write_sqlclient_package_consumer "$WORK/sqlclient-pkg-consumer" +assert_sqlclient_package_assets "$WORK/sqlclient-pkg-consumer" if [[ "$MODE" == "local" ]]; then write_projectreference_consumer "$WORK/projref-consumer" run_consumer "project-reference" "$WORK/projref-consumer" diff --git a/tools/verify-aot-autoinstrumentation-goal.py b/tools/verify-aot-autoinstrumentation-goal.py index deae62c..c12f173 100644 --- a/tools/verify-aot-autoinstrumentation-goal.py +++ b/tools/verify-aot-autoinstrumentation-goal.py @@ -34,9 +34,11 @@ ("real aspnetcore demo", [sys.executable, "tools/verify-real-aspnetcore-demo.py"]), ("real aspnetcore metrics demo", [sys.executable, "tools/verify-real-aspnetcore-metrics-demo.py"]), ("real azure demo", [sys.executable, "tools/verify-real-azure-demo.py"]), + ("real corewcf demo", [sys.executable, "tools/verify-real-corewcf-demo.py"]), ("real efcore demo", [sys.executable, "tools/verify-real-efcore-demo.py"]), ("real elasticsearch demo", [sys.executable, "tools/verify-real-elasticsearch-demo.py"]), ("real elastictransport demo", [sys.executable, "tools/verify-real-elastictransport-demo.py"]), + ("real genai demo", [sys.executable, "tools/verify-real-genai-demo.py"]), ("real graphql demo", [sys.executable, "tools/verify-real-graphql-demo.py"]), ("real grpc client demo", [sys.executable, "tools/verify-real-grpc-client-demo.py"]), ("real http client demo", [sys.executable, "tools/verify-real-http-client-demo.py"]), @@ -44,6 +46,7 @@ ("real kafka demo", [sys.executable, "tools/verify-real-kafka-demo.py"]), ("real log4net demo", [sys.executable, "tools/verify-real-log4net-demo.py"]), ("real masstransit demo", [sys.executable, "tools/verify-real-masstransit-demo.py"]), + ("real mcp demo", [sys.executable, "tools/verify-real-mcp-demo.py"]), ("real mongodb demo", [sys.executable, "tools/verify-real-mongodb-demo.py"]), ("real mysqlconnector demo", [sys.executable, "tools/verify-real-mysqlconnector-demo.py"]), ("real mysqldata demo", [sys.executable, "tools/verify-real-mysqldata-demo.py"]), diff --git a/tools/verify-aot-publish-gate.py b/tools/verify-aot-publish-gate.py index 67c4861..2c33f1d 100644 --- a/tools/verify-aot-publish-gate.py +++ b/tools/verify-aot-publish-gate.py @@ -31,6 +31,7 @@ "Qyl.RealGrpcClientDemo", "Qyl.RealHttpClientDemo", "Qyl.RealILoggerDemo", + "Qyl.RealMcpDemo", "Qyl.RealMySqlConnectorDemo", "Qyl.RealNLogDemo", "Qyl.RealNetRuntimeMetricsDemo", diff --git a/tools/verify-aspnetcore-middleware-delegate.py b/tools/verify-aspnetcore-middleware-delegate.py index b9dd490..d416e28 100644 --- a/tools/verify-aspnetcore-middleware-delegate.py +++ b/tools/verify-aspnetcore-middleware-delegate.py @@ -8,7 +8,7 @@ ("Cannot intercept 'next' because it is not an invocation of an ordinary member method"). The generator must not emit an `AspNetCoreRequestDelegate` interceptor for these call sites. -The verifier builds a consumer with `next(context)` middleware and an unrelated HttpClient +The verifier builds a consumer with `next(context)` middleware and an unrelated ILogger control, proving the generator skips delegate invocations while still emitting supported interceptors. """ @@ -31,14 +31,14 @@ TARGETS = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "buildTransitive" / "Qyl.OpenTelemetry.AutoInstrumentation.targets" TARGET_FRAMEWORK = "net10.0" REQUEST_DELEGATE_INTERCEPTOR_TOKEN = "AspNetCoreRequestDelegate_Invoke" -CONTROL_INTERCEPTOR_TOKEN = "global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedHttpClient.GetStringAsync(" +CONTROL_INTERCEPTOR_TOKEN = "global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode.QylInterceptedLogger.Log(" # Convention-based middleware whose next-hop call is a delegate invocation (`next(context)`), -# plus a never-executed HttpClient call as a control so we can prove the generator still emits +# plus a never-executed ILogger call as a control so we can prove the generator still emits # interceptors for ordinary methods and only withholds the un-interceptable delegate invocation. -PROGRAM = """using System.Net.Http; -using Microsoft.AspNetCore.Builder; +PROGRAM = """using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; var builder = WebApplication.CreateSlimBuilder(args); var app = builder.Build(); @@ -46,8 +46,13 @@ app.MapGet("/", () => "ok"); app.Run(); -static async System.Threading.Tasks.Task Probe(HttpClient client) - => _ = await client.GetStringAsync("https://qyl-middleware-delegate.invalid"); +static void Probe(ILogger logger) + => logger.Log( + LogLevel.Information, + new EventId(1, "probe"), + "probe", + exception: null, + static (state, exception) => state); internal sealed class PassThroughMiddleware(RequestDelegate next) { @@ -116,7 +121,7 @@ def main() -> None: if REQUEST_DELEGATE_INTERCEPTOR_TOKEN in text: fail("generator emitted an un-interceptable RequestDelegate.Invoke interceptor (CS9207 risk)") if CONTROL_INTERCEPTOR_TOKEN not in text: - fail("generator emitted no control HttpClient interceptor — it went silent instead of selective") + fail("generator emitted no control ILogger interceptor — it went silent instead of selective") print("aspnetcore-middleware-delegate-ok") diff --git a/tools/verify-contract-coverage-report.py b/tools/verify-contract-coverage-report.py index 8b375ee..a01c806 100644 --- a/tools/verify-contract-coverage-report.py +++ b/tools/verify-contract-coverage-report.py @@ -33,6 +33,7 @@ def build_report(artifacts: ModuleType, contract: dict[str, Any]) -> dict[str, A { "index": int(item["index"]), "contract_item_id": str(item["contract_item_id"]), + "contract_origin": str(item["contract_origin"]), "kind": str(item["kind"]), "key": str(item["key"]), "lane": str(item["lane"]), @@ -46,7 +47,7 @@ def build_report(artifacts: ModuleType, contract: dict[str, Any]) -> dict[str, A ] return { "schema_id": "qyl-aot-autoinstrumentation-contract-coverage-report", - "schema_version": "1.0.0", + "schema_version": "2.0.0", "counts": artifacts.contract_counts(contract), "items": records, } @@ -72,6 +73,8 @@ def main() -> None: print( "contract-coverage-report-ok " f"total={counts['total_contract_items']} " + f"upstream={counts['upstream_contract_items']} " + f"qyl_native={counts['qyl_native_contract_items']} " f"signals={counts['signal_specific_instrumentation_promises']} " f"environment_controls={counts['global_environment_controls']} " f"instrumentation_options={counts['instrumentation_options']}" diff --git a/tools/verify-contract-invariants.py b/tools/verify-contract-invariants.py index 1a9dac0..3a801b7 100644 --- a/tools/verify-contract-invariants.py +++ b/tools/verify-contract-invariants.py @@ -28,15 +28,33 @@ from types import ModuleType from typing import Any +from generator_manifest_coverage import ( + ManifestCoverageError, + parse_generated_interceptor_source, + parse_verified_manifest_artifact, +) + ROOT = Path(__file__).resolve().parents[1] ARTIFACTS_PATH = ROOT / "tools" / "generate-contract-artifacts.py" GENERATOR_PATH = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators" / "QylAutoInstrumentationGenerator.cs" INTERCEPTOR_CATALOG_PATH = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators" / "QylGeneratedSourceInterceptorCatalog.cs" +GENERATED_INTERCEPTOR_SNAPSHOT_PATH = ( + ROOT + / "tests" + / "Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.Snapshots" + / "verified" + / "QylAutoInstrumentation.Interceptors.g.verified.cs" +) +GENERATED_INTERCEPTOR_MANIFEST_ARTIFACT_PATH = ( + GENERATED_INTERCEPTOR_SNAPSHOT_PATH.parent + / "QylAutoInstrumentation.ContractManifests.verified.jsonl" +) OPTIONS_PATH = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "QylAutoInstrumentationOptions.cs" IDS_PATH = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "QylAutoInstrumentationIds.cs" SEMCONV_ATTRIBUTES_PATH = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "QylSemanticAttributes.cs" HANDOFF_GATE_PATH = ROOT / "tools" / "verify-aot-autoinstrumentation-goal.py" +DEMO_SOLUTION_PATH = ROOT / "Qyl.OpenTelemetry.AutoInstrumentation.Demos.slnx" RUNTIME_PROJECT_PATH = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "Qyl.OpenTelemetry.AutoInstrumentation.csproj" METRIC_METERS_PATH = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "QylMetricMeters.cs" METRIC_NAMES_PATH = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "QylMetricNames.cs" @@ -141,10 +159,16 @@ "signals.traces.NSERVICEBUS", "signals.traces.QUARTZ", "signals.traces.WCFCLIENT", + "signals.traces.WCFCORE", + "signals.traces.MICROSOFTEXTENSIONSAI", + "signals.metrics.MICROSOFTEXTENSIONSAI", + "signals.traces.MICROSOFTAGENTSAI", + "signals.metrics.MICROSOFTAGENTSAI", + "signals.traces.MICROSOFTAGENTSAIWORKFLOWS", } # External contract: well-known meter names published by .NET / providers that -# the metrics contract registers via intercepted AddMeter. Values, not C# -# constant names — declarations are free to rename. +# Qyl.Sdk registers explicitly. Values, not C# constant names — declarations +# are free to rename. REQUIRED_REGISTERED_METER_NAME_VALUES = { "Microsoft.AspNetCore.Hosting", "Microsoft.AspNetCore.Routing", @@ -160,17 +184,29 @@ "Microsoft.AspNetCore.Components.Server.Circuits", "System.Net.Http", "System.Net.NameResolution", + "System.Runtime", + "Qyl.OpenTelemetry.AutoInstrumentation.Database", + "Qyl.OpenTelemetry.AutoInstrumentation.NServiceBus", +} +FORBIDDEN_REGISTERED_METER_NAME_VALUES = { "Npgsql", "NServiceBus.Core", "NServiceBus.Core.Pipeline.Incoming", - "System.Runtime", - "Qyl.OpenTelemetry.AutoInstrumentation.Database", } -# External contract: well-known metric instrument names the contract pins. -REQUIRED_METRIC_NAME_VALUES = { - "aspnetcore.components.navigate", - "http.server.request.duration", - "dns.lookup.duration", +# External contract: instrument names emitted by qyl-owned metric producers. +# Native System.Net and ASP.NET Core instruments are selected by meter name and +# must not be mirrored as dead QylMetricNames constants. +QYL_OWNED_METRIC_NAME_VALUES = { + "db.client.operation.duration", + "nservicebus.messaging.operation.duration", + "dotnet.process.cpu.time", + "dotnet.process.memory.working_set", + "dotnet.process.cpu.count", + "dotnet.gc.collections", + "dotnet.gc.last_collection.heap.size", + "dotnet.gc.heap.total_allocated", + "dotnet.thread_pool.queue.length", + "dotnet.thread_pool.thread.count", } # External contract: upstream OTEL .NET auto-instrumentation environment # variable for additional metric sources. @@ -199,8 +235,6 @@ "src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/Semantics/HttpSemantics.cs", } DB_QUERY_TEXT_ALLOWED_PATHS = { - "src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs", - "src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/SqlClient/SqlClientDiagnosticListener.cs", "src/Qyl.OpenTelemetry.AutoInstrumentation.EntityFrameworkCore/EntityFrameworkCoreDiagnosticListener.cs", "src/Qyl.OpenTelemetry.AutoInstrumentation.SqlClient/SqlClientDiagnosticListener.cs", } @@ -357,6 +391,20 @@ def verify_managed_evidence_boundaries(artifacts: ModuleType, contract: dict[str ) +def verify_supported_version_overrides(artifacts: ModuleType, contract: dict[str, Any]) -> None: + items_by_key = { + str(item["key"]): item + for item in artifacts.contract_items(contract) + } + for key, expected in artifacts.QYL_SUPPORTED_VERSION_OVERRIDES.items(): + actual = str(items_by_key[key]["supported_versions"]) + if actual != expected: + fail( + f"qyl supported-version override mismatch for {key}: " + f"expected={expected!r} actual={actual!r}" + ) + + def verify_handoff_real_demo_coverage(artifacts: ModuleType, contract: dict[str, Any]) -> None: evidence_real_demo_verifiers = { evidence @@ -427,17 +475,19 @@ def verify_environment_contract(artifacts: ModuleType, contract: dict[str, Any]) fail(f"global control is not read by QylAutoInstrumentationOptions: {variable}") for item in instrumentation_options: - if item.get("qyl_status") != "option_bound": - continue variable = str(item["environment_variable"]) - if variable not in options: + status = item.get("qyl_status") + if status == "option_bound" and variable not in options: fail(f"instrumentation option is not read by QylAutoInstrumentationOptions: {variable}") + if status == "unsupported_nativeaot" and variable in options: + fail(f"unsupported instrumentation option must be absent from QylAutoInstrumentationOptions: {variable}") + implemented_signals = list(artifacts.implemented_signal_items(contract)) expected_by_signal = { signal: { str(item["instrumentation_id"]) - for item in items - if item["kind"] == artifacts.SIGNAL_KIND and item.get("signal") == signal + for item in implemented_signals + if item.get("signal") == signal } for signal in ["traces", "metrics", "logs"] } @@ -482,6 +532,20 @@ def verify_semconv_attribute_contract() -> None: fail(f"runtime telemetry attribute emission must not use literal keys: {path.relative_to(ROOT)}") +def verify_demo_solution_release_mapping() -> None: + listed_projects = set(re.findall(r' None: meters = METRIC_METERS_PATH.read_text() names = METRIC_NAMES_PATH.read_text() @@ -497,11 +561,17 @@ def verify_metric_contract() -> None: missing_meters = REQUIRED_REGISTERED_METER_NAME_VALUES - registered_meter_values if missing_meters: fail(f"QylMetricMeters must register required well-known meter names: {sorted(missing_meters)}") + forbidden_meters = FORBIDDEN_REGISTERED_METER_NAME_VALUES & registered_meter_values + if forbidden_meters: + fail(f"QylMetricMeters must not register superseded native meter names: {sorted(forbidden_meters)}") metric_name_values = set(parse_string_constants(names).values()) - missing_metric_names = REQUIRED_METRIC_NAME_VALUES - metric_name_values - if missing_metric_names: - fail(f"QylMetricNames must pin required well-known metric names: {sorted(missing_metric_names)}") + if metric_name_values != QYL_OWNED_METRIC_NAME_VALUES: + fail( + "QylMetricNames must contain exactly the qyl-owned instrument names: " + f"missing={sorted(QYL_OWNED_METRIC_NAME_VALUES - metric_name_values)} " + f"extra={sorted(metric_name_values - QYL_OWNED_METRIC_NAME_VALUES)}" + ) if METRICS_ADDITIONAL_SOURCES_VARIABLE not in options: fail( @@ -537,12 +607,27 @@ def verify_sensitive_attribute_emission_policy() -> None: for token in [ "QylCaptureHelpers.RedactQueryValues(", "AspNetCoreUrlQueryRedactionDisabled", - "HttpClientUrlQueryRedactionDisabled", "GraphQlSetDocument", ]: if token not in policy: fail(f"QylSensitiveCapturePolicy must implement the redaction/opt-in controls: {token}") + http_semantics = (ROOT / SENSITIVE_SEMANTIC_WRITER_ALLOWED_PATH).read_text() + for token in [ + "QylCaptureHelpers.FormatUrlFull(", + "HttpClientUrlQueryRedactionDisabled", + ]: + if token not in http_semantics: + fail(f"HttpSemantics must implement the HttpClient url.full redaction control: {token}") + + stale_db_query_text_paths = sorted( + relative_path + for relative_path in DB_QUERY_TEXT_ALLOWED_PATHS + if not (ROOT / relative_path).is_file() + ) + if stale_db_query_text_paths: + fail(f"db.query.text writer allowlist contains missing paths: {stale_db_query_text_paths}") + for root in RUNTIME_EMISSION_ROOTS: for path in root.rglob("*.cs"): relative_path = path.relative_to(ROOT).as_posix() @@ -739,49 +824,18 @@ def parse_emission_descriptor_bodies(generator: str) -> dict[str, str]: return bodies_by_kind -def parse_db_instrumentation_ids(generator: str) -> set[str]: - match = re.search( - r"private static string GetDbInstrumentationId\([^)]*\)\s*\{(?P.*?)\n \}", - generator, - re.DOTALL, - ) - if match is None: - fail("GetDbInstrumentationId helper missing from generator") - - ids = set(re.findall(r'return "([A-Z0-9]+)";', match.group("body"))) - if not ids: - fail("GetDbInstrumentationId must return database instrumentation ids") - - return ids - - -def parse_db_trace_contract_keys(generator: str) -> set[str]: - if re.search(r"TelemetrySignal\.Traces,\s*\n\s*instrumentationId,", generator) is None: - fail("DbCommand target must derive its trace contract key from GetDbInstrumentationId") - - return {f"signals.traces.{db_id}" for db_id in parse_db_instrumentation_ids(generator)} - - -def parse_additional_metric_id_keys(generator: str) -> set[str]: - # Additional signal claims are always metrics and are declared structurally - # as MetricIds("", ...) — never as freeform key strings. - keys: set[str] = set() - for match in re.finditer(r"MetricIds\((.*?)\)", generator, re.DOTALL): - keys.update(f"signals.metrics.{m}" for m in re.findall(r'"([A-Z0-9]+)"', match.group(1))) - - return keys - - -def parse_db_metric_id_keys(generator: str) -> set[str]: - match = re.search( - r"private static [^=]+ GetDbMetricIds\([^)]*\)\s*=>.*?};", - generator, - re.DOTALL, - ) - if match is None: - fail("GetDbMetricIds helper missing from generator") - - return {f"signals.metrics.{m}" for m in re.findall(r'"([A-Z0-9]+)"', match.group(0))} +def verify_generated_interceptor_manifests( + manifests: list[dict[str, Any]], + interceptor_kinds: set[str], + context: str, +) -> set[str]: + emitted_keys: set[str] = set() + for index, manifest in enumerate(manifests): + kind = manifest["interceptorKind"] + if kind not in interceptor_kinds: + fail(f"{context} manifest {index} has unknown interceptor kind: {kind!r}") + emitted_keys.update(manifest["contractKeys"]) + return emitted_keys def verify_matcher_registration(generator: str) -> None: @@ -819,29 +873,7 @@ def verify_interceptor_emission_bodies(generator: str, kinds: set[str]) -> None: fail(f"generator must model emission bodies as a closed self-emitting descriptor hierarchy: {token}") -def collect_generator_target_contract_keys(generator: str) -> set[str]: - # Primary keys are derived, never declared: each target names a - # TelemetrySignal and an InstrumentationId; the key is composed here. - signal_names = {"Traces": "traces", "Metrics": "metrics", "Logs": "logs"} - target_contract_keys = { - f"signals.{signal_names[match.group(1)]}.{match.group(2)}" - for match in re.finditer( - r"TelemetrySignal\.(Traces|Metrics|Logs),\s*\n\s*\"([A-Z0-9]+)\",", - generator, - ) - } - if not target_contract_keys: - fail("no structurally derived target contract keys found in the generator") - target_contract_keys.update(parse_additional_metric_id_keys(generator)) - target_contract_keys.update(parse_db_trace_contract_keys(generator)) - - if "GetDbMetricIds(instrumentationId)" not in generator: - fail("DbCommand target must route metric contract keys through GetDbMetricIds") - target_contract_keys.update(parse_db_metric_id_keys(generator)) - return target_contract_keys - - -def verify_interceptor_target_coverage(generator: str, implemented_signal_keys: set[str]) -> None: +def verify_interceptor_structure(generator: str) -> set[str]: kinds = parse_interceptor_kinds(generator) if not kinds: fail("InterceptorKind enum has no values") @@ -853,7 +885,9 @@ def verify_interceptor_target_coverage(generator: str, implemented_signal_keys: except IndexError: fail("EmitInterceptors dispatch block missing") - if "GetEmissionDescriptor(invocation.Target)" not in dispatch_block: + if "var target = invocation.Target;" not in dispatch_block: + fail("emitter dispatch must retain the matched InterceptorTarget") + if "GetEmissionDescriptor(in target)" not in dispatch_block: fail("emitter dispatch must use the descriptor table") if "private delegate void InterceptorEmitter" in generator or "InterceptorEmitter? Emitter" in generator or "descriptor.Emitter" in dispatch_block: fail("emitter dispatch must not use generic emitter delegates") @@ -861,6 +895,26 @@ def verify_interceptor_target_coverage(generator: str, implemented_signal_keys: # compiler through the abstract Emit member, not by this gate. if "descriptor.Body.Emit(builder, in invocation, index);" not in dispatch_block: fail("emitter dispatch must invoke the polymorphic body emit") + if "EmitInterceptorManifest(builder, in target);" not in dispatch_block: + fail("emitter dispatch must emit one manifest from each matched InterceptorTarget") + + try: + manifest_emitter = generator.split("private static void EmitInterceptorManifest(", 1)[1].split( + "private static void EmitInterceptsLocationAttribute(", + 1, + )[0] + except IndexError: + fail("generated interceptor manifest emitter missing") + for token in [ + "target.Kind", + "target.Signal", + "target.InstrumentationId", + "target.AdditionalMetricIds", + "GetContractKey(target.Signal, target.InstrumentationId)", + "GetContractKey(TelemetrySignal.Metrics, target.AdditionalMetricIds[index])", + ]: + if token not in manifest_emitter: + fail(f"generated interceptor manifest must derive its representation from InterceptorTarget: {token}") try: matcher_dispatch_block = generator.split("private static bool TryGetInvocation(", 1)[1].split( @@ -888,24 +942,8 @@ def verify_interceptor_target_coverage(generator: str, implemented_signal_keys: f"missing={sorted(descriptor_missing)} extra={sorted(descriptor_extra)}" ) - target_missing = { - kind - for kind in kinds - if f"InterceptorKind.{kind}," not in generator and f"= InterceptorKind.{kind};" not in generator - } - if target_missing: - fail(f"InterceptorKind values missing from target construction: {sorted(target_missing)}") - verify_interceptor_emission_bodies(generator, kinds) - - target_contract_keys = collect_generator_target_contract_keys(generator) - missing_contract_keys = implemented_signal_keys - target_contract_keys - extra_contract_keys = target_contract_keys - implemented_signal_keys - if missing_contract_keys or extra_contract_keys: - fail( - "generator target contract key mismatch: " - f"missing={sorted(missing_contract_keys)} extra={sorted(extra_contract_keys)}" - ) + return kinds def verify_generator_keys(artifacts: ModuleType, contract: dict[str, Any]) -> None: @@ -914,22 +952,12 @@ def verify_generator_keys(artifacts: ModuleType, contract: dict[str, Any]) -> No # key literals are unrepresentable — their reappearance is a regression. if re.search(r'"signals\.(?:traces|metrics|logs)\.', generator) is not None: fail("generator must derive contract keys structurally, never declare freeform key literals") - generator_keys = collect_generator_target_contract_keys(generator) implemented_signal_keys = {str(item["key"]) for item in artifacts.implemented_signal_items(contract)} source_interceptor_signal_keys = { str(item["key"]) for item in artifacts.source_interceptor_signal_items(contract) } unsupported_keys = {str(item["key"]) for item in artifacts.unsupported_signal_items(contract)} - if generator_keys != implemented_signal_keys: - fail( - "generator signal key mismatch: " - f"missing={sorted(implemented_signal_keys - generator_keys)} " - f"extra={sorted(generator_keys - implemented_signal_keys)}" - ) - - if generator_keys & unsupported_keys: - fail(f"unsupported keys leaked into generator: {sorted(generator_keys & unsupported_keys)}") for token in FORBIDDEN_GENERATOR_RUNTIME_DISPATCH_TOKENS: if token in generator: @@ -946,11 +974,71 @@ def verify_generator_keys(artifacts: ModuleType, contract: dict[str, Any]) -> No if "InterceptsLocationAttribute(" in generator and "GetInterceptsLocationAttributeSyntax(" not in generator: fail("generator must emit InterceptsLocationAttribute through Roslyn GetInterceptsLocationAttributeSyntax") - verify_interceptor_target_coverage(generator, implemented_signal_keys) - generator_target_keys = collect_generator_target_contract_keys(generator) - missing_source_interceptor_bindings = source_interceptor_signal_keys - generator_target_keys - if missing_source_interceptor_bindings: - fail(f"source_interceptor contract keys missing generator binding: {sorted(missing_source_interceptor_bindings)}") + interceptor_kinds = verify_interceptor_structure(generator) + try: + fixture_manifests = parse_generated_interceptor_source( + GENERATED_INTERCEPTOR_SNAPSHOT_PATH.read_text(encoding="utf-8"), + GENERATED_INTERCEPTOR_SNAPSHOT_PATH, + ) + coverage_manifests = parse_verified_manifest_artifact( + GENERATED_INTERCEPTOR_MANIFEST_ARTIFACT_PATH.read_text(encoding="utf-8"), + GENERATED_INTERCEPTOR_MANIFEST_ARTIFACT_PATH, + ) + except (ManifestCoverageError, OSError) as error: + fail(str(error)) + + verify_generated_interceptor_manifests( + fixture_manifests, + interceptor_kinds, + "two-ILogger deterministic fixture", + ) + fixture_kinds = [manifest["interceptorKind"] for manifest in fixture_manifests] + if fixture_kinds != ["ILoggerLog", "ILoggerLog"]: + fail(f"generated interceptor snapshot kind mismatch: {fixture_kinds}") + fixture_keys = [manifest["contractKeys"] for manifest in fixture_manifests] + if fixture_keys != [["signals.logs.ILOGGER"], ["signals.logs.ILOGGER"]]: + fail(f"generated interceptor snapshot contract-key mismatch: {fixture_keys}") + + emitted_keys = verify_generated_interceptor_manifests( + coverage_manifests, + interceptor_kinds, + "verified live coverage", + ) + emitted_kinds = {str(manifest["interceptorKind"]) for manifest in coverage_manifests} + if emitted_kinds != interceptor_kinds: + fail( + "live generated manifest kinds must exactly match the interceptor catalog: " + f"missing={sorted(interceptor_kinds - emitted_kinds)} " + f"unexpected={sorted(emitted_kinds - interceptor_kinds)}" + ) + unsupported_emitted = emitted_keys & unsupported_keys + if unsupported_emitted: + fail(f"unsupported keys leaked into generated interceptor manifests: {sorted(unsupported_emitted)}") + unknown_emitted = emitted_keys - implemented_signal_keys + if unknown_emitted: + fail(f"generated interceptor manifests contain non-implemented keys: {sorted(unknown_emitted)}") + non_source_interceptors = emitted_keys - source_interceptor_signal_keys + allowed_non_source_interceptors = ( + set(artifacts.IMPLEMENTED_COMPILE_BINDING_ONLY_ALLOWLIST) + | set(artifacts.GENERATED_INTERCEPTOR_ALTERNATE_PATH_SIGNAL_ALLOWLIST) + ) + unexpected_non_source_interceptors = non_source_interceptors - allowed_non_source_interceptors + if unexpected_non_source_interceptors: + fail( + "generated interceptor manifests contain keys not owned by the source_interceptor lane: " + f"{sorted(unexpected_non_source_interceptors)}" + ) + stale_non_source_allowlist = allowed_non_source_interceptors - non_source_interceptors + if stale_non_source_allowlist: + fail(f"generated interceptor non-source allowlist contains stale keys: {sorted(stale_non_source_allowlist)}") + missing_source_interceptors = source_interceptor_signal_keys - emitted_keys + if missing_source_interceptors: + fail( + "source_interceptor contract keys missing from live generated manifests: " + f"{sorted(missing_source_interceptors)}" + ) + if not any(manifest["additionalMetricIds"] for manifest in coverage_manifests): + fail("live generated interceptor manifests do not exercise AdditionalMetricIds") def main() -> None: @@ -960,11 +1048,13 @@ def main() -> None: verify_compile_binding_only_truth_gate(artifacts, contract) verify_conformance_profile_gate(artifacts) verify_managed_evidence_boundaries(artifacts, contract) + verify_supported_version_overrides(artifacts, contract) verify_handoff_real_demo_coverage(artifacts, contract) verify_nativeaot_evidence_is_executable(artifacts, contract) verify_generator_keys(artifacts, contract) verify_environment_contract(artifacts, contract) verify_semconv_attribute_contract() + verify_demo_solution_release_mapping() verify_metric_contract() verify_sensitive_attribute_emission_policy() verify_bounded_activity_name_policy() diff --git a/tools/verify-environment-options-behavior.py b/tools/verify-environment-options-behavior.py index acc0afd..bff17cb 100644 --- a/tools/verify-environment-options-behavior.py +++ b/tools/verify-environment-options-behavior.py @@ -16,13 +16,14 @@ ROOT = Path(__file__).resolve().parents[1] PACK_LOCK_PATH = Path(tempfile.gettempdir()) / "qyl-dotnet-autoinstrumentation-pack.lock" CORE_PROJECT = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "Qyl.OpenTelemetry.AutoInstrumentation.csproj" +DIAGNOSTIC_LISTENERS_PROJECT = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners" / "Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.csproj" +HOSTING_PROJECT = ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation.Hosting" / "Qyl.OpenTelemetry.AutoInstrumentation.Hosting.csproj" TARGET_FRAMEWORK = "net10.0" NUGET_ORG = "https://api.nuget.org/v3/index.json" PROGRAM = r''' using Qyl.OpenTelemetry.AutoInstrumentation; -using Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode; var options = QylAutoInstrumentationOptions.Current; @@ -53,9 +54,8 @@ # External-consumer runtime probe: public API only (no IVT — the assembly name is # deliberately NOT VerifierProbe). Proves option env vars change EMITTED SPANS, -# not merely parsed option values: a real HttpClient call through the generated -# interceptor against a loopback server, asserting url.full redaction and -# captured header attributes on the stopped activity. +# not merely parsed option values: a real HttpClient call through the Hosting runtime +# listener against a loopback server, asserting url.full redaction on the stopped activity. RUNTIME_PROGRAM = r''' using System.Diagnostics; using System.Net; @@ -133,7 +133,7 @@ metric.http=True metric.sql=True log.ilogger=True -meters=Microsoft.AspNetCore.Hosting|Microsoft.AspNetCore.Routing|Microsoft.AspNetCore.Diagnostics|Microsoft.AspNetCore.RateLimiting|Microsoft.AspNetCore.HeaderParsing|Microsoft.AspNetCore.Server.Kestrel|Microsoft.AspNetCore.Http.Connections|Microsoft.AspNetCore.Authorization|Microsoft.AspNetCore.Authentication|Microsoft.AspNetCore.Components|Microsoft.AspNetCore.Components.Lifecycle|Microsoft.AspNetCore.Components.Server.Circuits|System.Net.Http|System.Net.NameResolution|Npgsql|Qyl.OpenTelemetry.AutoInstrumentation.Database|NServiceBus.Core|NServiceBus.Core.Pipeline.Incoming|System.Runtime +meters=Microsoft.AspNetCore.Hosting|Microsoft.AspNetCore.Routing|Microsoft.AspNetCore.Diagnostics|Microsoft.AspNetCore.RateLimiting|Microsoft.AspNetCore.HeaderParsing|Microsoft.AspNetCore.Server.Kestrel|Microsoft.AspNetCore.Http.Connections|Microsoft.AspNetCore.Authorization|Microsoft.AspNetCore.Authentication|Microsoft.AspNetCore.Components|Microsoft.AspNetCore.Components.Lifecycle|Microsoft.AspNetCore.Components.Server.Circuits|System.Net.Http|System.Net.NameResolution|Qyl.OpenTelemetry.AutoInstrumentation.Database|Qyl.OpenTelemetry.AutoInstrumentation.NServiceBus|System.Runtime ef.text=False graphql.document=False oracle.text=False @@ -181,7 +181,7 @@ metric.http=True metric.sql=False log.ilogger=False -meters=Microsoft.AspNetCore.Hosting|Microsoft.AspNetCore.Routing|Microsoft.AspNetCore.Diagnostics|Microsoft.AspNetCore.RateLimiting|Microsoft.AspNetCore.HeaderParsing|Microsoft.AspNetCore.Server.Kestrel|Microsoft.AspNetCore.Http.Connections|Microsoft.AspNetCore.Authorization|Microsoft.AspNetCore.Authentication|Microsoft.AspNetCore.Components|Microsoft.AspNetCore.Components.Lifecycle|Microsoft.AspNetCore.Components.Server.Circuits|System.Net.Http|System.Net.NameResolution|Npgsql|Qyl.OpenTelemetry.AutoInstrumentation.Database|NServiceBus.Core|NServiceBus.Core.Pipeline.Incoming|System.Runtime +meters=Microsoft.AspNetCore.Hosting|Microsoft.AspNetCore.Routing|Microsoft.AspNetCore.Diagnostics|Microsoft.AspNetCore.RateLimiting|Microsoft.AspNetCore.HeaderParsing|Microsoft.AspNetCore.Server.Kestrel|Microsoft.AspNetCore.Http.Connections|Microsoft.AspNetCore.Authorization|Microsoft.AspNetCore.Authentication|Microsoft.AspNetCore.Components|Microsoft.AspNetCore.Components.Lifecycle|Microsoft.AspNetCore.Components.Server.Circuits|System.Net.Http|System.Net.NameResolution|Qyl.OpenTelemetry.AutoInstrumentation.Database|Qyl.OpenTelemetry.AutoInstrumentation.NServiceBus|System.Runtime ef.text=False graphql.document=False oracle.text=False @@ -205,7 +205,7 @@ metric.http=True metric.sql=True log.ilogger=True -meters=Microsoft.AspNetCore.Hosting|Microsoft.AspNetCore.Routing|Microsoft.AspNetCore.Diagnostics|Microsoft.AspNetCore.RateLimiting|Microsoft.AspNetCore.HeaderParsing|Microsoft.AspNetCore.Server.Kestrel|Microsoft.AspNetCore.Http.Connections|Microsoft.AspNetCore.Authorization|Microsoft.AspNetCore.Authentication|Microsoft.AspNetCore.Components|Microsoft.AspNetCore.Components.Lifecycle|Microsoft.AspNetCore.Components.Server.Circuits|System.Net.Http|System.Net.NameResolution|Npgsql|Qyl.OpenTelemetry.AutoInstrumentation.Database|NServiceBus.Core|NServiceBus.Core.Pipeline.Incoming|System.Runtime +meters=Microsoft.AspNetCore.Hosting|Microsoft.AspNetCore.Routing|Microsoft.AspNetCore.Diagnostics|Microsoft.AspNetCore.RateLimiting|Microsoft.AspNetCore.HeaderParsing|Microsoft.AspNetCore.Server.Kestrel|Microsoft.AspNetCore.Http.Connections|Microsoft.AspNetCore.Authorization|Microsoft.AspNetCore.Authentication|Microsoft.AspNetCore.Components|Microsoft.AspNetCore.Components.Lifecycle|Microsoft.AspNetCore.Components.Server.Circuits|System.Net.Http|System.Net.NameResolution|Qyl.OpenTelemetry.AutoInstrumentation.Database|Qyl.OpenTelemetry.AutoInstrumentation.NServiceBus|System.Runtime ef.text=True graphql.document=True oracle.text=True @@ -229,7 +229,7 @@ metric.http=True metric.sql=True log.ilogger=True -meters=Microsoft.AspNetCore.Hosting|Microsoft.AspNetCore.Routing|Microsoft.AspNetCore.Diagnostics|Microsoft.AspNetCore.RateLimiting|Microsoft.AspNetCore.HeaderParsing|Microsoft.AspNetCore.Server.Kestrel|Microsoft.AspNetCore.Http.Connections|Microsoft.AspNetCore.Authorization|Microsoft.AspNetCore.Authentication|Microsoft.AspNetCore.Components|Microsoft.AspNetCore.Components.Lifecycle|Microsoft.AspNetCore.Components.Server.Circuits|System.Net.Http|System.Net.NameResolution|Npgsql|Qyl.OpenTelemetry.AutoInstrumentation.Database|NServiceBus.Core|NServiceBus.Core.Pipeline.Incoming|System.Runtime|YourCompany.CustomMeter|custom.case.Meter +meters=Microsoft.AspNetCore.Hosting|Microsoft.AspNetCore.Routing|Microsoft.AspNetCore.Diagnostics|Microsoft.AspNetCore.RateLimiting|Microsoft.AspNetCore.HeaderParsing|Microsoft.AspNetCore.Server.Kestrel|Microsoft.AspNetCore.Http.Connections|Microsoft.AspNetCore.Authorization|Microsoft.AspNetCore.Authentication|Microsoft.AspNetCore.Components|Microsoft.AspNetCore.Components.Lifecycle|Microsoft.AspNetCore.Components.Server.Circuits|System.Net.Http|System.Net.NameResolution|Qyl.OpenTelemetry.AutoInstrumentation.Database|Qyl.OpenTelemetry.AutoInstrumentation.NServiceBus|System.Runtime|YourCompany.CustomMeter|custom.case.Meter ef.text=False graphql.document=False oracle.text=False @@ -273,11 +273,12 @@ def pack_runtime(feed: Path, env: dict[str, str]) -> None: if fcntl is not None: fcntl.flock(lock, fcntl.LOCK_EX) try: - run_checked( - ["dotnet", "pack", str(CORE_PROJECT), "-c", "Release", "-o", str(feed), "-v", "quiet"], - ROOT, - env, - ) + 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) @@ -290,6 +291,7 @@ def write_project( version: str, assembly_name: str = "Qyl.OpenTelemetry.AutoInstrumentation.VerifierProbe", program: str | None = None, + package_id: str = "Qyl.OpenTelemetry.AutoInstrumentation", ) -> Path: directory.mkdir(parents=True) project_path = directory / "Consumer.csproj" @@ -307,7 +309,7 @@ def write_project( - + ''', @@ -425,6 +427,7 @@ def main() -> None: version, assembly_name="Qyl.OpenTelemetry.RuntimeProbe", program=RUNTIME_PROGRAM, + package_id="Qyl.OpenTelemetry.AutoInstrumentation.Hosting", ) run_checked(["dotnet", "build", str(runtime_project), "-c", "Release", "-v", "quiet"], runtime_project.parent, env) runtime_assembly = runtime_project.parent / "bin" / "Release" / TARGET_FRAMEWORK / "Qyl.OpenTelemetry.RuntimeProbe.dll" diff --git a/tools/verify-generator-snapshots.py b/tools/verify-generator-snapshots.py index d863076..cc0389c 100644 --- a/tools/verify-generator-snapshots.py +++ b/tools/verify-generator-snapshots.py @@ -6,6 +6,8 @@ import subprocess from pathlib import Path +from generator_manifest_coverage import ManifestCoverageError, collect_live_manifest_artifact + ROOT = Path(__file__).resolve().parents[1] FIXTURE_PROJECT = ( @@ -18,6 +20,8 @@ FIXTURE_DIR = FIXTURE_PROJECT.parent GENERATED_ROOT = ROOT / "artifacts" / "obj" / FIXTURE_PROJECT.stem / "generated" VERIFIED_ROOT = FIXTURE_DIR.parent / "verified" +VERIFIED_MANIFEST_ARTIFACT = VERIFIED_ROOT / "QylAutoInstrumentation.ContractManifests.verified.jsonl" +RECEIVED_MANIFEST_ARTIFACT = GENERATED_ROOT / "QylAutoInstrumentation.ContractManifests.received.jsonl" EXPECTED_FILES = { "QylAutoInstrumentation.Interceptors.g.verified.cs": "QylAutoInstrumentation.Interceptors.g.cs", @@ -59,9 +63,14 @@ def run_build() -> None: str(FIXTURE_PROJECT), "-c", "Release", + "--disable-build-servers", "--no-incremental", + "-m:1", + "/nr:false", "-v", "quiet", + "-p:NuGetAudit=false", + "-p:UseSharedCompilation=false", ], cwd=ROOT, text=True, @@ -125,9 +134,31 @@ def compare_snapshots(files: dict[str, Path]) -> None: verify_interceptor_snapshot((VERIFIED_ROOT / "QylAutoInstrumentation.Interceptors.g.verified.cs").read_text(encoding="utf-8")) +def compare_contract_manifest_coverage() -> None: + if not VERIFIED_MANIFEST_ARTIFACT.exists(): + fail(f"missing verified generator manifest artifact: {VERIFIED_MANIFEST_ARTIFACT}") + try: + received = collect_live_manifest_artifact() + except ManifestCoverageError as error: + fail(str(error)) + + verified = VERIFIED_MANIFEST_ARTIFACT.read_text(encoding="utf-8") + if received == verified: + return + + RECEIVED_MANIFEST_ARTIFACT.parent.mkdir(parents=True, exist_ok=True) + RECEIVED_MANIFEST_ARTIFACT.write_text(received, encoding="utf-8") + fail( + "generator contract manifest snapshot mismatch\n" + f"verified={VERIFIED_MANIFEST_ARTIFACT}\n" + f"received={RECEIVED_MANIFEST_ARTIFACT}" + ) + + def main() -> None: run_build() compare_snapshots(generated_files_by_name()) + compare_contract_manifest_coverage() print("generator-snapshots-ok") diff --git a/tools/verify-instrumentation-disabled-behavior.py b/tools/verify-instrumentation-disabled-behavior.py index 08a2670..24cf7a4 100644 --- a/tools/verify-instrumentation-disabled-behavior.py +++ b/tools/verify-instrumentation-disabled-behavior.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -"""Negative-space gate: a DISABLED instrumentation actually emits zero telemetry. +"""Negative-space gate: a disabled instrumentation actually emits zero qyl telemetry. -The interceptor is compiled in unconditionally; disabling is a runtime gate inside the -forwarding helper (QylInterceptedHttpClient.cs: `if (!observation.IsEnabled) return -SendOriginal(...)`). Every other verifier proves the enabled path or that options are *read*; -none fire a real request with instrumentation OFF and assert no span escapes. This does. +The HttpClient runtime listener and ILogger source interceptor are present in the consumer; +this gate fires real operations with instrumentation off and proves their qyl spans disappear. +Native BCL metrics are intentionally outside this gate because qyl controls their SDK +registration, not whether the framework records them. Control (enabled) -> exactly one HttpClient span. Disabled (http) -> zero spans, via OTEL_DOTNET_AUTO_TRACES_HTTPCLIENT_INSTRUMENTATION_ENABLED=false. @@ -27,7 +27,6 @@ PROGRAM = r''' using System.Diagnostics; -using System.Diagnostics.Metrics; using System.Net; using System.Net.Sockets; using System.Text; @@ -43,33 +42,6 @@ }; ActivitySource.AddActivityListener(listener); -// qyl publishes http.client.request.duration on a Meter named System.Net.Http, the -// same identity the BCL uses. qyl measurements carry ONLY http.request.method (+ -// status code); BCL measurements always carry server.address. Count qyl's only. -var qylMeasurements = 0; -using var meterListener = new MeterListener(); -meterListener.InstrumentPublished = static (instrument, l) => -{ - if (instrument.Meter.Name == "System.Net.Http" && instrument.Name == "http.client.request.duration") - l.EnableMeasurementEvents(instrument); -}; -meterListener.SetMeasurementEventCallback((instrument, value, tags, state) => -{ - var hasMethod = false; - var bclShaped = false; - foreach (var tag in tags) - { - if (tag.Key == "http.request.method") - hasMethod = true; - else if (tag.Key == "server.address") - bclShaped = true; - } - - if (hasMethod && !bclShaped) - Interlocked.Increment(ref qylMeasurements); -}); -meterListener.Start(); - await using var server = LoopbackHttpServer.Start(); using var http = new HttpClient(); using (await http.GetAsync(server.Uri + "probe")) @@ -104,7 +76,6 @@ Console.WriteLine("httpclient.spans=" + httpClientSpans.ToString(System.Globalization.CultureInfo.InvariantCulture)); Console.WriteLine("ilogger.spans=" + iloggerSpans.ToString(System.Globalization.CultureInfo.InvariantCulture)); -Console.WriteLine("httpclient.measurements=" + qylMeasurements.ToString(System.Globalization.CultureInfo.InvariantCulture)); return 0; internal sealed class CapturingLogger : ILogger @@ -256,49 +227,38 @@ def main() -> None: run_checked(["dotnet", "build", str(project), "-c", "Release", "-v", "quiet"], project.parent, env) assembly = project.parent / "bin" / "Release" / TARGET_FRAMEWORK / "Consumer.dll" - def expect(spans: int, logs: int, measurements: int) -> str: - return f"httpclient.spans={spans}\nilogger.spans={logs}\nhttpclient.measurements={measurements}" + def expect(spans: int, logs: int) -> str: + return f"httpclient.spans={spans}\nilogger.spans={logs}" - # Control: everything on -> one span per lane, one qyl measurement. - assert_spans("enabled (control)", run_scenario(assembly, env, {}), expect(1, 1, 1)) + # Control: everything on -> one span per lane. + assert_spans("enabled (control)", run_scenario(assembly, env, {}), expect(1, 1)) # Per-integration trace kill switch: only the HttpClient TRACE lane dies. assert_spans( "http trace instrumentation disabled", run_scenario(assembly, env, {"OTEL_DOTNET_AUTO_TRACES_HTTPCLIENT_INSTRUMENTATION_ENABLED": "false"}), - expect(0, 1, 1), + expect(0, 1), ) # Global kill switch: every lane dies. assert_spans( "global instrumentation disabled", run_scenario(assembly, env, {"OTEL_DOTNET_AUTO_INSTRUMENTATION_ENABLED": "false"}), - expect(0, 0, 0), + expect(0, 0), ) # Signal-level switches: exactly one signal dies each time. assert_spans( "traces signal disabled", run_scenario(assembly, env, {"OTEL_DOTNET_AUTO_TRACES_INSTRUMENTATION_ENABLED": "false"}), - expect(0, 1, 1), - ) - assert_spans( - "metrics signal disabled", - run_scenario(assembly, env, {"OTEL_DOTNET_AUTO_METRICS_INSTRUMENTATION_ENABLED": "false"}), - expect(1, 1, 0), + expect(0, 1), ) assert_spans( "logs signal disabled", run_scenario(assembly, env, {"OTEL_DOTNET_AUTO_LOGS_INSTRUMENTATION_ENABLED": "false"}), - expect(1, 0, 1), - ) - # Per-integration switches on the metrics and logs signals. - assert_spans( - "http metrics instrumentation disabled", - run_scenario(assembly, env, {"OTEL_DOTNET_AUTO_METRICS_HTTPCLIENT_INSTRUMENTATION_ENABLED": "false"}), - expect(1, 1, 0), + expect(1, 0), ) assert_spans( "ilogger logs instrumentation disabled", run_scenario(assembly, env, {"OTEL_DOTNET_AUTO_LOGS_ILOGGER_INSTRUMENTATION_ENABLED": "false"}), - expect(1, 0, 1), + expect(1, 0), ) print("instrumentation-disabled-behavior-ok") diff --git a/tools/verify-otlp-receiver.py b/tools/verify-otlp-receiver.py index 66d151b..7b6f0cf 100644 --- a/tools/verify-otlp-receiver.py +++ b/tools/verify-otlp-receiver.py @@ -174,6 +174,8 @@ def pack_local_packages() -> str: for project in ( ROOT / "src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.csproj", ROOT / "src/Qyl.OpenTelemetry.AutoInstrumentation/Qyl.OpenTelemetry.AutoInstrumentation.csproj", + ROOT / "src/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners/Qyl.OpenTelemetry.AutoInstrumentation.DiagnosticListeners.csproj", + ROOT / "src/Qyl.OpenTelemetry.AutoInstrumentation.Hosting/Qyl.OpenTelemetry.AutoInstrumentation.Hosting.csproj", ): run( [ @@ -233,7 +235,7 @@ def write_consumer(version: str, *, published: bool) -> None: {WORK / "packages"} - + diff --git a/tools/verify-real-aspnetcore-demo.py b/tools/verify-real-aspnetcore-demo.py index f785696..48fc80c 100644 --- a/tools/verify-real-aspnetcore-demo.py +++ b/tools/verify-real-aspnetcore-demo.py @@ -66,9 +66,16 @@ def verify_report(name: str, completed: subprocess.CompletedProcess[str], expect fail(f"{name} expected at least 2 ASP.NET Core activities, got {activities!r}") -def run_managed(env: dict[str, str]) -> subprocess.CompletedProcess[str]: - run_checked(["dotnet", "build", str(PROJECT), "-c", "Release", "-v", "quiet"], ROOT, env) - assembly = artifacts_bin_assembly(PROJECT) +def build_managed(env: dict[str, str]) -> Path: + run_checked( + ["dotnet", "build", str(PROJECT), "-c", "Release", "-v", "quiet", "--no-incremental"], + ROOT, + env, + ) + return artifacts_bin_assembly(PROJECT) + + +def run_managed(assembly: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]: return subprocess.run( ["dotnet", str(assembly)], cwd=PROJECT.parent, @@ -80,7 +87,7 @@ def run_managed(env: dict[str, str]) -> subprocess.CompletedProcess[str]: ) -def run_nativeaot(env: dict[str, str]) -> subprocess.CompletedProcess[str]: +def publish_nativeaot(env: dict[str, str]) -> Path: output = artifacts_publish_dir(PROJECT, "nativeaot") run_checked( [ @@ -106,9 +113,13 @@ def run_nativeaot(env: dict[str, str]) -> subprocess.CompletedProcess[str]: if not executable.exists(): fail(f"NativeAOT ASP.NET Core executable missing: {executable}") + return executable + + +def run_nativeaot(executable: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]: return subprocess.run( [str(executable)], - cwd=output, + cwd=executable.parent, env=env, text=True, stdout=subprocess.PIPE, @@ -119,21 +130,23 @@ def run_nativeaot(env: dict[str, str]) -> subprocess.CompletedProcess[str]: def main() -> None: env = clean_env() - managed = run_managed(env) - nativeaot = run_nativeaot(env) - verify_report("managed ASP.NET Core demo", managed, "dynamic-code-supported") - verify_report("NativeAOT ASP.NET Core demo", nativeaot, "nativeaot") - - # Opt-in scenario: header capture + raw url.query on the emitted server span, - # asserted by the demo report itself. Default runs above assert redaction and - # header absence. Reuses the published NativeAOT binary. optin_env = dict(env) optin_env["OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS"] = "X-Demo-Req" optin_env["OTEL_DOTNET_AUTO_TRACES_ASPNETCORE_INSTRUMENTATION_CAPTURE_RESPONSE_HEADERS"] = "X-Demo-Res" optin_env["OTEL_DOTNET_EXPERIMENTAL_ASPNETCORE_DISABLE_URL_QUERY_REDACTION"] = "true" optin_env["AOT_PUBLISH_GATE_SET"] = env.get("AOT_PUBLISH_GATE_SET", "warned") - verify_report("managed ASP.NET Core demo (capture opt-in)", run_managed(optin_env), "dynamic-code-supported") - verify_report("NativeAOT ASP.NET Core demo (capture opt-in)", run_nativeaot(optin_env), "nativeaot") + + managed_assembly = build_managed(env) + managed = run_managed(managed_assembly, env) + managed_optin = run_managed(managed_assembly, optin_env) + nativeaot_executable = publish_nativeaot(env) + nativeaot = run_nativeaot(nativeaot_executable, env) + nativeaot_optin = run_nativeaot(nativeaot_executable, optin_env) + + verify_report("managed ASP.NET Core demo", managed, "dynamic-code-supported") + verify_report("managed ASP.NET Core demo (capture opt-in)", managed_optin, "dynamic-code-supported") + verify_report("NativeAOT ASP.NET Core demo", nativeaot, "nativeaot") + verify_report("NativeAOT ASP.NET Core demo (capture opt-in)", nativeaot_optin, "nativeaot") print("real-aspnetcore-demo-ok") diff --git a/tools/verify-real-azure-demo.py b/tools/verify-real-azure-demo.py index ba008f8..0662d9a 100755 --- a/tools/verify-real-azure-demo.py +++ b/tools/verify-real-azure-demo.py @@ -66,8 +66,8 @@ def verify_report(name: str, completed: subprocess.CompletedProcess[str], expect fail(f"{name} report did not pass:\n{json.dumps(report, indent=2, sort_keys=True)}") activities = report.get("Activities") - if not isinstance(activities, list) or len(activities) != 2: - fail(f"{name} expected exactly 2 Azure activities, got {activities!r}") + if not isinstance(activities, list) or len(activities) != 4: + fail(f"{name} expected exactly 4 Azure activities, got {activities!r}") def run_managed(env: dict[str, str]) -> subprocess.CompletedProcess[str]: diff --git a/tools/verify-real-corewcf-demo.py b/tools/verify-real-corewcf-demo.py new file mode 100644 index 0000000..58f8d0a --- /dev/null +++ b/tools/verify-real-corewcf-demo.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import subprocess +from pathlib import Path +from typing import Any + +from verify_helpers import artifacts_bin_assembly, clean_env, run_checked + + +ROOT = Path(__file__).resolve().parents[1] +PROJECT = ROOT / "demos" / "Qyl.RealCoreWcfDemo" / "Qyl.RealCoreWcfDemo.csproj" + + +def fail(message: str) -> None: + raise SystemExit(message) + + +def parse_report(stdout: str) -> dict[str, Any]: + start = stdout.find("{\n") + if start < 0: + fail(f"CoreWCF demo did not emit a JSON report\nstdout={stdout}") + + try: + report = json.loads(stdout[start:]) + except json.JSONDecodeError as exc: + fail(f"CoreWCF demo emitted invalid JSON: {exc}\nstdout={stdout}") + + if not isinstance(report, dict): + fail(f"CoreWCF report must be an object: {report!r}") + return report + + +def run_demo(env: dict[str, str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["dotnet", str(artifacts_bin_assembly(PROJECT))], + cwd=PROJECT.parent, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + +def verify_demo(completed: subprocess.CompletedProcess[str], *, registration_enabled: bool) -> None: + if completed.returncode != 0: + fail( + "managed CoreWCF demo failed\n" + f"exit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}" + ) + if completed.stderr: + fail(f"managed CoreWCF demo wrote stderr:\n{completed.stderr}") + + report = parse_report(completed.stdout) + if report.get("Pass") is not True: + fail(f"CoreWCF report did not pass:\n{json.dumps(report, indent=2, sort_keys=True)}") + + if report.get("QylRegistrationEnabled") is not registration_enabled: + fail(f"CoreWCF registration state mismatch: {report!r}") + if report.get("QylRegistrationObserved") is not registration_enabled: + fail(f"CoreWCF source subscription mismatch: {report!r}") + + activities = report.get("Activities") + expected_count = 2 if registration_enabled else 0 + if not isinstance(activities, list) or len(activities) != expected_count: + fail(f"expected exactly {expected_count} CoreWCF activities, got {activities!r}") + + +def main() -> None: + env = clean_env() + run_checked( + [ + "dotnet", + "build", + str(PROJECT), + "-c", + "Release", + "-v", + "quiet", + "--disable-build-servers", + "-m:1", + ], + ROOT, + env, + ) + verify_demo(run_demo(env), registration_enabled=True) + + disabled_env = dict(env) + disabled_env["OTEL_DOTNET_AUTO_TRACES_WCFCORE_INSTRUMENTATION_ENABLED"] = "false" + verify_demo(run_demo(disabled_env), registration_enabled=False) + + print("real-corewcf-demo-ok managed-only") + + +if __name__ == "__main__": + main() diff --git a/tools/verify-real-genai-demo.py b/tools/verify-real-genai-demo.py new file mode 100755 index 0000000..58a36a3 --- /dev/null +++ b/tools/verify-real-genai-demo.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import subprocess +from pathlib import Path +from typing import Any + +from verify_helpers import artifacts_bin_assembly, clean_env, run_checked + +ROOT = Path(__file__).resolve().parents[1] +PROJECT = ROOT / "demos" / "Qyl.RealGenAiDemo" / "Qyl.RealGenAiDemo.csproj" +GENAI_SOURCES = { + "Experimental.Microsoft.Extensions.AI", + "Experimental.Microsoft.Agents.AI", + "Microsoft.Agents.AI.Workflows", +} +GENAI_METERS = { + "Experimental.Microsoft.Agents.AI", + "Experimental.Microsoft.Extensions.AI", +} + + +def fail(message: str) -> None: + raise SystemExit(message) + + +def parse_report(stdout: str) -> dict[str, Any]: + start = stdout.find("{\n") + if start < 0: + fail(f"GenAI demo did not emit a JSON report\nstdout={stdout}") + + try: + report = json.loads(stdout[start:]) + except json.JSONDecodeError as exc: + fail(f"GenAI demo emitted an invalid JSON report: {exc}\nstdout={stdout}") + + if not isinstance(report, dict): + fail(f"GenAI demo report must be a JSON object: {report!r}") + return report + + +def verify_report( + name: str, + completed: subprocess.CompletedProcess[str], + *, + expected_qyl_sources: set[str], + expected_qyl_meters: set[str], +) -> None: + if completed.returncode != 0: + fail( + f"{name} failed\n" + f"exit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}" + ) + if completed.stderr: + fail(f"{name} wrote stderr:\n{completed.stderr}") + + report = parse_report(completed.stdout) + if report.get("RuntimeMode") != "dynamic-code-supported": + fail(f"managed GenAI demo runtime mode mismatch: {report.get('RuntimeMode')}") + if report.get("Pass") is not True: + fail(f"managed GenAI demo report did not pass:\n{json.dumps(report, indent=2, sort_keys=True)}") + + for field in ("BareResponseVerified", "AgentResponseVerified", "WorkflowOutputVerified"): + if report.get(field) is not True: + fail(f"managed GenAI demo did not verify {field}: {report!r}") + + activities = report.get("Activities") + if not isinstance(activities, list): + fail(f"managed GenAI demo Activities must be a list: {activities!r}") + source_names = {activity.get("SourceName") for activity in activities if isinstance(activity, dict)} + if source_names != GENAI_SOURCES: + fail(f"{name} source mismatch: expected={GENAI_SOURCES!r} actual={source_names!r}") + + meter_names = report.get("PublishedMeterNames") + if meter_names != sorted(GENAI_METERS): + fail(f"{name} meter mismatch: expected={sorted(GENAI_METERS)!r} actual={meter_names!r}") + + measurements = report.get("Measurements") + if not isinstance(measurements, list) or len(measurements) != 14: + fail(f"managed GenAI demo expected exactly 14 measurements, got {measurements!r}") + + qyl_sources = report.get("QylExportedActivitySources") + if qyl_sources != sorted(expected_qyl_sources): + fail(f"{name} Qyl.Sdk exported source mismatch: expected={sorted(expected_qyl_sources)!r} actual={qyl_sources!r}") + + qyl_meters = report.get("QylExportedMeterNames") + if not isinstance(qyl_meters, list): + fail(f"{name} Qyl.Sdk exported meters must be a list: {qyl_meters!r}") + actual_qyl_meters = set(qyl_meters) & GENAI_METERS + if actual_qyl_meters != expected_qyl_meters: + fail(f"{name} Qyl.Sdk exported meter mismatch: expected={sorted(expected_qyl_meters)!r} actual={qyl_meters!r}") + + +def run_demo(env: dict[str, str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["dotnet", str(artifacts_bin_assembly(PROJECT))], + cwd=PROJECT.parent, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + +def main() -> None: + env = clean_env() + run_checked( + [ + "dotnet", + "build", + str(PROJECT), + "-c", + "Release", + "-v", + "quiet", + "--disable-build-servers", + "-m:1", + ], + ROOT, + env, + ) + verify_report( + "managed GenAI demo", + run_demo(env), + expected_qyl_sources=GENAI_SOURCES, + expected_qyl_meters=GENAI_METERS, + ) + + scenarios = [ + ( + "MEAI traces disabled", + "OTEL_DOTNET_AUTO_TRACES_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED", + GENAI_SOURCES - {"Experimental.Microsoft.Extensions.AI"}, + GENAI_METERS, + ), + ( + "MEAI metrics disabled", + "OTEL_DOTNET_AUTO_METRICS_MICROSOFTEXTENSIONSAI_INSTRUMENTATION_ENABLED", + GENAI_SOURCES, + GENAI_METERS - {"Experimental.Microsoft.Extensions.AI"}, + ), + ( + "Agents traces disabled", + "OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED", + GENAI_SOURCES - {"Experimental.Microsoft.Agents.AI"}, + GENAI_METERS, + ), + ( + "Agents metrics disabled", + "OTEL_DOTNET_AUTO_METRICS_MICROSOFTAGENTSAI_INSTRUMENTATION_ENABLED", + GENAI_SOURCES, + GENAI_METERS - {"Experimental.Microsoft.Agents.AI"}, + ), + ( + "Workflows traces disabled", + "OTEL_DOTNET_AUTO_TRACES_MICROSOFTAGENTSAIWORKFLOWS_INSTRUMENTATION_ENABLED", + GENAI_SOURCES - {"Microsoft.Agents.AI.Workflows"}, + GENAI_METERS, + ), + ] + for name, variable, expected_sources, expected_meters in scenarios: + scenario_env = dict(env) + scenario_env[variable] = "false" + verify_report( + f"managed GenAI demo ({name})", + run_demo(scenario_env), + expected_qyl_sources=expected_sources, + expected_qyl_meters=expected_meters, + ) + + all_disabled_env = dict(env) + for _, variable, _, _ in scenarios: + all_disabled_env[variable] = "false" + verify_report( + "managed GenAI demo (all registrations disabled)", + run_demo(all_disabled_env), + expected_qyl_sources=set(), + expected_qyl_meters=set(), + ) + print("real-genai-demo-ok") + + +if __name__ == "__main__": + main() diff --git a/tools/verify-real-mcp-demo.py b/tools/verify-real-mcp-demo.py new file mode 100644 index 0000000..d5588a0 --- /dev/null +++ b/tools/verify-real-mcp-demo.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +from collections import Counter +import json +import platform +import subprocess +from pathlib import Path +from typing import Any + +from verify_helpers import artifacts_bin_assembly, artifacts_publish_dir, clean_env, run_checked + + +ROOT = Path(__file__).resolve().parents[1] +PROJECT = ROOT / "demos" / "Qyl.RealMcpDemo" / "Qyl.RealMcpDemo.csproj" +MCP_SOURCE = "Experimental.ModelContextProtocol" +SENSITIVE_VALUES = ( + "mcp-sensitive-argument-73f8a9", + "mcp-sensitive-result-9d20c4", +) +EXPECTED_OPERATIONS = { + "initialize": "initialize", + "notifications/initialized": "notifications/initialized", + "tools/list": "tools/list", + "tools/call qyl_sensitive_probe": "tools/call", +} + + +def fail(message: str) -> None: + raise SystemExit(message) + + +def runtime_identifier() -> str: + system = platform.system().lower() + machine = platform.machine().lower() + arm = machine in {"arm64", "aarch64"} + if system == "darwin": + return "osx-arm64" if arm else "osx-x64" + if system == "linux": + return "linux-arm64" if arm else "linux-x64" + if system == "windows": + return "win-arm64" if arm else "win-x64" + fail(f"unsupported NativeAOT MCP gate platform: {system}/{machine}") + + +def parse_report(stdout: str) -> dict[str, Any]: + start = stdout.find("{\n") + if start < 0: + fail(f"MCP demo did not emit a JSON report\nstdout={stdout}") + + try: + report = json.loads(stdout[start:]) + except json.JSONDecodeError as exc: + fail(f"MCP demo emitted invalid JSON: {exc}\nstdout={stdout}") + + if not isinstance(report, dict): + fail(f"MCP report must be an object: {report!r}") + return report + + +def require_tags(activity: dict[str, Any], expected: dict[str, str]) -> None: + tags = activity.get("Tags") + if not isinstance(tags, dict): + fail(f"MCP activity has invalid tags: {activity!r}") + for key, value in expected.items(): + if tags.get(key) != value: + fail(f"MCP activity tag mismatch for {key}: expected={value!r} activity={activity!r}") + + +def verify_report( + name: str, + completed: subprocess.CompletedProcess[str], + expected_runtime_mode: str, + *, + registration_enabled: bool, +) -> None: + if completed.returncode != 0: + fail( + f"{name} failed\n" + f"exit={completed.returncode}\nstdout={completed.stdout}\nstderr={completed.stderr}" + ) + if completed.stderr: + fail(f"{name} wrote stderr:\n{completed.stderr}") + for sensitive in SENSITIVE_VALUES: + if sensitive in completed.stdout: + fail(f"{name} leaked a sensitive tool value to telemetry output: {sensitive}") + + report = parse_report(completed.stdout) + if report.get("RuntimeMode") != expected_runtime_mode: + fail( + f"{name} runtime mode mismatch: " + f"expected={expected_runtime_mode!r} actual={report.get('RuntimeMode')!r}" + ) + if report.get("Pass") is not True: + fail(f"{name} report did not pass:\n{json.dumps(report, indent=2, sort_keys=True)}") + if report.get("ToolListed") is not True or report.get("ToolCalled") is not True: + fail(f"{name} tool discovery/invocation failed: {report!r}") + if report.get("QylRegistrationEnabled") is not registration_enabled: + fail(f"{name} registration state mismatch: {report!r}") + + activities = report.get("Activities") + expected_count = 8 if registration_enabled else 0 + if not isinstance(activities, list) or len(activities) != expected_count: + fail(f"{name} expected exactly {expected_count} activities, got {activities!r}") + if not registration_enabled: + return + if not all(isinstance(activity, dict) for activity in activities): + fail(f"{name} activities must be objects: {activities!r}") + + activity_objects: list[dict[str, Any]] = activities + if {activity.get("Source") for activity in activity_objects} != {MCP_SOURCE}: + fail(f"{name} ActivitySource mismatch: {activity_objects!r}") + if Counter(activity.get("Name") for activity in activity_objects) != Counter( + {name: 2 for name in EXPECTED_OPERATIONS} + ): + fail(f"{name} operation set mismatch: {activity_objects!r}") + + root_trace_id = report.get("RootTraceId") + root_span_id = report.get("RootSpanId") + if not isinstance(root_trace_id, str) or not isinstance(root_span_id, str): + fail(f"{name} report is missing root context: {report!r}") + + sessions: dict[str, set[str]] = {"Client": set(), "Server": set()} + for activity_name, method in EXPECTED_OPERATIONS.items(): + pair = [activity for activity in activity_objects if activity.get("Name") == activity_name] + by_kind = {activity.get("Kind"): activity for activity in pair} + if set(by_kind) != {"Client", "Server"}: + fail(f"{name} activity pair mismatch for {activity_name}: {pair!r}") + + client = by_kind["Client"] + server = by_kind["Server"] + require_tags(client, {"mcp.method.name": method, "network.transport": "pipe"}) + require_tags(server, {"mcp.method.name": method, "network.transport": "pipe"}) + require_tags(client, {"session.id": "qyl-mcp-evidence-session"}) + + if client.get("TraceId") != root_trace_id or client.get("ParentSpanId") != root_span_id: + fail(f"{name} client activity lost its qyl parent for {activity_name}: {client!r}") + if server.get("TraceId") != client.get("TraceId") or server.get("ParentSpanId") != client.get("SpanId"): + fail(f"{name} server activity did not continue client context for {activity_name}: {pair!r}") + + for kind, activity in by_kind.items(): + tags = activity["Tags"] + session = tags.get("mcp.session.id") + if not isinstance(session, str) or not session: + fail(f"{name} {kind} activity is missing mcp.session.id: {activity!r}") + sessions[kind].add(session) + + if any(len(values) != 1 for values in sessions.values()): + fail(f"{name} session identifiers were not stable: {sessions!r}") + if sessions["Client"] == sessions["Server"]: + fail(f"{name} client/server sessions were not distinct: {sessions!r}") + + tool_pair = [ + activity for activity in activity_objects + if activity.get("Name") == "tools/call qyl_sensitive_probe" + ] + for activity in tool_pair: + require_tags( + activity, + { + "gen_ai.tool.name": "qyl_sensitive_probe", + "gen_ai.operation.name": "execute_tool", + "mcp.protocol.version": "2025-11-25", + }, + ) + + +def publish_nativeaot(env: dict[str, str]) -> Path: + output = artifacts_publish_dir(PROJECT, "nativeaot") + run_checked( + [ + "dotnet", + "publish", + str(PROJECT), + "-c", + "Release", + "-r", + runtime_identifier(), + "-p:PublishAot=true", + "-p:TreatWarningsAsErrors=true", + "--self-contained", + "true", + "--disable-build-servers", + "-o", + str(output), + "-v", + "quiet", + ], + ROOT, + env, + ) + executable = output / ("Qyl.RealMcpDemo.exe" if platform.system().lower() == "windows" else "Qyl.RealMcpDemo") + if not executable.exists(): + fail(f"NativeAOT MCP executable missing: {executable}") + return executable + + +def run_nativeaot(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 main() -> None: + env = clean_env() + run_checked( + [ + "dotnet", + "build", + str(PROJECT), + "-c", + "Release", + "-v", + "quiet", + "--disable-build-servers", + "-m:1", + ], + ROOT, + env, + ) + managed = subprocess.run( + ["dotnet", str(artifacts_bin_assembly(PROJECT))], + cwd=PROJECT.parent, + env=env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + verify_report("managed MCP demo", managed, "dynamic-code-supported", registration_enabled=True) + + disabled_env = dict(env) + disabled_env["OTEL_DOTNET_AUTO_TRACES_MCP_INSTRUMENTATION_ENABLED"] = "false" + managed_disabled = subprocess.run( + ["dotnet", str(artifacts_bin_assembly(PROJECT))], + cwd=PROJECT.parent, + env=disabled_env, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + verify_report( + "managed MCP demo (registration disabled)", + managed_disabled, + "dynamic-code-supported", + registration_enabled=False, + ) + + executable = publish_nativeaot(env) + nativeaot = run_nativeaot(executable, env) + verify_report("NativeAOT MCP demo", nativeaot, "nativeaot", registration_enabled=True) + nativeaot_disabled = run_nativeaot(executable, disabled_env) + verify_report( + "NativeAOT MCP demo (registration disabled)", + nativeaot_disabled, + "nativeaot", + registration_enabled=False, + ) + print("real-mcp-demo-ok") + + +if __name__ == "__main__": + main() diff --git a/tools/verify-real-sqlclient-demo.py b/tools/verify-real-sqlclient-demo.py index 2d28af6..ad84f6a 100644 --- a/tools/verify-real-sqlclient-demo.py +++ b/tools/verify-real-sqlclient-demo.py @@ -2,12 +2,14 @@ from __future__ import annotations import json +import importlib.util import os import platform import socket import subprocess import sys from pathlib import Path +from types import ModuleType from typing import Any from verify_container_helpers import run_published_container @@ -15,6 +17,7 @@ ROOT = Path(__file__).resolve().parents[1] PROJECT = ROOT / "demos" / "Qyl.RealSqlClientDemo" / "Qyl.RealSqlClientDemo.csproj" +AOT_PUBLISH_GATE = ROOT / "tools" / "verify-aot-publish-gate.py" TARGET_FRAMEWORK = "net10.0" SQLSERVER_IMAGE = os.environ.get("QYL_SQLSERVER_IMAGE", "mcr.microsoft.com/mssql/server:2022-latest") SQL_PASSWORD = os.environ.get("QYL_SQL_PASSWORD", "Qyl_strong_Password_2026!") @@ -45,7 +48,14 @@ def parse_report(stdout: str) -> dict[str, Any]: return report -def verify_report(name: str, completed: subprocess.CompletedProcess[str], expected_runtime_mode: str, port: int) -> None: +def verify_report( + name: str, + completed: subprocess.CompletedProcess[str], + expected_runtime_mode: str, + expected_trace_owner: str, + expected_metric_count: int, + port: int, +) -> None: if completed.returncode != 0: fail( f"{name} failed\n" @@ -60,20 +70,57 @@ def verify_report(name: str, completed: subprocess.CompletedProcess[str], expect report = parse_report(completed.stdout) if report.get("RuntimeMode") != expected_runtime_mode: fail(f"{name} runtime mode mismatch: expected={expected_runtime_mode} actual={report.get('RuntimeMode')}") + if report.get("TraceOwner") != expected_trace_owner: + fail(f"{name} trace owner mismatch: expected={expected_trace_owner} actual={report.get('TraceOwner')}") if report.get("Pass") is not True: fail(f"{name} report did not pass:\n{json.dumps(report, indent=2, sort_keys=True)}") activities = report.get("Activities") if not isinstance(activities, list) or len(activities) != 4: fail(f"{name} expected exactly 4 SqlClient activities, got {activities!r}") + metrics = report.get("Metrics") + if not isinstance(metrics, list) or len(metrics) != expected_metric_count: + fail(f"{name} expected exactly {expected_metric_count} SqlClient duration metrics, got {metrics!r}") - if f'"server.port": "{port}"' not in completed.stdout: + if expected_trace_owner == "specialist_listener" and f'"server.port": "{port}"' not in completed.stdout: fail(f"{name} did not capture the published SQL Server port {port}\nstdout={completed.stdout}") -def run_managed(env: dict[str, str]) -> subprocess.CompletedProcess[str]: - run_checked(["dotnet", "build", str(PROJECT), "-c", "Release", "-v", "quiet"], ROOT, env) - assembly = artifacts_bin_assembly(PROJECT) +def source_interceptor_property(enabled: bool) -> str: + return f"-p:QylSqlClientSourceInterceptors={'true' if enabled else 'false'}" + + +def load_aot_publish_gate() -> ModuleType: + spec = importlib.util.spec_from_file_location("qyl_aot_publish_gate_for_sqlclient", AOT_PUBLISH_GATE) + if spec is None or spec.loader is None: + fail(f"cannot load NativeAOT publish gate: {AOT_PUBLISH_GATE}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def build_managed(env: dict[str, str], *, source_interceptors: bool) -> Path: + run_checked( + [ + "dotnet", + "build", + str(PROJECT), + "-c", + "Release", + "-v", + "quiet", + "--no-incremental", + "--disable-build-servers", + "-m:1", + source_interceptor_property(source_interceptors), + ], + ROOT, + env, + ) + return artifacts_bin_assembly(PROJECT) + + +def run_managed(assembly: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]: return subprocess.run( ["dotnet", str(assembly)], cwd=PROJECT.parent, @@ -85,19 +132,56 @@ def run_managed(env: dict[str, str]) -> subprocess.CompletedProcess[str]: ) -def run_nativeaot(env: dict[str, str]) -> subprocess.CompletedProcess[str]: - output = artifacts_publish_dir(PROJECT, "nativeaot") - if env.get("AOT_PUBLISH_GATE_SET") not in {"warned", "all"}: - run_checked( - [sys.executable, "tools/verify-aot-publish-gate.py", "--set", "warned", "--demo", - PROJECT.stem, "--strict-promotion", "--keep-publish"], ROOT, env) +def publish_nativeaot(env: dict[str, str], *, source_interceptors: bool) -> Path: + name = "nativeaot" if source_interceptors else "nativeaot-specialist" + output = artifacts_publish_dir(PROJECT, name) + if source_interceptors: + if env.get("AOT_PUBLISH_GATE_SET") not in {"warned", "all"}: + run_checked( + [sys.executable, "tools/verify-aot-publish-gate.py", "--set", "warned", "--demo", + PROJECT.stem, "--strict-promotion", "--keep-publish"], ROOT, env) + else: + gate = load_aot_publish_gate() + rid = runtime_identifier() + code, diagnostics, tail = gate.publish( + PROJECT, + output, + strict=False, + rid=rid, + env=env, + extra_props=[source_interceptor_property(False)], + ) + if code != 0: + detail = diagnostics[0] if diagnostics else tail + fail(f"specialist-only NativeAOT SqlClient publish failed: exit={code}; {detail}") + if not diagnostics: + fail("specialist-only NativeAOT SqlClient publish is now warning-clean; promote its vendor boundary") + valid, detail = gate.validate_warning_policy(PROJECT.stem, PROJECT, diagnostics, rid) + if not valid: + fail(f"specialist-only NativeAOT SqlClient warning policy failed: {detail}") executable = output / ("Qyl.RealSqlClientDemo.exe" if platform.system().lower() == "windows" else "Qyl.RealSqlClientDemo") if not executable.exists(): fail(f"NativeAOT SqlClient executable missing: {executable}") + return executable + + +def runtime_identifier() -> str: + system = platform.system().lower() + machine = platform.machine().lower() + arm = machine in {"arm64", "aarch64"} + if system == "darwin": + return "osx-arm64" if arm else "osx-x64" + if system == "linux": + return "linux-arm64" if arm else "linux-x64" + if system == "windows": + return "win-arm64" if arm else "win-x64" + fail(f"unsupported NativeAOT SqlClient platform: {system}/{machine}") + +def run_nativeaot(executable: Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]: return subprocess.run( [str(executable)], - cwd=output, + cwd=executable.parent, env=env, text=True, stdout=subprocess.PIPE, @@ -129,22 +213,45 @@ def main() -> None: "Encrypt=True;TrustServerCertificate=True;Connect Timeout=5" ) env["QYL_SQLCLIENT_EXPECTED_PORT"] = str(sqlserver.port) - managed = run_managed(env) - nativeaot = run_nativeaot(env) - - # Statement opt-in inside the same container session: the successful SELECT - # span must carry db.query.text when SET_DBSTATEMENT_FOR_TEXT=true (and - # never by default, asserted by the runs above). - optin_env = dict(env) - optin_env["OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT"] = "true" - optin_env["AOT_PUBLISH_GATE_SET"] = env.get("AOT_PUBLISH_GATE_SET", "warned") - managed_optin = run_managed(optin_env) - nativeaot_optin = run_nativeaot(optin_env) - - verify_report("managed SqlClient demo", managed, "dynamic-code-supported", host_port) - verify_report("NativeAOT SqlClient demo", nativeaot, "nativeaot", host_port) - verify_report("managed SqlClient demo (statement opt-in)", managed_optin, "dynamic-code-supported", host_port) - verify_report("NativeAOT SqlClient demo (statement opt-in)", nativeaot_optin, "nativeaot", host_port) + source_env = dict(env) + source_env["QYL_SQLCLIENT_EXPECTED_TRACE_OWNER"] = "source_interceptor" + source_optin_env = dict(source_env) + source_optin_env["OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT"] = "true" + source_managed_assembly = build_managed(source_env, source_interceptors=True) + source_managed = run_managed(source_managed_assembly, source_env) + source_managed_optin = run_managed(source_managed_assembly, source_optin_env) + source_native_executable = publish_nativeaot(source_env, source_interceptors=True) + source_native = run_nativeaot(source_native_executable, source_env) + source_native_optin = run_nativeaot(source_native_executable, source_optin_env) + + specialist_env = dict(env) + specialist_env["QYL_SQLCLIENT_EXPECTED_TRACE_OWNER"] = "specialist_listener" + specialist_optin_env = dict(specialist_env) + specialist_optin_env["OTEL_DOTNET_AUTO_SQLCLIENT_SET_DBSTATEMENT_FOR_TEXT"] = "true" + specialist_managed_assembly = build_managed(specialist_env, source_interceptors=False) + specialist_managed = run_managed(specialist_managed_assembly, specialist_env) + specialist_managed_optin = run_managed(specialist_managed_assembly, specialist_optin_env) + specialist_native_executable = publish_nativeaot(specialist_env, source_interceptors=False) + specialist_native = run_nativeaot(specialist_native_executable, specialist_env) + specialist_native_optin = run_nativeaot(specialist_native_executable, specialist_optin_env) + + source_runs = [ + ("managed SqlClient source-interceptor demo", source_managed, "dynamic-code-supported"), + ("managed SqlClient source-interceptor demo (statement opt-in)", source_managed_optin, "dynamic-code-supported"), + ("NativeAOT SqlClient source-interceptor demo", source_native, "nativeaot"), + ("NativeAOT SqlClient source-interceptor demo (statement opt-in)", source_native_optin, "nativeaot"), + ] + for name, completed, runtime_mode in source_runs: + verify_report(name, completed, runtime_mode, "source_interceptor", 4, host_port) + + specialist_runs = [ + ("managed SqlClient specialist demo", specialist_managed, "dynamic-code-supported"), + ("managed SqlClient specialist demo (statement opt-in)", specialist_managed_optin, "dynamic-code-supported"), + ("NativeAOT SqlClient specialist demo", specialist_native, "nativeaot"), + ("NativeAOT SqlClient specialist demo (statement opt-in)", specialist_native_optin, "nativeaot"), + ] + for name, completed, runtime_mode in specialist_runs: + verify_report(name, completed, runtime_mode, "specialist_listener", 0, host_port) print("real-sqlclient-demo-ok") diff --git a/tools/verify-source-interceptor-consumer.py b/tools/verify-source-interceptor-consumer.py index b77519c..ec143b8 100644 --- a/tools/verify-source-interceptor-consumer.py +++ b/tools/verify-source-interceptor-consumer.py @@ -23,9 +23,7 @@ PROGRAM = r''' using System.Diagnostics; -using System.Net; -using System.Net.Sockets; -using System.Text; +using Microsoft.Extensions.Logging; using Qyl.OpenTelemetry.AutoInstrumentation; var captured = new List(); @@ -37,121 +35,60 @@ }; ActivitySource.AddActivityListener(activityListener); -await using var server = LoopbackHttpServer.Start(HttpStatusCode.InternalServerError, HttpStatusCode.NoContent); -using var http = new HttpClient { BaseAddress = server.Uri }; -http.DefaultRequestHeaders.Add("x-qyl-source", "default-header"); - -try -{ - await http.GetStringAsync("failure"); -} -catch (HttpRequestException exception) +var concreteLogger = new CapturingLogger(); +ILogger logger = concreteLogger; +logger.Log( + LogLevel.Warning, + new EventId(1, "warning"), + "warning", + exception: null, + static (state, exception) => state); +logger.Log( + LogLevel.Error, + new EventId(2, "error"), + "error", + exception: null, + static (state, exception) => state); + +var logActivities = captured + .Where(static activity => activity.TagObjects.Any(static tag => + tag.Key == "qyl.instrumentation.domain" && + StringComparer.Ordinal.Equals(tag.Value as string, "log.ilogger"))) + .ToArray(); +var severities = logActivities + .Select(static activity => activity.GetTagItem("log.severity") as string) + .OfType() + .OrderBy(static severity => severity, StringComparer.Ordinal) + .ToArray(); + +Console.WriteLine("logger.calls=" + concreteLogger.Calls.ToString(System.Globalization.CultureInfo.InvariantCulture)); +Console.WriteLine("activity.count=" + logActivities.Length.ToString(System.Globalization.CultureInfo.InvariantCulture)); +Console.WriteLine("activity.severities=" + string.Join("|", severities)); + +return concreteLogger.Calls == 2 && logActivities.Length == 2 ? 0 : 1; + +internal sealed class CapturingLogger : ILogger { - Console.WriteLine("http.exception.status=" + ((int?)exception.StatusCode)?.ToString(System.Globalization.CultureInfo.InvariantCulture)); -} + public int Calls { get; private set; } -using var content = new StringContent("payload"); -content.Headers.Add("x-qyl-content", "content-header"); -using (await http.PostAsync("content", content)) -{ -} -await server.RequestCompleted; - -var failure = captured.Single(activity => HasTag(activity, "http.response.status_code", "500")); -var failureTags = Tags(failure); -Console.WriteLine("http.activity.status=" + failure.Status); -Console.WriteLine("server.address" + "=" + failureTags["server.address"]); -Console.WriteLine("http.request.header.x-qyl-source=" + failureTags["http.request.header.x-qyl-source"]); -Console.WriteLine("http.response.status_code" + "=" + failureTags["http.response.status_code"]); -Console.WriteLine("error.type" + "=" + failureTags["error.type"]); - -var success = captured.Single(activity => HasTag(activity, "http.response.status_code", "204")); -var successTags = Tags(success); -Console.WriteLine("http.request.header.x-qyl-content=" + successTags["http.request.header.x-qyl-content"]); -Console.WriteLine("activity.count=" + captured.Count.ToString(System.Globalization.CultureInfo.InvariantCulture)); - -return captured.Count == 2 ? 0 : 1; - -static bool HasTag(Activity activity, string key, string expected) - => activity.TagObjects.Any(tag => tag.Key == key && - string.Equals(Convert.ToString(tag.Value, System.Globalization.CultureInfo.InvariantCulture), expected, StringComparison.Ordinal)); - -static Dictionary Tags(Activity activity) - => activity.TagObjects.ToDictionary( - static tag => tag.Key, - static tag => tag.Value is string[] values - ? string.Join(",", values) - : Convert.ToString(tag.Value, System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty, - StringComparer.Ordinal); - -internal sealed class LoopbackHttpServer : IAsyncDisposable -{ - private readonly TcpListener _listener; - - private LoopbackHttpServer(TcpListener listener, HttpStatusCode[] statuses) - { - _listener = listener; - Uri = new Uri($"http://127.0.0.1:{((IPEndPoint)listener.LocalEndpoint).Port}/", UriKind.Absolute); - RequestCompleted = ServeAsync(listener, statuses); - } - - public Uri Uri { get; } - - public Task RequestCompleted { get; } - - public static LoopbackHttpServer Start(params HttpStatusCode[] statuses) - { - var listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(statuses.Length); - return new LoopbackHttpServer(listener, statuses); - } - - public ValueTask DisposeAsync() - { - _listener.Stop(); - return ValueTask.CompletedTask; - } - - private static async Task ServeAsync(TcpListener listener, HttpStatusCode[] statuses) - { - foreach (var status in statuses) - { - using var client = await listener.AcceptTcpClientAsync(); - await using var stream = client.GetStream(); - await ReadRequestAsync(stream); - var reason = status == HttpStatusCode.NoContent ? "No Content" : "Internal Server Error"; - var response = Encoding.ASCII.GetBytes( - $"HTTP/1.1 {(int)status} {reason}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); - await stream.WriteAsync(response); - } - } - - private static async Task ReadRequestAsync(NetworkStream stream) - { - var buffer = new byte[4096]; - var received = new List(); - while (true) - { - var count = await stream.ReadAsync(buffer); - if (count == 0) - throw new InvalidOperationException("Loopback client closed before sending HTTP headers."); - received.AddRange(buffer.AsSpan(0, count).ToArray()); - if (received.Count >= 4 && received.ToArray().AsSpan().IndexOf("\r\n\r\n"u8) >= 0) - return; - } - } + 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++; } ''' -EXPECTED = """http.exception.status=500 -http.activity.status=Error -server.address=127.0.0.1 -http.request.header.x-qyl-source=default-header -http.response.status_code=500 -error.type=500 -http.request.header.x-qyl-content=content-header +EXPECTED = """logger.calls=2 activity.count=2 +activity.severities=Error|Warning """ @@ -189,6 +126,7 @@ def write_project(directory: Path, feed: Path, packages: Path, version: str) -> +
@@ -218,10 +156,9 @@ def verify_generated_source(directory: Path) -> None: text = generated[0].read_text(encoding="utf-8") for token in [ "namespace Qyl.OpenTelemetry.AutoInstrumentation.Generated", - "HttpClient_GetStringAsync_", - "QylInterceptedHttpClient.GetStringAsync(", - "HttpClient_PostAsync_", - "QylInterceptedHttpClient.PostAsync(", + "ILogger_Log_0", + "QylInterceptedLogger.Log(", + '"contractKeys":["signals.logs.ILOGGER"]', ]: if token not in text: fail(f"generated interceptor source missing token: {token}") @@ -257,7 +194,6 @@ def verify_completed(name: str, completed: subprocess.CompletedProcess[str]) -> def main() -> None: env = clean_env() - env["OTEL_DOTNET_AUTO_TRACES_HTTP_INSTRUMENTATION_CAPTURE_REQUEST_HEADERS"] = "x-qyl-source,x-qyl-content" with tempfile.TemporaryDirectory(prefix="qyl-source-interceptor-") as temp: root = Path(temp) feed = root / "feed" diff --git a/tools/verify-version-sync.py b/tools/verify-version-sync.py index 7ef5615..1d96007 100755 --- a/tools/verify-version-sync.py +++ b/tools/verify-version-sync.py @@ -6,6 +6,7 @@ * Directory.Build.props (the release-version owner) must be >= the latest stable v* tag, * any version-pinned README package-reference example must equal that version, + * the generated-code ABI anchor and emitted references must exactly match the package major, * QylInstrumentation.Version must stay DERIVED from the build (no hardcoded semver literal). Any mismatch is a hard build failure. @@ -22,6 +23,22 @@ INSTRUMENTATION = ( ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "QylInstrumentation.cs" ) +GENERATED_CODE_ABI = ( + ROOT / "src" / "Qyl.OpenTelemetry.AutoInstrumentation" / "QylGeneratedCodeAbi.cs" +) +GENERATOR = ( + ROOT + / "src" + / "Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators" + / "QylAutoInstrumentationGenerator.cs" +) +GENERATED_INTERCEPTOR_SNAPSHOT = ( + ROOT + / "tests" + / "Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators.Snapshots" + / "verified" + / "QylAutoInstrumentation.Interceptors.g.verified.cs" +) STABLE_TAG = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") @@ -92,6 +109,40 @@ def check_readme(version: str) -> None: print(f" - {len(refs)} README example(s) all pinned to {version}") +def check_generated_code_abi(version: str) -> None: + package_major = semver(version)[0] + expected_anchor = f"V{package_major}" + expected_declaration = [(expected_anchor, str(package_major))] + abi_text = GENERATED_CODE_ABI.read_text(encoding="utf-8") + declarations = re.findall(r"\bpublic const int (V\d+)\s*=\s*(\d+)\s*;", abi_text) + if declarations != expected_declaration: + fail( + "generated-code ABI declaration must exactly match the package major: " + f"expected={expected_declaration!r} actual={declarations!r}" + ) + + expected_reference = ( + "global::Qyl.OpenTelemetry.AutoInstrumentation.GeneratedCode." + f"QylGeneratedCodeAbi.{expected_anchor}" + ) + reference_pattern = re.compile( + r"global::Qyl\.OpenTelemetry\.AutoInstrumentation\.GeneratedCode\." + r"QylGeneratedCodeAbi\.V\d+" + ) + for label, path in [ + ("source generator", GENERATOR), + ("generated interceptor snapshot", GENERATED_INTERCEPTOR_SNAPSHOT), + ]: + references = set(reference_pattern.findall(path.read_text(encoding="utf-8"))) + if references != {expected_reference}: + fail( + f"{label} must reference exactly {expected_reference}: " + f"actual={sorted(references)!r}" + ) + + print(f" - package major {package_major} exactly matches QylGeneratedCodeAbi.{expected_anchor}") + + def check_version_is_derived() -> None: text = INSTRUMENTATION.read_text(encoding="utf-8") literal = re.search(r'\bVersion\s*=\s*"\d', text) @@ -114,6 +165,7 @@ def main() -> None: print(f"version-sync: single source of truth = Directory.Build.props = {version}") check_props_version(version) check_readme(version) + check_generated_code_abi(version) check_version_is_derived() print("version-sync: OK")