-
Notifications
You must be signed in to change notification settings - Fork 1
Integration ProxyTap Extension
The Kronikol.Extensions.ProxyTap package is Kronikol's first out-of-process capture component. A ProxyTap is a transparent HTTP tee: it listens on a port, forwards every request byte-for-byte to the real service, and records a copy of each exchange as a Kronikol request/response pair attributed to the running test. Nothing inside the services needs to change.
Use it when the system under test is polyglot, third-party or legacy — services you cannot add TestTrackingMessageHandler / TestTrackingContextMiddleware to — and you still want per-test sequence diagrams of every hop.
Three capture topologies. Kronikol's classic model (topology A) instruments the system under test: handlers and middleware inside the backend record the calls. The proxy-tap model (topology B) leaves the backend untouched: a browser or test fixture stamps identity headers, they ride the real request through the services, and a tap on each hop is the sink. The span model (topology C, Integration Otlp Extension) tees the OTLP traces the services already export and maps their client spans. All three produce the same
RequestResponseLogentries and the same reports.
Tapping a database hop?
ProxyTapspeaks HTTP. For the Redis and MongoDB hops of the same stack use Integration TcpTap Extension — the same tee shape and the same options, over a byte-for-byte TCP tee with RESP and OP_MSG decoders, producing arrows identical to the in-process Integration Redis Extension / Integration MongoDB Extension.
When a hop cannot be proxied — a binary or compressed protocol, an in-process driver, a cloud SDK — use Integration Otlp Extension instead, or as well: it attributes by W3C trace id rather than by header, and
kronikol ingest --merge-duplicatesfolds the two views of a call that both taps saw into one arrow (see Ingesting External Captures).
dotnet add package Kronikol.Extensions.ProxyTapNo ASP.NET Core host is required — the tap is built on HttpListener and binds localhost without URL ACLs.
browser/test ──► tap :8082 ──► graphql :8081 ──► tap :9192 ──► myDotnetService :9091 ──► ...
│ records │ records
▼ ▼
RequestResponseLogger / NDJSON file (IRequestResponseSink)
For every exchange the tap:
-
Resolves the test identity from the inbound request —
test-tracking-current-test-name/test-tracking-current-test-id(the same headersTestTrackingContextMiddlewarereads), then any configured fallback headers, then (by default) the W3Ctraceparenttrace id. The traceparent fallback is what makes browser-driven suites work: the test mints the trace, every browser request carries it, and downstream hops that drop the custom headers still carrytraceparent. -
Re-injects the four correlation headers (
test-tracking-current-test-name,-current-test-id,-caller-name,-trace-id) on the forwarded request when missing, so attribution survives a hop that would otherwise drop them. -
Forwards the original bytes (hop-by-hop headers handled;
Hostrewritten to the target). When anActivityListeneris attached to theKronikol.ProxyTapsource it emits a server span (parented on the inboundtraceparent) and a client span, and re-parents the forwardedtraceparenton the client span — the downstream service then nests under the tap in your distributed trace. Without a listener it is fully transparent. - Responds first, records second — capture decodes (gzip/deflate/br) and caps a copy of both bodies after the response has been written back, so it never sits on the request path.
-
Redacts secrets at capture —
authorization,proxy-authorization,cookie,set-cookie,x-api-key, … are replaced with[REDACTED](or dropped) before the entry reaches any sink. This is the security boundary: the secret never enters the in-memory store,TestRunReport.json, or an NDJSON file. (ExcludedHeadersonly hides headers in the diagram — see Capture-Time-Redaction.) -
Logs a
Requestand aResponseentry (sharedTraceId/RequestResponseId,ServiceName/CallerNamefrom the options, status, timestamps,ActivityTraceId/ActivitySpanId) to the configured sink.
Requests with no resolvable identity (health probes, warm-ups) are forwarded but not captured unless CaptureUnattributedRequests is set.
using Kronikol.Extensions.ProxyTap;
await using var tap = new ProxyTap(new ProxyTapOptions
{
ListenPort = 8082,
ForwardBaseUri = new Uri("http://localhost:8081"),
CallerName = "web", // the participant that dials the tap
ServiceName = "graphql", // the participant the tap forwards to
});
await tap.StartAsync();
// Point the caller at :8082 instead of :8081 — it sees identical traffic.Then generate reports exactly as you would for in-process tracking (ReportGenerator.CreateStandardReportsWithDiagrams(features, start, end, options)); every tapped call renders as an arrow between web and graphql in the scenario whose Id equals the resolved test id.
services.AddProxyTapTestTracking(o =>
{
o.ListenPort = 8082;
o.ForwardBaseUri = new Uri("http://localhost:8081");
o.CallerName = "web";
o.ServiceName = "graphql";
});One IHostedService per tap starts it with the host and stops it on shutdown. Call it once per hop (a ProxyTap per listen → forward pair); the taps are resolvable as IEnumerable<ProxyTap>.
-
.NET + Playwright — use Integration Playwright (
browser.NewTrackedContextAsync(identity)): the four headers plus atraceparentland on every browser request. -
Any other client — send the headers named in
Kronikol.Constants.TestTrackingHttpHeaders, or just a W3Ctraceparentwhose trace id you use as the scenario id. -
In-process HttpClient —
TestTrackingMessageHandleralready stamps them.
| Option | Default | Meaning |
|---|---|---|
ListenPort / ListenHost
|
— / localhost
|
Where the tap listens. |
ForwardBaseUri |
— | The real service (scheme + host + port). |
CallerName / ServiceName
|
— | The two participants this hop joins. |
DependencyCategory / CallerDependencyCategory
|
null |
A DependencyCategories value for shape/colour (e.g. BigQuery, AI). |
CaptureBodies / BodyCapBytes
|
true / 65536
|
Capture bodies; truncate decoded text beyond the cap (…truncated (N chars total)). |
HeaderPolicy |
AllExceptSecrets |
All, AllExceptSecrets, Whitelist (+ HeaderWhitelist), None. |
SecretDenylist / DropSecretHeaders / RedactedValue
|
CaptureRedaction.DefaultSecretHeaders / false / [REDACTED]
|
Capture-time secret handling. |
ReinjectCorrelation |
true |
Re-stamp missing test-tracking-* headers on the forwarded request. |
IdentityFromTraceparent |
true |
Use the inbound traceparent trace id as the test id when no id header is present. |
TestNameHeaderFallbacks / TestIdHeaderFallbacks
|
empty | Extra headers to read name/id from (legacy header names). |
IdentityResolver |
null |
Custom (headers, traceparent) → (name, id)? override. |
FallbackTestName |
Unknown |
Name when only an id is known (the tests file / kronikol ingest normalises it). |
CaptureUnattributedRequests |
false |
Capture requests with no identity (under FallbackTestName). |
FallbackTestId |
null |
With CaptureUnattributedRequests, use this fixed id for all unattributed traffic (one "outside any test" scenario) instead of a fresh id per request. |
Sink |
RequestResponseLoggerSink.Instance |
Where entries go; combine with NdjsonInteractionWriter via CompositeRequestResponseSink. |
Phase |
Unknown |
TestPhase stamped on entries. |
EmitActivities / SynthesizeTraceparent
|
true / true
|
Spans on ProxyTap.ActivitySource; mint a traceparent when the caller sent none. |
ForwardTimeout / ConnectTimeout
|
200 s / 5 s | Upstream deadlines. |
InFlightRegistry |
null |
Publish the identity of every request in flight to ServiceName, so a capturer that cannot read headers can attribute what it sees — see In-flight identity. |
Log / Name
|
null |
Diagnostics callback and display name. |
Added in v3.0.45
A tap on an HTTP hop reads the test identity straight off the request headers. A tap on a database connection cannot: the Redis and MongoDB wire protocols have nowhere to put a test-tracking-current-test-id, and the connection is pooled besides. The only honest source left is which test was the service handling when the query left it — and the HTTP tap in front of that service knows.
InFlightIdentityRegistry is that channel. Point one or more taps at it and each registers every request it forwards, for as long as it is in flight:
var inFlight = new InFlightIdentityRegistry(); // or InFlightIdentityRegistry.Shared
services.AddProxyTap(o =>
{
o.CallerName = "graphql";
o.ServiceName = "myDotnetService";
o.InFlightRegistry = inFlight; // publish who myDotnetService is working for
// …
});
// The database capturer asks, per captured command:
var identity = inFlight.MostRecentFor("myDotnetService");
var testId = identity?.Id ?? fallbackTestId;| Member | |
|---|---|
Register(serviceName, testName, testId) |
Returns a handle; dispose it when the request completes. The tap does this in a using covering every exit, so a failed forward cannot leave a stale identity behind. |
MostRecentFor(serviceName) |
The identity of the most recently started request still in flight, or null when the service is idle. Most recent, not first: a call a service makes belongs to the request it is currently handling, and nested work always starts after its cause. |
CountFor(serviceName) / ActiveServices
|
Diagnostics. |
Clear() |
Forget everything (tests, restarts). |
Shared |
A process-wide instance for hosts that would rather not thread one through. |
Service names match case-insensitively, and the whole thing is thread-safe.
Prefer ingest-time window attribution. With one worker MostRecentFor is exact; with several it is a best guess, and it couples the database tap to an HTTP tap in the same process. kronikol ingest --attribute-by-window reconstructs the same answer deterministically from the tests NDJSON's timeline, works for any capturer in any process, and is documented in Ingesting External Captures. The registry is the live option for suites that genuinely run tests in parallel; nothing is published unless InFlightRegistry is set.
RequestResponseLogger is a process-wide static store, so a tap running in the same process as report generation (a test host, an orchestrator) can log directly — the default sink. A tap running in another process (a sidecar, a polyglot harness) cannot reach that store: give it an NdjsonInteractionWriter sink and replay the file with kronikol ingest (see Ingesting External Captures). Use both at once to get the live in-process report and a replayable artifact:
using var file = new NdjsonInteractionWriter(".logs/taps/web-graphql.ndjson");
o.Sink = new CompositeRequestResponseSink(RequestResponseLoggerSink.Instance, file);Polling and retry traffic produces long runs of identical arrows. Enable collapsing on the report: ReportConfigurationOptions.CollapseConsecutiveIdenticalCalls = true (+ CollapseThreshold, MaxArrowsPerDiagram) — see Report Configuration. A scenario whose id matched no tapped call shows an explicit No interactions captured marker rather than an empty section.
- HTTP/1.1 only (the
HttpListenerfront end); HTTP/2 or gRPC-over-h2c hops need a different tee. - The tap is a participant in the trace: latencies you see include the tap's own (small) overhead.
- The tap cannot repair a downstream service that resets trace context on its own (e.g. a Java agent starting a fresh root for an outbound call); such legs need the service to forward the id — see the generic alternative (header re-injection) before patching anything.
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
- 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