Skip to content

Diagnostics and Debugging

aryehcitron@gmail.com edited this page May 17, 2026 · 13 revisions

Diagnostics and Debugging

Kronikol includes several diagnostic tools to help you understand what's being tracked, identify configuration issues, and debug missing or unexpected diagram content.


Diagnostic Mode

Enable DiagnosticMode on ReportConfigurationOptions to generate a comprehensive DiagnosticReport.html alongside your normal reports:

var options = new ReportConfigurationOptions
{
    DiagnosticMode = true // generates DiagnosticReport.html
};

The diagnostic report includes:

Section Contents
Configuration Dump All ReportConfigurationOptions values, including InternalFlow settings
Log Counts by Service Number of request/response logs per ServiceName
Log Counts by Test Number of logs per test, sorted descending — highlights tests with zero logs
Unpaired Requests Request logs without matching responses (same TraceId + RequestResponseId)
Orphaned Test IDs Test IDs in logs that don't match any Feature/Scenario
Scenarios with No Logs Scenarios that ran but produced zero logged interactions
Activity Source Discovery All detected ActivitySource names from captured OTel spans, with span counts and tracked/well-known status

Tip: Enable DiagnosticMode temporarily during development to identify why diagrams are missing content — unpaired logs, orphaned IDs, or missing activity sources are the most common root causes.


Report Diagnostics

ReportDiagnostics.Analyse() runs automatically at report generation time and produces warning strings that appear in the report footer. Common warnings include:

  • "No request/response logs recorded" — No interactions were tracked. Check that TestTrackingMessageHandler is wired up.
  • "N test(s) have no request/response logs" — Some scenarios didn't produce any HTTP interactions.
  • "InternalFlowSpanStore contains 0 spans" — No OTel spans were captured. Check that the activity listener is running.

When ActivitySourceDiscovery is enabled on ReportConfigurationOptions, the diagnostics additionally list all discovered activity sources with their span counts:

var options = new ReportConfigurationOptions
{
    ActivitySourceDiscovery = true // includes source discovery in diagnostics
};

Activity Source Discovery

ActivitySourceDiscovery.GetDiscoveredSources() returns a dictionary of all ActivitySource names that produced spans during the test run, with their span counts:

var sources = ActivitySourceDiscovery.GetDiscoveredSources();
// e.g. { "Microsoft.AspNetCore": 42, "System.Net.Http": 38, "MyApi.Services": 15 }

This is useful for:

  • Identifying which sources are producing spans — Helps you decide which to include/exclude via InternalFlowSpanGranularity and InternalFlowActivitySources.
  • Verifying custom sources are being captured — If your custom ActivitySource doesn't appear, check that InternalFlowActivitySources includes its name.
  • Understanding span volume — High span counts from a single source may indicate noisy instrumentation that could be filtered.

InternalFlowSpanStore.Complete()

When creating manual spans (e.g. inside a TrackingProxy or custom tracking code), use InternalFlowSpanStore.Complete(activity) as a one-liner to stop and store the span:

var activity = new ActivitySource("MySource").StartActivity("MyOperation");
try
{
    // ... do work ...
}
finally
{
    InternalFlowSpanStore.Complete(activity);
}

Complete() is null-safe — it does nothing if the activity is null. If the activity hasn't been stopped, it calls Stop() before adding it to the store.


Empty Diagram Diagnostics

When InternalFlowNoDataBehavior is set to ShowMessage (the default), segments with no captured spans show an enriched diagnostic message instead of a generic "No data" notice. The message includes:

  • The current InternalFlowSpanGranularity setting
  • The configured InternalFlowActivitySources (if Manual granularity)
  • A count of total spans in the InternalFlowSpanStore
  • Actionable suggestions (e.g. "Try switching to Full granularity", "Check that your activity sources are listed")

This makes it much easier to diagnose why a particular popup has no internal flow content.


RequestResponseLogger.LogPair()

LogPair() is a convenience method that logs a matched request/response pair in a single call, automatically generating TraceId and RequestResponseId values:

RequestResponseLogger.LogPair(
    testName: "My test",
    testId: "test-123",
    method: "Cache Get",                    // OneOf<HttpMethod, string>
    uri: new Uri("redis://cache/my-key"),
    serviceName: "Redis Cache",
    callerName: "My Service",
    requestContent: "GET my-key",
    responseContent: "{\"value\": 42}",
    statusCode: HttpStatusCode.OK,
    dependencyCategory: DependencyCategories.Redis  // optional — controls participant shape/colour
);

