Skip to content

Integration OpenTelemetry Extension

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

OpenTelemetry Extension

The Kronikol core library automatically captures System.Diagnostics.Activity spans during test execution via a non-invasive ActivityListener. These spans are used by Internal Flow Tracking to show what happened inside the SUT between HTTP boundaries.


Zero-Config (Default)

No setup required. The TestTrackingMessageHandler constructor auto-starts an InternalFlowActivityListener that subscribes to all well-known auto-instrumentation sources (ASP.NET Core, HttpClient, EF Core, Redis, Cosmos, etc.). InternalFlowTracking defaults to true on ReportConfigurationOptions.

To also capture custom ActivitySources, add their names to the handler options you're already configuring:

new XUnitTestTrackingMessageHandlerOptions
{
    CallerName = "Tests",
    PortsToServiceNames = { [5001] = "MyApi" },
    InternalFlowActivitySources = ["MyApi.Services", "MyApi.Workers"]  // optional
}

Explicit DI Registration (Optional)

If you need the listener started before any handler is constructed, or want to register via DI for container-managed disposal:

builder.ConfigureTestServices(services =>
{
    // ... existing tracking setup ...
    services.AddActivityListenerForInternalFlowTracking("MyApi.Services");
});

This is:

  • Non-invasive — uses a raw System.Diagnostics.ActivityListener (BCL), not the OTel SDK
  • Zero interference — does not touch the SUT's existing OpenTelemetry sampling, exporters, or telemetry assertions
  • Automatic — subscribes to all well-known auto-instrumentation sources
  • No extra packages — lives in the core Kronikol package

Namespace: Kronikol.InternalFlow — you may need to add a using directive.


How It Works

Option A: AddActivityListenerForInternalFlowTracking() (recommended)

Registers an InternalFlowActivityListener — a System.Diagnostics.ActivityListener that:

  1. Subscribes to all well-known auto-instrumentation sources plus any additional sources you specify
  2. Samples with AllData (not AllDataAndRecorded) — the Activity.Recorded flag stays false, so existing OTel exporters that check Recorded will skip these activities
  3. Captures completed spans via ActivityStopped into InternalFlowSpanStore (a ConcurrentQueue<Activity> in the core package)

Because it uses the BCL ActivityListener rather than the OTel SDK, it works regardless of whether the SUT configures OpenTelemetry, and never interferes with existing exporters or samplers.

Option B: AddTestTrackingExporter() (manual OTel SDK)

For users who want to add the exporter to their own TracerProviderBuilder:

services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource("MyApi.Services")
        .AddSource("Microsoft.AspNetCore")
        .AddSource("System.Net.Http")
        .AddTestTrackingExporter());

This requires the Kronikol.Extensions.OpenTelemetry package and gives you full control over the OTel pipeline. The exporter writes to the same InternalFlowSpanStore as the listener approach.

⚠️ Warning: Adding sources and the exporter to an existing TracerProviderBuilder causes all exporters on that builder to receive the additional activity data — which can break telemetry assertion tests. See Projects with Existing OpenTelemetry Configuration below.


When to Use Which

Scenario Recommendation
Standard case Do nothing — auto-start covers well-known sources
Custom ActivitySources Add InternalFlowActivitySources to handler options
Need DI-registered listener Use AddActivityListenerForInternalFlowTracking()
Full OTel SDK control Use AddTestTrackingExporter() on a separate AddOpenTelemetry() call

Install

No additional package is needed for the default auto-start behaviour — span capture is built into the core Kronikol package.

For the manual AddTestTrackingExporter() approach:

dotnet add package Kronikol.Extensions.OpenTelemetry

Dependencies:

  • OpenTelemetry (1.*)
  • OpenTelemetry.Exporter.InMemory (>= 1.15.1)
  • Kronikol (core library)

Version alignment: The OpenTelemetry extension package requires a matching (or newer) version of the core Kronikol package. If you're using other Kronikol packages (CosmosDB, Redis, EF Core, etc.) or a framework package (xUnit3, NUnit, etc.), ensure they are all at the same version. Mixing versions (e.g. OpenTelemetry extension at 2.0.2 with xUnit3 at 2.0.1) will cause NuGet restore failures referencing the core Kronikol package.

If you use central package management (Directory.Packages.props), you may also need to bump your OpenTelemetry.Exporter.InMemory version to >= 1.15.1 to satisfy the transitive dependency. With TreatWarningsAsErrors enabled, a version mismatch will produce a build-breaking NU1605 error.


Setup

Default: Zero-Config Auto-Start

No setup required. The TestTrackingMessageHandler constructor auto-starts an InternalFlowActivityListener for well-known sources. To capture custom ActivitySources, add them to your handler options:

new XUnitTestTrackingMessageHandlerOptions
{
    CallerName = "Tests",
    PortsToServiceNames = { [5001] = "MyApi" },
    InternalFlowActivitySources = ["MyApi.Services"]  // optional
}

