DSoftStudio.Mediator.OpenTelemetry 1.1.1

Prefix Reserved
dotnet add package DSoftStudio.Mediator.OpenTelemetry --version 1.1.1
                    
NuGet\Install-Package DSoftStudio.Mediator.OpenTelemetry -Version 1.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DSoftStudio.Mediator.OpenTelemetry" Version="1.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DSoftStudio.Mediator.OpenTelemetry" Version="1.1.1" />
                    
Directory.Packages.props
<PackageReference Include="DSoftStudio.Mediator.OpenTelemetry" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DSoftStudio.Mediator.OpenTelemetry --version 1.1.1
                    
#r "nuget: DSoftStudio.Mediator.OpenTelemetry, 1.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DSoftStudio.Mediator.OpenTelemetry@1.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DSoftStudio.Mediator.OpenTelemetry&version=1.1.1
                    
Install as a Cake Addin
#tool nuget:?package=DSoftStudio.Mediator.OpenTelemetry&version=1.1.1
                    
Install as a Cake Tool

DSoftStudio Mediator

DSoftStudio.Mediator.OpenTelemetry

NuGet License

OpenTelemetry instrumentation for DSoftStudio.Mediator. Distributed tracing and metrics for Send, Publish and CreateStream, attached through the core's observation ports rather than by replacing anything you registered.

Features

  • Distributed tracing — Activity spans for Send, Publish and CreateStream with semantic attributes
  • Per-handler notification spans — one span for the publish, one per handler it resolved, each tagged with mediator.handler.type
  • Metrics — mediator.request.duration histogram (seconds, with sub-second buckets) plus mediator.request.active / mediator.request.errors counters, created from the DI IMeterFactory
  • Database dependency enrichment — automatically tags database spans with a redaction-safe db.operation.name / db.sql.table / db.stored_procedure.name, so each query (SELECT, INSERT, CALL…) shows as its own dependency instead of one aggregated row — no configuration
  • Configurable filtering — Include or exclude specific request types via MediatorInstrumentationOptions
  • Zero configuration — Works out of the box with any OpenTelemetry exporter

How it attaches

Worth knowing, because it decides what the spans can see and what the package changes about your container.

Operation Mechanism
Send IMediatorDispatchObserver — the core's observation port
Publish IMediatorNotificationObserver — same idea, for notifications
CreateStream IStreamPipelineBehavior — streams have no dispatch port
Metrics IPipelineBehavior

The request span is opened at the dispatch boundary, not inside the behavior chain, so it wraps the whole pipeline — pre- and post-processors included. A behavior cannot do that: they run outside it.

Publishing is observed rather than intercepted. Earlier versions replaced INotificationPublisher, which was never neutral — it disarmed the generated dispatch, resolved handlers from the container instead of the compile-time table (so a handler the generator could not see went from skipped to invoked), and handed back a different instance for a stateful singleton. The observer substitutes nothing. The publisher is only decorated when you registered your own, because at that point the fast path is already disarmed and decorating changes nothing that was not already changed.

An application that registers no instrumentation pays nothing: the core takes the observers as an IEnumerable and the empty case is known before the container is built.

What tracing a stream costs

Requests and notifications are observed at the dispatch boundary, so instrumentation is a fixed cost per operation. Streams are not: the span has to be made ambient again on every MoveNextAsync, or spans opened inside the handler leave the stream subtree from the second item onward.

Activity.Current is an AsyncLocal, and writing one copies the ExecutionContext value map. Measured, the set-and-restore pair is 144 B and roughly 185 ns per item — a thousand-item stream pays about 144 KB and 185 µs it would not pay untraced.

That is the trade, and it is deliberate: the alternative is not a cheaper trace but a wrong one. None of it applies without a listener — with tracing disabled, or with nothing subscribed to the source, the behavior hands the stream straight to the next link and never enters the instrumented path. Requests, notifications and metrics are unaffected.

Installation

dotnet add package DSoftStudio.Mediator.OpenTelemetry

Quick Start

services
    .AddMediator()
    .RegisterMediatorHandlers()
    .AddMediatorInstrumentation()
    .PrecompilePipelines()
    .PrecompileNotifications()
    .PrecompileStreams();

services
    .AddOpenTelemetry()
    .WithTracing(b => b.AddMediatorInstrumentation())
    .WithMetrics(b => b.AddMediatorInstrumentation());

The three Precompile* calls arm three separate dispatch tables. PrecompilePipelines() is the one this package needs, but stopping there leaves the rest of your application without notification or stream dispatch — and the two fail differently: CreateStream throws and names the missing call, while Publish silently reaches no handlers. AddMediator(builder => { }) calls all three for you.

Registration order

AddMediatorInstrumentation() registers the stream tracing and metrics behaviors as open generics. PrecompilePipelines() is the step that closes every open-generic behavior over the discovered request/response pairs and builds the pipeline chains, so the call has to sit after RegisterMediatorHandlers() and before PrecompilePipelines() — as in the Quick Start above.

Get the order wrong and nothing on the request side is instrumented at all — not the metrics behavior, and not the request span either.

Being an observer rather than a behavior does not make the dispatch observer independent of this ordering. PipelineChainHandler is its only consumer, and Mediator.Send only reaches that handler when a chain exists:

if (RequestDispatch<TRequest, TResponse>.HasPipelineChain)
{
    var chain = PipelineChainCache<TRequest, TResponse>.Resolve(_serviceProvider);
    if (chain is not null)
        return chain.Handle(request, cancellationToken);   // the only path that has the observer
}

return HandlerCache<TRequest, TResponse>.Resolve(_serviceProvider)
    .Handle(request, cancellationToken);                   // no chain, no observer, no span

With no behavior closed into a chain, a request goes straight to its handler and the observer is never constructed. Request spans, stream spans and metrics all disappear together. Notification spans are the exception and survive, because publishing does not go through a pipeline chain.

It can hide in the IDE and surface in production. Anything else that registers a pipeline component — the Pipeline Explorer's profiling mode, for one — forces a chain to exist, and the request spans come back. A developer profiling locally sees a complete trace, exports a production trace built without it, and finds the requests missing.

The analyzer reports DSOFT010 when it can see the mistake, which means both calls in the same method on the same service collection: the Program.cs shape. A registration made from another method or another assembly is beyond what any analyzer can order, so keep the calls in one chain where the order is visible.

Calling AddMediatorInstrumentation() twice on the same collection is safe — the second call sees a marker the first left behind and returns. Without that, a second call would register a second set of observers and every span would be emitted twice.

Configuration

services.AddMediatorInstrumentation(options =>
{
    // Skip noisy request types (e.g. health checks).
    options.Filter = type => !type.Name.Contains("HealthCheck");

    // Tracing and metrics are both ON by default — turn one off if you only want the other.
    // options.EnableTracing = false;
    // options.EnableMetrics = false;

    // Keep error.type on the span but drop the (verbose) exception stack trace.
    options.RecordExceptionStackTraces = false;
});

Documentation

📖 Full documentation

License

MIT License

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1 421 10/13/2026
1.1.1-rc.2 81 9/17/2026
1.1.0 148 7/8/2026
Loading failed