The optional dependencyCategory parameter (v2.28.21+) accepts a value from DependencyCategories and controls how the target participant renders in sequence diagrams. For example, passing DependencyCategories.Redis renders the participant with the cache shape and colour, while omitting it (or passing null) renders a generic entity.

This is equivalent to calling RequestResponseLogger.Log() twice (once for Request, once for Response) with matching identifiers and timestamps. It's the recommended approach for custom dependency tracking when you already have both the request and response data available.

See Tracking Custom Dependencies for the full manual approach when you need more control (e.g. different timestamps for request vs response, custom headers, or TrackingIgnore support).


Flame Chart Zoom

Flame charts in internal flow popups and whole-test flow sections now support interactive zoom:

Action Effect
Click a bar Zooms into that span's time range (with 5% padding)
Double-click anywhere Resets to the full view

When zoomed, a hint banner appears: "🔍 Zoomed — double-click to reset"

Tooltips on each bar now include:

  • Activity source name (e.g. [Microsoft.AspNetCore])
  • Span display name
  • Duration in milliseconds
  • Percentage of total duration

TrackingComponentRegistry

All tracking components (TestTrackingMessageHandler, SqlTrackingInterceptor, CosmosTrackingMessageHandler, BlobTrackingMessageHandler, BigQueryTrackingMessageHandler, RedisTracker) auto-register with TrackingComponentRegistry when constructed. This enables automated detection of misconfigured components that were wired up but never processed any traffic.

Automatic Warnings

When ReportDiagnostics.Analyse() runs at report generation time, it automatically checks for unused tracking components and produces console warnings:

Warning: 1 tracking component(s) were registered but never invoked: SqlTrackingInterceptor (Identity Database).
This usually means the component was added to the wrong pipeline or options. Enable DiagnosticMode for details.

This happens automatically — no extra code needed. The warning is informational only and never throws or fails tests.

Diagnostic Report Details

When DiagnosticMode=true, the HTML diagnostic report includes a dedicated Tracking Components section with:

  • A grouped table of all registered components — instances with the same ComponentName are aggregated into a single row showing total invocations, instance count, and active count. Multiple instances (common with ICollectionFixture patterns) are shown with an expandable <details> element for per-instance breakdown.
  • An HttpContextAccessor column showing whether each component has an accessor configured (✓ configured), is missing one (⚠ null), or is inactive ().
  • Smart "never invoked" warnings that distinguish between:
    • Fully inactive types (0 invocations across ALL instances) — likely a real misconfiguration
    • Partially inactive types (some instances active, others not) — typically expected when using collection fixtures with uneven test distribution
  • Unmatched HTTP Client Names section — shows clientName values passed to TestTrackingMessageHandler that didn't match any key in ClientNamesToServiceNames, along with request counts and a fix suggestion.
  • Troubleshooting hints for common causes (EF Core DbContextOptions<T> mismatch, incorrect HttpClient registration, untracked Redis IDatabase)

Inspect Programmatically

// All registered components
var all = TrackingComponentRegistry.GetRegisteredComponents();

// Only unused components
var unused = TrackingComponentRegistry.GetUnusedComponents();

// Individual component properties (via ITrackingComponent interface)
foreach (var c in all)
{
    Console.WriteLine($"{c.ComponentName}: invoked={c.WasInvoked}, count={c.InvocationCount}");
}

Reset Between Runs

Call Clear() alongside RequestResponseLogger.Clear():

RequestResponseLogger.Clear();
TrackingComponentRegistry.Clear();

ITrackingComponent Interface

All tracking components implement this interface:

public interface ITrackingComponent
{
    string ComponentName { get; }     // e.g. "SqlTrackingInterceptor (Identity Database)"
    bool WasInvoked { get; }          // true after first request/command
    int InvocationCount { get; }      // total requests/commands processed
    bool HasHttpContextAccessor => false; // true when IHttpContextAccessor is configured
}

Common Issues: Missing Dependencies in Diagrams

gRPC dependency not appearing in per-test reports

Symptom: GrpcTrackingInterceptor is registered and WasInvoked is true, but gRPC calls don't appear in per-test diagrams. The diagnostic report may show logs with "Unknown" test identity.

Cause: The gRPC client runs inside the SUT's request pipeline (on a worker thread), and CurrentTestInfoFetcher cannot resolve the test framework's execution context from that thread. Without HttpContextAccessor, the interceptor falls back to the delegate, which throws → caught internally → logged as "Unknown" → filtered out of per-test reports.

Fix (v2.26.1+): Use AddTrackedGrpcClient<TClient>() which auto-resolves IHttpContextAccessor from DI:

services.RemoveAll<MyGrpcClient>();
services.AddTrackedGrpcClient<MyGrpcClient>(
    handler,
    new Uri("http://localhost/"),
    opts =>
    {
        opts.ServiceName = "My gRPC Service";
        opts.CallerName = "My API";
        opts.CurrentTestInfoFetcher = () => GetCurrentTestInfo();
        // HttpContextAccessor auto-resolved from DI — no manual wiring needed
    });

Fix (v2.25.2): Set HttpContextAccessor manually on GrpcTrackingOptions:

services.AddSingleton(sp =>
{
    var options = new GrpcTrackingOptions
    {
        ServiceName = "My gRPC Service",
        CallerName = "My API",
        CurrentTestInfoFetcher = () => GetCurrentTestInfo(),
        HttpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>()
    };
    // ...
});

See Integration Grpc Extension#Dual-Resolution Test Identity (HttpContextAccessor) for the full explanation.

Spanner gRPC interceptor not appearing in per-test reports

Symptom: WithTestTracking() is called on SpannerConnectionStringBuilder and the interceptor fires (Spanner operations execute successfully), but they don't appear in per-test diagrams.

Cause: The Spanner gRPC interceptor runs inside the SUT's request pipeline (on a thread spawned by the TestServer), and AsyncLocal-based CurrentTestInfoFetcher cannot resolve test identity from that thread. This is particularly common with WebApplicationFactory scenarios using Spanner.InMemoryEmulator.

Fix (v2.27.3+): Pass IHttpContextAccessor to the WithTestTracking() overload. Create the builder inside a DI factory lambda so the accessor is available:

services.AddSingleton<ISpannerConnectionFactory>(sp =>
{
    var httpContextAccessor = sp.GetService<IHttpContextAccessor>();
    var builder = new SpannerConnectionStringBuilder { DataSource = "..." }
        .WithTestTracking(trackingOptions, httpContextAccessor);
    return new MyConnectionFactory(builder);
});

See Integration Spanner Extension#DI Setup (WebApplicationFactory) for the full example.

Other dependencies not appearing in per-test reports

Symptom: An extension tracker is registered and WasInvoked is true, but calls don't appear in per-test diagrams. The diagnostic report may show logs with "Unknown" test identity.

Cause: Same as gRPC above — the tracker runs inside the SUT's request pipeline (on a worker thread), and CurrentTestInfoFetcher cannot resolve the test framework's execution context from that thread.

Fix (v2.26.3+): All extension options classes now have an HttpContextAccessor property, and DI extensions / convenience methods auto-resolve it from DI. If you use AddServiceBusTestTracking(), AddTrackedGrpcClient<T>(), CreateTestTrackingClient(), or similar DI conveniences, this is handled automatically.

If you construct trackers manually, set HttpContextAccessor on the options object:

services.AddSingleton(sp =>
{
    var options = new KafkaTrackingOptions
    {
        ServiceName = "Kafka",
        CallerName = "My API",
        CurrentTestInfoFetcher = CurrentTestInfo.Fetcher,
        HttpContextAccessor = sp.GetRequiredService<IHttpContextAccessor>()
    };
    return new KafkaTracker(options);
});

See HTTP Tracking Setup#Dual-Resolution Test Identity (v2.23.0+) for details.

CurrentTestInfo.Fetcher throws NullReferenceException on background threads (fixed in v2.27.10)

Symptom: Tests fail with NullReferenceException when CurrentTestInfo.Fetcher is invoked outside of a test execution context — e.g. during hosted service processing, background threads, or Service Bus message handlers. This could crash event handlers (like BeforePublish/AfterPublish on MessageTracker), causing downstream assertion failures.

Cause (v2.27.9 and earlier): CurrentTestInfo.Fetcher directly accessed TestContext.Current.Test without a null check. Additionally, MessageTracker.GetTestInfo() invoked the delegate without exception handling (unlike all other extensions, which route through TestInfoResolver.Resolve()).

Fix: Upgrade to v2.27.10+. Both issues are resolved:

  1. All 8 framework CurrentTestInfo.Fetcher implementations now return ("Unknown", "unknown") when the test context is unavailable
  2. MessageTracker.GetTestInfo() wraps the delegate call in a try-catch, matching all other extensions

Troubleshooting

Empty report — tests pass but no diagrams generated

Symptom: Report HTML is generated but shows 0 scenarios, or "No test results found".

Cause: DiagrammedTestRun.TestContexts.Enqueue(TestContext.Current) is not being called in the test's DisposeAsync().