Optional: Explicit DI Registration

If you prefer DI-based lifecycle management or need the listener before any handler is constructed:

using Kronikol.InternalFlow;

builder.ConfigureTestServices(services =>
{
    services.AddActivityListenerForInternalFlowTracking("MyApi.Services");
});

Manual OTel SDK Approach

For full pipeline control (requires Kronikol.Extensions.OpenTelemetry):

using Kronikol.Extensions.OpenTelemetry;

builder.ConfigureTestServices(services =>
{
    services.AddOpenTelemetry()
        .WithTracing(tracing => tracing
            .AddSource("MyApi.Services")
            .AddSource("Microsoft.AspNetCore")
            .AddSource("System.Net.Http")
            .AddSource("Microsoft.EntityFrameworkCore")
            .AddTestTrackingExporter());                // ← Captures spans for diagrams
});

The auto-started listener and the OTel exporter can be active simultaneously — InternalFlowSpanStore deduplicates by reference identity.

Projects with Existing OpenTelemetry Configuration

⚠️ This section only applies to the manual AddTestTrackingExporter() approach. The auto-started listener and AddActivityListenerForInternalFlowTracking() are always non-invasive and never interfere with existing OTel configuration.

If your project already configures OpenTelemetry (e.g. via a shared telemetry library or an existing TracerProviderBuilder), do not add .AddTestTrackingExporter() and the additional activity sources to the existing builder. Adding sources like Microsoft.AspNetCore, System.Net.Http, etc. to an existing builder causes all exporters on that builder to receive the additional activity data — which can break telemetry assertion tests that rely on a specific set of captured activities.

Recommended approach: Register a separate AddOpenTelemetry().WithTracing(...) exclusively for the Kronikol exporter, keeping the existing builder untouched:

builder.ConfigureTestServices(services =>
{
    // ✅ Existing OTel setup — unchanged, feeds your ExportedActivities for telemetry assertions
    services.AddMyTelemetry(options => { ... }, builder => builder
        .AddInMemoryExporter(exportedActivities));

    // ✅ Separate registration for Kronikol Internal Flow Tracking
    services.AddOpenTelemetry()
        .WithTracing(tracing => tracing
            .AddSource("MyApi.Services")
            .AddSource("Microsoft.AspNetCore")
            .AddSource("System.Net.Http")
            .AddSource("Microsoft.Azure.Cosmos")
            .AddTestTrackingExporter());
});

Or even simpler — just use the recommended approach instead:

builder.ConfigureTestServices(services =>
{
    // ✅ Existing OTel setup — completely untouched
    services.AddMyTelemetry(options => { ... }, builder => builder
        .AddInMemoryExporter(exportedActivities));

    // ✅ Non-invasive — doesn't touch the TracerProvider at all
    services.AddActivityListenerForInternalFlowTracking("MyApi.Services");
});

About InternalFlowTracking in Report Options

InternalFlowTracking defaults to true on ReportConfigurationOptions. No explicit setting is needed. To opt out:

new ReportConfigurationOptions
{
    InternalFlowTracking = false, // disable internal flow popups
}

The captured spans automatically appear in sequence diagram popups at report viewing time.


What Activity Sources to Add

You must explicitly register each ActivitySource you want to capture. Common sources:

Important: Your SUT's own ActivitySource is the most valuable one to add. Without it, the internal flow diagrams will only show framework-level spans (HTTP pipeline, ASP.NET Core middleware, database calls) and not your actual application logic. The auto-instrumentation sources below provide useful context, but it's the custom sources that show your business logic flow in the internal flow popups.

Source Package What it captures
Microsoft.AspNetCore Built-in (ASP.NET Core) Incoming HTTP request processing
System.Net.Http Built-in (.NET) Outgoing HTTP client calls
Microsoft.EntityFrameworkCore Built-in (EF Core) Database queries and commands
Npgsql Npgsql PostgreSQL-specific operations
StackExchange.Redis StackExchange.Redis Redis operations
Azure.Cosmos / Microsoft.Azure.Cosmos Azure Cosmos SDK Cosmos DB operations
Your custom sources Your code Any custom ActivitySource in your SUT

Creating Custom Activity Sources

If your SUT uses custom ActivitySource instances, add those source names too:

// In your SUT code
public static class Telemetry
{
    public static readonly ActivitySource Source = new("MyApi.Services");
}

// In a service method
using var activity = Telemetry.Source.StartActivity("ProcessOrder");
// ... processing logic
// In your test setup
tracing.AddSource("MyApi.Services")
       .AddTestTrackingExporter();

Clearing Spans Between Tests

The span store is static and accumulates spans across all tests in a run. In most cases this is fine — InternalFlowSegmentBuilder correlates spans with specific tests using TraceId.

