Qyl.Telemetry.Testing
3.0.0
See the version list below for details.
dotnet add package Qyl.Telemetry.Testing --version 3.0.0
NuGet\Install-Package Qyl.Telemetry.Testing -Version 3.0.0
<PackageReference Include="Qyl.Telemetry.Testing" Version="3.0.0" />
<PackageVersion Include="Qyl.Telemetry.Testing" Version="3.0.0" />
<PackageReference Include="Qyl.Telemetry.Testing" />
paket add Qyl.Telemetry.Testing --version 3.0.0
#r "nuget: Qyl.Telemetry.Testing, 3.0.0"
#:package Qyl.Telemetry.Testing@3.0.0
#addin nuget:?package=Qyl.Telemetry.Testing&version=3.0.0
#tool nuget:?package=Qyl.Telemetry.Testing&version=3.0.0
Qyl.Telemetry.Testing
Capture OTLP telemetry inside a .NET test and inspect what was exported. The receiver accepts traces, logs and metrics
over OTLP/HTTP protobuf and OTLP/gRPC, including gzip. It keeps typed values, resource and scope metadata, ordered span events and
links, named log events and all five metric shapes. Fully read protobuf exports are retained before decoding in
Snapshot().Requests, including unknown fields and rejected records. JSON captures include every metric's data shape
and points; the original payload provides the authoritative protobuf representation.
The package targets .NET 10 and uses the shared Microsoft.AspNetCore.App framework. Its native test integration uses
MSTest 4.5.1 and Microsoft.Testing.Platform 2.5.1. Version 3.0.0 consumes SemanticConventions 13.0.0. Application attributes keep their
original names and types; read them through SpanRecord.Attributes and the typed getters.
Capture a trace
Configure the application's OTLP exporters with the endpoint supplied to the action. Each action has a W3C trace root
and its own receiver. For the .NET OTLP exporter, use new Uri(capture.Endpoint, "v1/traces"), "v1/logs" or
"v1/metrics" with OtlpExportProtocol.HttpProtobuf. For gRPC, set OtlpExportProtocol.Grpc and use capture.GrpcEndpoint
without a signal path. Both endpoints bind to separate dynamic loopback ports. Propagate capture.TraceParent when
calling a separate service.
var trace = await QylTest.TraceAsync(
async capture =>
{
// Configure and start the application with capture.Endpoint.
// Propagate capture.TraceParent in the request's traceparent header.
await ExerciseApplicationAsync(capture.Endpoint, capture.TraceParent);
},
until: trace => trace.Spans.Any(span => span.Name == "checkout"),
cancellationToken: TestContext.CancellationToken);
Assert.HasCount(1, trace.Services);
Assert.IsTrue(trace.Spans.Any(span => span.Name == "checkout"));
ExerciseApplicationAsync above is the application's own startup/request code. The receiver does not start a service or
configure an exporter on its behalf. Stop spans and flush exporters before returning from the action. TraceAsync waits
for the supplied evidence condition, with a ten-second default deadline. A missing condition times out; cancellation and
exceptions remain failures.
trace.Capture contains every received record, including records from other trace IDs. trace.Spans and trace.Logs
select the action's trace. Services, Calls and StoresOf(service) derive service and database relationships from
those spans. SessionIds contains observed session.id values; SessionId is populated only when exactly one was
observed. A trace ID is not a session ID.
Repeated exports remain visible in Spans and Capture. The derived call graph uses unique span IDs; separate client/server pairs between the same services remain separate calls.
Native MSTest integration
Standalone TraceAsync needs no custom test class. To share a root across an entire MSTest invocation, replace
[TestClass] with [QylTelemetry] and add these native lifecycle hooks to the consuming test assembly:
[TestClass]
public static class TelemetryLifecycle
{
[GlobalTestInitialize]
public static void Initialize(TestContext context) => QylTestHooks.Initialize(context);
[GlobalTestCleanup]
public static void Cleanup(TestContext context) => QylTestHooks.Cleanup(context);
}
The wrapper delegates execution to the original TestMethodAttribute. Native data-row identity, source navigation,
duration, assertions and exceptions remain MSTest's responsibility. Each invocation has a separate root. The hooks
expose its trace ID as TestContext.Properties[QylTestHooks.TraceIdProperty]; they do not register themselves
automatically from a referenced assembly.
Metrics and conformance
A metric point usually has no trace ID. Add capture.Partition.ResourceAttribute and capture.Partition.Value to the
exporting resource to explicitly identify this test's metrics, or export an exemplar carrying the trace ID.
trace.Metrics identifies the matching point indexes and whether the evidence came from a resource partition or an
exemplar. Other points remain visible in Capture; they are not attributed to the trace.
RegistryConformance.Check(trace.Capture) audits observed attribute types, registered metric units and deprecations
across spans, logs, resource/scope attributes, events, links, all metric point shapes and exemplars. Custom names are
listed separately and remain valid. Deprecations are visible improvement advice; type and unit mismatches are
violations. An empty capture cannot pass. The audit does not infer SDK instruments from exported aggregations.
trace.Conformance(definition, select) checks an explicitly selected SpanDefinition<TKind> from
Qyl.Telemetry.SemanticConventions. It checks span kind, required attributes, supported value types and deprecations in
that definition. Missing conditional requirements are reported as unevaluated; an empty selection cannot pass. The
explicit definition check does not evaluate arbitrary condition text, allowed enum members or span-name patterns; the
separate registry audit does not infer definition-specific required attributes.
Receiver contract
OtlpReceiver.StartAsync() exposes Endpoint for HTTP and GrpcEndpoint for HTTP/2 gRPC, each bound directly to an available loopback port. Snapshot() returns
received records; AwaitAsync(condition, timeout, cancellationToken) waits for a condition without polling sleeps.
HTTP failures return a decodable Google protobuf Status and a developer-facing message, with the response encoding
matched to the request. HTTP status remains authoritative. The receiver accepts protobuf exports; unsupported JSON
exports receive a JSON-encoded error. Profiling-only string/key table references are ignored non-fatally: values become
empty, unresolved keys are absent, and each warning remains in the export receipt and OTLP response. A warning does not
increment the rejected-record count. Full receipt diagnostics remain available when optional response text is bounded.
OtlpExportRequest.Transport and ExportReceipt.Transport identify how each export arrived. gRPC returns native status
codes for transport failures and the same per-record partial-success acknowledgements as HTTP. Malformed protobuf remains
in the original requests and returns InvalidArgument. The framework rejects oversized messages before capture; gzip
decompression is bounded while reading. Incomplete or cancelled uploads do not create a decoded export.
A cancelled wait does not evaluate its condition and retains the caller's cancellation token. Dispose the receiver to close both listeners and wake pending waits.
The wire model comes from OpenTelemetry protobuf v1.11.1. Generated DTOs are internal. Canonical span, resource and scope models come from Qyl.Telemetry.SemanticConventions. Values preserve their wire types, including signed 64-bit integers, doubles, bytes, arrays and maps. Unsigned timestamps and metric counts remain unsigned. Absent optional values remain absent.
The HTTP receiver supports protobuf, not OTLP JSON. Unknown HTTP signal routes return 404; unsupported content types or
encodings return 415. Both transports default to 16 MiB. HTTP uses ASP.NET Core request decompression and Kestrel's
body-size limit: encoded wire bytes and decompressed bytes are bounded independently, including gzip requests without a
Content-Length. Kestrel counts HTTP chunk framing against its wire budget. HTTP size failures return 413 with a
protobuf Status; malformed gzip or protobuf returns 400. gRPC bounds each encoded and decompressed message, returning
ResourceExhausted for overflow and InvalidArgument for malformed protobuf. Size diagnostics report the configured
byte limit.
Pass maxPayloadBytes to choose a different limit for both transports when starting a receiver. The production
Collector's separate gRPC default is 4 MiB; select that value explicitly when matching its default:
await using var receiver = await OtlpReceiver.StartAsync(
maxPayloadBytes: 4 * 1024 * 1024,
cancellationToken: TestContext.CancellationToken);
QylTest.TraceAsync also accepts maxPayloadBytes as its third argument. Limits must be positive and are validated
before either listener is opened.
The receiver is created through StartAsync. Duplicate attribute keys and malformed IDs reject the affected record and
return an OTLP partial-success count; accepted records remain captured. This in-process receiver is intended for tests, not production ingestion.
Develop and release
Run ./build.sh Check for the native suite, an unfiltered coverage report and JetBrains InspectCode at INFO severity.
Tests and inspections must pass; coverage is informational, without a blanket percentage target for library and
dependency implementation. ./build.sh Pack includes those checks and packages the library. The deliberately failing
probe project is built as a dependency and executed by passing parent tests that assert the exact failures, discovery
locations and data-row identities.
The full coverage report includes every instrumented dependency as well as this library. The owner removed blanket 100%
library thresholds on 16 October 2026; tests focus on behavior and invariants. Earlier failed threshold runs and
symbol-provenance evidence remain in eng/verification.json. Both the native DotCov summary and raw Cobertura report
are retained. DotCov unions hits by source file and line; the raw report counts generated initializer and lambda entries
separately on the same line. The exact counts and unexecuted lambda entries are recorded in eng/verification.json; no
coverage exclusion is involved. The full test review is in TEST-REVIEW.md. Proto provenance is
recorded in src/Qyl.Telemetry.Testing/Protos/upstream.json; third-party licensing is
in THIRD-PARTY-NOTICES.md.
Releases use ./release.sh <version> after changing the version and changelog. A release is complete only after the
public package is indexed and its consumer smoke succeeds. Local candidates and passing source tests do not establish
public availability.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Google.Api.CommonProtos (>= 2.17.0)
- Google.Protobuf (>= 3.36.2)
- Grpc.AspNetCore.Server (>= 2.84.0)
- MSTest.TestFramework (>= 4.5.1)
- Qyl.Telemetry.SemanticConventions (>= 13.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.