-
Notifications
You must be signed in to change notification settings - Fork 1
Exporting to OpenTelemetry
The Kronikol.Extensions.Otlp package also works in the outbound direction: it exports Kronikol's captured interactions as OpenTelemetry spans over OTLP/HTTP, so the traffic only Kronikol can see — proxy-tap and TCP-tap hops from services that emit no telemetry, handler-captured calls with exact test attribution — appears in Tempo, Jaeger or any collector next to the application's real traces. It is the outbound twin of the same package's receiver-tee (Integration Otlp Extension).
Three layers, each useful alone:
| Layer | Type / verb | When |
|---|---|---|
| Batch push | OtlpExporter |
At the end of a run, from the in-process store or any list of logs — the primary mode; test suites are batch-shaped |
| Streaming sink | OtlpExportSink : IRequestResponseSink |
Live tap topologies — compose onto a ProxyTap/TcpTap/OtlpTap Sink
|
| CLI | kronikol export |
Post-hoc, from NDJSON capture files, in any language's pipeline |
One request/response pair becomes one span: the request supplies the start time and request attributes, the response the end time and status.
| Span field / attribute | Source |
|---|---|
resource service.name
|
CallerName (one resourceSpans entry per caller) |
| scope |
Kronikol + the assembly version |
span name
|
the Kronikol method label (GET, Find ← Trial) |
kind |
CLIENT, or PRODUCER when the pair is an event (MetaType: Event) |
url.full |
Uri |
http.request.method |
Method, when it is an HTTP verb |
http.response.status_code |
the numeric StatusCode; span status ERROR when ≥ 400 (the client-span semconv rule) or when the status is the string form of a failure (Error, Failed, Timeout…) |
db.system.name |
the reverse of the receiver's category mapping (Redis → redis, MongoDB → mongodb, generic Database → other_sql); omitted for non-database calls |
peer.service |
ServiceName |
kronikol.test.id / kronikol.test.name
|
TestId / TestName
|
kronikol.phase |
Phase, when not Unknown
|
kronikol.dependency.category |
DependencyCategory, when set |
kronikol.captured.by |
CapturedBy, when set |
kronikol.request.body / kronikol.response.body
|
the bodies — only with IncludeBodies = true, capped at BodyAttributeCapBytes (default 8 KiB) |
| headers | not exported (size + secret risk outweigh the value; an allow-list option can be added on demand) |
The kronikol.* attribute names are the same vocabulary the taps' own Activities already emit, so everything Kronikol puts on a span reads consistently.
Timestamps are never a reason to drop a record: a request with no timestamp borrows its response's; a pair with none at all is stamped with the export time and marked kronikol.times.synthetic = true.
- A captured W3C trace id always wins (
ActivityTraceId/ActivitySpanId): those spans land in the same distributed trace the system under test emitted, which is the whole point (invariant D4, below). - When no Activity id was captured, the default
TraceIdStrategy.PerTestderives the trace id deterministically fromTestId(theInteractionRecord.ToGuidrecipe — a 32-hex browser-minted test id maps to itself), so one test renders as one trace in Tempo/Jaeger. Without this, capture paths that mint a fresh Guid per pair would flood the backend with thousands of single-span traces.TraceIdStrategy.PerPairkeeps the raw per-pair id for users who want it (--per-pair-traceson the CLI). - The span id is the captured
ActivitySpanId, else the first 16 hex ofRequestResponseId— deterministic, so re-exporting produces identical spans. -
Exported traces are flat: no
parentSpanIdis emitted, so a trace is a fan of sibling spans. That is by design, not a bug — inferring parent/child from caller-name chains and interval nesting is a possible future enhancement.
-
Echo suppression (default). Records with
capturedBy: spanorwire + spancame from the backend's own telemetry via the OTLP tap — re-exporting them would duplicate spans the backend already stores. Opt in withIncludeSpanSourced/--include-span-sourcedif you know the tap was a leaf and the collector never saw them. -
Always skipped: diagram marker records (rendering control, not telemetry) and
TrackingIgnorerecords. - A record whose other half never shows up is still exported — as a zero-duration orphan span marked
kronikol.orphan = true(afterPendingRequestTtl, default 30 s, in the streaming sink). A lone request that carries its own measureddurationMs(the one-record NDJSON contract) is a complete span, not an orphan.
using Kronikol.Extensions.Otlp;
using Kronikol.Tracking;
using var exporter = new OtlpExporter(new OtlpExportOptions
{
Endpoint = new Uri("http://localhost:4318/v1/traces"),
Headers = { ["authorization"] = $"Bearer {token}" }, // the tap's ExpectedHeaders, in reverse
Gzip = true,
});
var result = await exporter.ExportAsync(RequestResponseLogger.RequestAndResponseLogs);
// result.SpansExported / TraceCount / SkippedRecords / OrphanSpans / BatchesFailedBatches are paged by BatchMaxSpans (default 512). A failed batch gets one immediate re-attempt, then is counted and logged — never thrown, and no retry loop that would hold a test process open; result.Success == false tells you the collector was down.
Redaction note: this path is already redacted — RequestResponseLogger.Redaction ran when each entry was logged (see Capture-Time Redaction).
For live tap topologies, OtlpExportSink streams captures out as they happen. Compose it with the tap's normal sink:
var otlp = new OtlpExportSink(new OtlpExportOptions
{
Endpoint = new Uri("http://localhost:4318/v1/traces"),
Name = "otlp-export",
});
proxyTapOptions.Sink = new CompositeRequestResponseSink(
new NdjsonInteractionWriter(".logs/taps/web.ndjson"), // for kronikol ingest
otlp); // and live to the collector
// ... run the tests ...
await otlp.DisposeAsync(); // drains and flushes, orphaning requests that never got a responseThe same works on TcpTapOptions.Sink and OtlpTapOptions.Sink (mind the echo: spans an OtlpTap captured are suppressed by default, see above). The taps redact via their own hooks (SecretDenylist, Key/Value/DocumentRedaction) before the sink, so what streams out is what they would have stored.
The sink obeys D3. Log() is a TryWrite into a bounded channel (QueueCapacity, default 4096) — it never blocks and never throws. A background worker pairs, batches (BatchMaxSpans / FlushInterval, default 2 s) and POSTs. Drops and delivery failures are counted and surfaced as DiagnosticKind.CaptureDegraded entries from Diagnostics() — hand them to IngestRequest.HostDiagnostics (or --diagnostic) so a degraded export is a line in the report, not only in a log. FlushAsync() gives a deterministic flush point; DisposeAsync() waits at most ShutdownTimeout (default 5 s) before cancelling an in-flight POST, so a hung collector cannot hold the process open.
kronikol export <captures.ndjson>... --otlp <endpoint> [--header k=v]...
[--include-bodies] [--body-cap N] [--include-span-sourced]
[--per-pair-traces] [--no-redact] [--redact-header h]...
[--gzip] [--dry-run [--out file.json]]
Reads the same NDJSON capture files kronikol ingest does and POSTs them as spans. --dry-run writes the encoded OTLP/JSON instead of POSTing (to --out, else stdout) — testable without a listener, and a handy debug view of exactly what would leave. Counts are printed (spans / traces / skipped / orphans); exit codes follow kronikol ingest (0 success, 1 runtime failure, 2 usage).
Redaction is per-path, and this is the path that needs it: batch export from the in-process store was redacted at capture, tap-fed sinks were redacted by the taps — but NDJSON files have had nothing run over them (RequestResponseLogger.Redaction applies only on ingest-replay). kronikol export therefore applies CaptureRedaction itself, default on, with --no-redact / --redact-header mirroring kronikol ingest exactly.
The exporter is a standalone HttpClient POSTing OTLP/JSON to a URL. It never touches the system under test's TracerProviderBuilder, never registers processors, never re-emits Activity objects and never flips Activity.Recorded. Exporting Kronikol's captures cannot change what the observed system emits, samples or reports — it only adds spans, under Kronikol's own scope name, to whatever collector you point it at.
Protocol note: the export encoding is OTLP/JSON (application/json on /v1/traces), which collectors accept alongside protobuf. A hand-rolled protobuf encoder is a possible follow-on if a JSON-rejecting endpoint ever shows up in practice.
Two invariants this feature (and the taps) cite by name:
-
D3 — capture never blocks or degrades the observed system. Every Kronikol capture path hands work to a bounded queue and returns; when the queue is full the newest item is dropped and counted, never awaited. A slow report pipeline, a hung collector or a blocked sink can never slow the system under test. The counters surface as
CaptureDegradedreport diagnostics. -
D4 — real W3C ids are preserved end-to-end. When a genuine trace/span id was observed (an incoming
traceparent, a captured Activity, an OTLP span), Kronikol carries it through capture, storage, reports (activityTraceId/activitySpanIdinTestRunReport.json) and now export — so Kronikol's view and the backend's view of the same call always cross-link.
- Integration Otlp Extension — the inbound direction: the OTLP receiver-tee.
- Integration ProxyTap Extension / Integration TcpTap Extension — the taps whose captures this exports.
- Ingesting External Captures — the NDJSON format the CLI reads.
- Capture-Time Redaction — what has been redacted where.
Getting Started
Common Tasks
Integration Guides
- Integration xUnit3
- Integration xUnit2
- Integration NUnit
- Integration MSTest
- Integration TUnit
- Integration BDDfy xUnit3
- Integration LightBDD xUnit2
- Integration LightBDD xUnit3
- Integration LightBDD TUnit
- Integration ReqNRoll xUnit2
- Integration ReqNRoll xUnit3
- Integration ReqNRoll TUnit
- Integration Playwright
Uninstrumentable / polyglot backends
- Integration ProxyTap Extension
- Integration TcpTap Extension
- Integration Otlp Extension
- Exporting to OpenTelemetry
- Ingesting External Captures
- Integration Cucumber Messages
- Capture-Time Redaction
Extensions
- Integration AtlasDataApi Extension
- Integration BigQuery Extension
- Integration Bigtable Extension
- Integration BlobStorage Extension
- Integration ClickHouse Extension
- Integration CloudStorage Extension
- Integration CosmosDB Extension
- Integration Dapper Extension
- Integration DynamoDB Extension
- Integration EF Core Relational Extension
- Integration Elasticsearch Extension
- Integration EventBridge Extension
- Integration EventHubs Extension
- Integration Grpc Extension
- Integration Kafka Extension
- Integration MassTransit Extension
- Integration MongoDB Extension
- Integration MySqlConnector Extension
- Integration Npgsql Extension
- Integration Oracle Extension
- Integration PubSub Extension
- Integration Redis Extension
- Integration S3 Extension
- Integration ServiceBus Extension
- Integration SNS Extension
- Integration Spanner Extension
- Integration SqlClient Extension
- Integration Sqlite Extension
- Integration SQS Extension
- Integration StorageQueues Extension
- Integration OpenTelemetry Extension
- Integration DispatchProxy Extension
- Integration MediatR Extension
- Integration PlantUML IKVM
Configuration
- Tracking Dependencies
- Tracking Custom Dependencies
- HTTP Tracking Setup
- Report Configuration
- Diagram Customisation
- Phase-Aware Tracking
- Content Formatting
- PlantUML Server Configuration
Features
- Generated Reports
- Search Syntax
- Component Diagrams
- PlantUML Browser Rendering
- Inline SVG Rendering
- Internal Flow Tracking
- Tags and Attributes
- Excluding Requests
- Excluded Headers
- Multi-Host Test Architectures
- Event-Driven Architecture Testing
- Service Bus Tracking Patterns
- Background Thread Correlation
- Parallel-Safe Background Correlation
- Event & Message Tracking
- Assertion Tracking
- Step Tracking
- Tabular Attributes
- Large Response and Diagram Handling
- Querying Reports
- Diagnostics and Debugging
- CI Summary Integration
- CI Artifact Upload
- Merging Parallel Reports
Reference