If you need to clear spans between tests (e.g. for isolation), call:

InternalFlowSpanStore.Clear();               // Core package
// or
TestTrackingSpanStore.Clear();               // Backward-compatible facade (Extensions.OpenTelemetry)

API Reference

Core Package (Kronikol)

InternalFlowServiceCollectionExtensions

Method Description
AddActivityListenerForInternalFlowTracking(this IServiceCollection, params string[]) Registers a non-invasive InternalFlowActivityListener via DI for container-managed disposal. Optional — the TestTrackingMessageHandler auto-starts a listener automatically. Use this when you need custom sources via DI or want the listener started before any handler is constructed.

InternalFlowActivityListener

IDisposable class wrapping System.Diagnostics.ActivityListener. Auto-started by TestTrackingMessageHandler constructor (process-wide singleton). Can also be created manually or via AddActivityListenerForInternalFlowTracking().

Member Description
Constructor (params string[] additionalActivitySources) Creates and registers an ActivityListener for well-known + custom sources.
EnsureStarted(string[]?) Starts the process-wide singleton (first caller wins). Called automatically by TestTrackingMessageHandler.
Dispose() Unregisters the listener. Stops capturing new spans.

InternalFlowSpanStore

Static thread-safe store (ConcurrentQueue<Activity>) in the core package. Deduplicates by reference identity.

Member Description
Add(Activity) Enqueues a span if not already present (reference-equality dedup). Called by InternalFlowActivityListener and TestTrackingSpanExporter.
GetSpans() Returns a snapshot of all captured Activity[] spans.
Clear() Clears all captured spans.

Extensions Package (Kronikol.Extensions.OpenTelemetry)

OpenTelemetryTrackingExtensions

Method Description
AddTestTrackingExporter(this TracerProviderBuilder) Registers a SimpleActivityExportProcessor with TestTrackingSpanExporter to capture all completed spans into InternalFlowSpanStore.

TestTrackingSpanStore (facade)

Backward-compatible static class that delegates to InternalFlowSpanStore in the core package.

Member Description
Add(Activity) Delegates to InternalFlowSpanStore.Add().
GetSpans() Delegates to InternalFlowSpanStore.GetSpans().
Clear() Delegates to InternalFlowSpanStore.Clear().

TestTrackingSpanExporter

Internal class. Implements BaseExporter<Activity> and forwards each completed span to InternalFlowSpanStore.Add().


Architecture

Option A: ActivityListener (recommended)      Option B: OTel SDK (manual)

┌────────────────────────────────┐          ┌──────────────────────────────┐
│ InternalFlowActivityListener   │          │ OpenTelemetry TracerProvider  │
│  └─ ShouldListenTo(wellKnown+) │          │  └─ AddSource("...")         │
│  └─ Sample = AllData           │          │  └─ AddTestTrackingExporter  │
│  └─ ActivityStopped callback   │          └──────────┬───────────────────┘
└──────────┬─────────────────────┘                     │
           │                                           ▼
           │                          ┌──────────────────────────────────┐
           │                          │ SimpleActivityExportProcessor     │
           │                          │  └─ TestTrackingSpanExporter     │
           │                          └──────────┬───────────────────────┘
           │                                     │
           ▼                                     ▼
      ┌──────────────────────────────────────────────┐
      │ InternalFlowSpanStore (core package)          │
      │  └─ ConcurrentQueue<Activity>                 │
      │  └─ GetSpans() → Activity[]                   │
      └──────────┬───────────────────────────────────┘
                 │
                 ▼
      ┌──────────────────────────────┐
      │ InternalFlowSpanCollector    │
      │  └─ CollectSpans()           │
      │  └─ FilterSpans(granularity) │
      └──────────────────────────────┘

Both paths feed the same InternalFlowSpanStore in the core package. The store deduplicates by reference identity, so both paths can be active simultaneously without duplicates. InternalFlowSpanCollector reads directly from the store (no reflection).

No hard dependency: The auto-started listener and AddActivityListenerForInternalFlowTracking() live in the core package and require zero additional NuGet packages. The AddTestTrackingExporter() method requires the Kronikol.Extensions.OpenTelemetry package.


Differences from Standard HTTP Tracking

TestTrackingMessageHandler InternalFlowActivityListener / TestTrackingSpanExporter
What it captures HTTP request/response pairs between services All Activity spans from registered sources
Where it appears Sequence diagram arrows Internal flow popups (when clicking an arrow)
Granularity One entry per HTTP call One entry per activity span (can be many per HTTP call)
Configuration TestTrackingMessageHandlerOptions Auto-started (default) or InternalFlowActivitySources option
Required for diagrams Yes — core functionality No — optional enhancement for internal flow

Home


Demo


Getting Started

Common Tasks

Integration Guides

Extensions

Configuration

Features

Reference

Clone this wiki locally