Fix: Ensure every test class (or its base class) calls this in DisposeAsync():

public ValueTask DisposeAsync()
{
    DiagrammedTestRun.TestContexts.Enqueue(TestContext.Current);
    return ValueTask.CompletedTask;
}

Verify (v2.28.13+): When logs exist but no test contexts are enqueued, a prominent console warning is now emitted automatically:

⚠ WARNING: No test contexts were enqueued, but tracking logs exist.

If using DiagnosticMode = true, the diagnostic report is still generated even when features are empty, so you can inspect what was tracked.

Large number of "Unknown" tracking entries

Symptom: The Diagnostic Report shows thousands of entries attributed to Unknown / unknown.

Root cause: Background threads where TestContext.Current.Test is null.

Common sources:

  • Cosmos DB Change Feed Processor (polls on a background thread)
  • Azure Functions timer triggers invoked via HttpClient.PostAsync() without tracking headers
  • Hosted services (IHostedService) running background work
  • Event handlers (e.g. AfterPublish) firing on thread pool threads
  • Read-through cache refresh operations

Fix: Use TestIdentityScope.Begin() or an instance-scoped ActiveTestTracker. See Background Thread Correlation.

HTTP client calls tracked as "Unknown" despite API tracking working

Symptom: Inbound API calls show correctly in diagrams, but outgoing HTTP calls (via typed clients) show as "Unknown" or don't appear.

Root cause: IHttpContextAccessor not passed to TestTrackingMessageHandler when constructing it inside an IHttpMessageHandlerBuilderFilter.

Common mistake:

// ❌ Discards service provider — IHttpContextAccessor is null
services.AddSingleton<IHttpMessageHandlerBuilderFilter>(_ =>
    new MyFilter(options));

// ✅ Resolves IHttpContextAccessor from DI
services.AddSingleton<IHttpMessageHandlerBuilderFilter>(sp =>
    new MyFilter(options, sp.GetService<IHttpContextAccessor>()));

Fix: Use sp => instead of _ => in the factory lambda. See HTTP Tracking Setup#Dual-Resolution Test Identity (v2.23.0+).

Service Bus publishes not appearing in diagrams

Symptom: Tests that publish Service Bus messages don't show Service Bus in their sequence diagrams.

Root cause: Service Bus tracking requires explicit wiring — it's not automatic like HTTP tracking.

Fix:

  1. Register TrackMessagesForDiagrams() in the test host's DI
  2. Create a handler that bridges publish events to MessageTracker
  3. Wire the handler after both hosts are initialised

See Service Bus Tracking Patterns for the full setup.

Function trigger operations not attributed to the originating test

Symptom: A test triggers an Azure Function, but the Function's Cosmos/HTTP operations show as "Unknown".

Root cause: The function trigger call doesn't include test tracking headers, so the Function host's IHttpContextAccessor has no tracking context.

Fix: Use HttpRequestMessage with explicit tracking headers:

var request = new HttpRequestMessage(HttpMethod.Post, "/api/functions/MyTrigger")
{
    Content = new StringContent(string.Empty)
};

var (testName, testId) = CurrentTestInfo.Fetcher();
request.Headers.Add("test-tracking-current-test-name", testName);
request.Headers.Add("test-tracking-current-test-id", testId);
request.Headers.Add("test-tracking-trace-id", Guid.NewGuid().ToString());

await functionClient.SendAsync(request);

Or use TestIdentityScope.Begin() — see Background Thread Correlation.

All CosmosDB operations show as "Unknown"

Symptom: The diagnostic report shows CosmosDB entries but they're all attributed to "Unknown" rather than individual tests.

Cause: The handler captured a null HttpContextAccessor at construction time because DI wasn't yet available.

Fix: Use the LazyHttpContextAccessor pattern — assign a wrapper to options.HttpContextAccessor before building the Cosmos client, then wire the real IHttpContextAccessor after WebApplicationFactory initialisation. See Background Thread Correlation#LazyHttpContextAccessor Pattern.

HTTP dependency tracked but with wrong service name

Symptom: An outgoing HTTP call appears in the diagram but labelled as localhost:5001 instead of a human-readable service name.

Cause: ClientNamesToServiceNames key doesn't exactly match the HttpClient name. For typed HttpClients (services.AddHttpClient<PaymentGatewayHttpClient>(...)), the name is the full type name ("PaymentGatewayHttpClient").

Fix: Use the exact type name as the dictionary key. See HTTP Tracking Setup#ClientNamesToServiceNames — Matching Semantics.

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally