APM Tips

Application Insights

DiagnosticSource and Activity: how .NET libraries emit telemetry

Updated · 6 min read · By APM Tips editorial team

.NET has two built-in ways for a library to announce what it's doing. DiagnosticSource (with DiagnosticListener) publishes named events with rich in-process payloads that a subscriber has to understand; ActivitySource and Activity create spans, and are .NET's implementation of OpenTelemetry's Tracer and Span. Libraries should depend only on these APIs; the app decides who listens. The classic Application Insights 2.x SDK subscribed to well-known DiagnosticSource events and ignored other activities, while SDK 3.x and the Microsoft OpenTelemetry Distro record any ActivitySource you register with AddSource.

Every request, SQL call and HTTP dependency that Application Insights records in a .NET app starts as a callback from a library that knows nothing about Application Insights. Understanding those callbacks explains why some operations appear automatically, why your own don’t, and what changed between the classic SDK and OpenTelemetry.

Two APIs in one package

Both live in System.Diagnostics.DiagnosticSource, which is part of the .NET runtime; apps on runtimes older than .NET 5 need version 5 or later of the package for the activity APIs.

DiagnosticSource / DiagnosticListenerActivitySource / Activity
PurposeNamed events with arbitrary payloads, for in-process toolsSpans of work with IDs, timing, tags and status
PayloadAny object, including HttpContext or HttpResponseMessageStandard fields plus tags and events
DiscoverySubscribe to DiagnosticListener.AllListeners by nameActivityListener, or OpenTelemetry’s AddSource
Consumer must knowEach event name and payload shapeOnly the source name
OpenTelemetry termNoneTracer (source) and Span (activity)

.NET kept its own names because Activity predates the OpenTelemetry specification. For the other two pillars, .NET’s OpenTelemetry implementation uses System.Diagnostics.Metrics.Meter for metrics and ILogger for logs, so OpenTelemetry doesn’t have to ship APIs for library authors at all.

DiagnosticSource: named events

A DiagnosticListener is created with a component name and writes events by name, each with one payload object. The pattern Microsoft documents always guards the write, so that nothing is allocated when nobody listens.

Publishing a DiagnosticSource event from a library:

private static readonly DiagnosticSource Diagnostics = new DiagnosticListener("Contoso.Payments");

if (Diagnostics.IsEnabled("ChargeStart"))
{
    Diagnostics.Write("ChargeStart", new { Request = request, Amount = amount });
}

A consumer finds listeners by name through DiagnosticListener.AllListeners, subscribes, and receives KeyValuePair<string, object> events whose payloads it must know how to read. That’s the strength (anything in the process can be passed, unserialised) and the weakness: every consumer is coupled to every producer’s event names and payload types.

ActivitySource: spans

An ActivitySource creates Activity objects around units of work. Microsoft’s best practices for library authors: create the source once and keep it in a static field; give it a unique name, hierarchical if there are several in an assembly (Microsoft.AspNetCore.Hosting is the model); pass a version if the telemetry may change between releases.

Instrumenting a library operation as an activity:

public sealed class PaymentClient
{
    private static readonly ActivitySource Source = new("Contoso.Payments", "2.1.0");

    public async Task<Receipt> ChargeAsync(Charge charge)
    {
        using var activity = Source.StartActivity("Charge", ActivityKind.Client);
        activity?.SetTag("payment.provider", charge.Provider);
        try
        {
            var receipt = await SendAsync(charge);
            activity?.SetStatus(ActivityStatusCode.Ok);
            return receipt;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
            throw;
        }
    }
}

StartActivity returns null when no listener wants the activity, which is why every call after it uses ?.. Activities flow through async code automatically, nest into parent and child, and carry W3C trace context across HTTP, as the correlation guide explains.

Design rules for library authors

Most of what makes instrumentation good is restraint. Microsoft’s .NET guidance adds a few rules on top of the source naming above:

  • Follow the semantic conventions. OpenTelemetry publishes recommended tag names for common work (HTTP, databases, messaging). Using them is what lets a backend recognise a span as a SQL call instead of an anonymous operation.
  • Don’t pay for detail nobody reads. Activity.IsAllDataRequested tells hot-path code whether any listener wants tags at all; skip expensive tag values when it’s false.
  • Use events sparingly. Activity events are held in memory until the span is exported, so they suit a handful of milestones, not a stream. For high or unbounded volume, log through ILogger, which picks up the current activity’s IDs and stays correlated with the trace.
  • Report outcome with status. ActivityStatusCode is Unset, Ok or Error; set Error with a short description when the operation fails, and let the app decide what counts as a failure for requests.
  • Pick the right kind. The default Internal fits work with no remote side; use Client or Producer for outgoing calls and Server or Consumer for incoming work, because backends, Application Insights included, file spans by kind.

Keep tag values low in cardinality where they’ll be grouped (names, types, targets) and leave IDs to tags that are only filtered on. Personal data doesn’t belong in tags at all: they’re exported to whatever backend the app chose, not to one you control.

Listening without a vendor SDK

Nothing about this requires Application Insights or even OpenTelemetry. .NET’s ActivityListener lets any code in the process subscribe, which is handy for tests and for checking what a library emits.

A bare ActivityListener that prints every activity in the process:

ActivitySource.AddActivityListener(new ActivityListener
{
    ShouldListenTo = source => true,
    Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllDataAndRecorded,
    ActivityStopped = activity => Console.WriteLine($"{activity.Source.Name} {activity.OperationName} {activity.Duration}")
});

ShouldListenTo is called once per source; Sample decides whether an activity is created and recorded. Without a listener whose Sample says yes, StartActivity doesn’t create anything, which is the mechanism behind “unobserved instrumentation is nearly free”.

Classic SDK 2.x

Microsoft lists three techniques the classic SDK used to collect dependencies: bytecode instrumentation through the instrumentation engine, EventSource callbacks for libraries that emit structured events, and DiagnosticSource callbacks, used by the newer .NET SDKs for libraries that support distributed tracing. That’s why sdkVersion on a dependency row starts with rdddsd when it came through DiagnosticSource. The SDK used Activity and DiagnosticSource for correlation too.

What it didn’t do was pick up arbitrary activities. .NET’s own documentation notes that Application Insights (the classic SDK) only collects specific, well-known Activity instrumentation, ignores activities you add, and offers TrackDependency as its vendor-specific API instead. A separate Microsoft.ApplicationInsights.DiagnosticSourceListener package could forward DiagnosticSource events as traces. For custom spans in 2.x, use StartOperation<DependencyTelemetry> or TrackDependency, as in agent-based dependency tracking.

Today: SDK 3.x and the Microsoft OpenTelemetry Distro

Both current .NET paths are built on OpenTelemetry, and OpenTelemetry collects activities only from sources it’s told about. Register yours once at startup; the Distro and SDK 3.x then record every activity they produce, and the instrumentation libraries they bundle (ASP.NET Core, HttpClient, SqlClient, Azure SDK) do the same for those components.

Registering a library’s ActivitySource with the Microsoft OpenTelemetry Distro:

builder.Services.ConfigureOpenTelemetryTracerProvider((sp, tracing) => tracing.AddSource("Contoso.Payments"));
builder.Services.AddOpenTelemetry().UseMicrosoftOpenTelemetry(o => o.Exporters = ExportTarget.AzureMonitor);

The kind decides where an activity lands: Server and Consumer go to requests; Client, Producer and the default Internal go to dependencies. Tags become custom dimensions, and a BaseProcessor<Activity> can enrich or rename activities before export, which replaces the classic initializer (initializers and processors). SDK 3.x has no DiagnosticSourceListener package; Microsoft says its 3.x packages collect HTTP and SQL telemetry automatically, and anything else you want as telemetry should come through an activity, a metric or ILogger.

A library that only publishes DiagnosticSource events isn’t visible to OpenTelemetry on its own; it needs an instrumentation library that translates those events into activities. When you write a library today, emit activities directly and you skip that step for every consumer.

Troubleshooting

SymptomLikely causeFix
Custom activities never appearSource not registeredAddSource with the exact name, or a wildcard pattern
NullReferenceException in instrumentationStartActivity returned nullUse activity?. everywhere
Custom activities missing with the 2.x SDKClassic SDK ignores user-added activitiesUse TrackDependency, or move to SDK 3.x or the Distro
Spans appear as dependencies, expected requestsDefault ActivityKind.InternalStart with Server or Consumer
Library events visible to DiagnosticListener onlyLibrary uses DiagnosticSource, not ActivitySourceAdd an instrumentation library or emit activities
Old app shows hierarchical IDsPre-.NET 5 default ID formatSet Activity.DefaultIdFormat = ActivityIdFormat.W3C

Queries for this topic

From the KQL library: explained queries for the Application Insights tables, ready to copy.

  • End-to-end trace for one operation_Id

    requestsdependenciestracesexceptionspageViewscustomEvents operations

    Returns: Every telemetry item recorded with the given operation_Id in the last 7 days, oldest first, with its type, service, name or message, duration, outcome, severity, and the IDs that link parent and child.

Frequently asked questions

Should my library reference Application Insights or OpenTelemetry?

Neither. Microsoft's guidance is that library authors can rely only on the System.Diagnostics APIs (ActivitySource, Meter) and ILogger, so the library works whatever the app uses to collect telemetry. Only the application references OpenTelemetry, the Distro or the Application Insights SDK.

Why does StartActivity return null?

Because nothing is listening to that source, or the sampler decided not to record it. That's by design: an unobserved activity costs almost nothing. Register the source name with AddSource in the app, and always use null-conditional calls such as activity?.SetTag in the library.

Is DiagnosticSource obsolete?

No. It's still part of .NET and documented for current releases, and it remains the tool for in-process consumers that need rich, non-serializable payloads. For distributed tracing, .NET's instrumentation guidance uses ActivitySource and Activity, which is what OpenTelemetry collects.

Sources, checked

  1. DiagnosticSource and DiagnosticListener (.NET)learn.microsoft.com · checked
  2. Add distributed tracing instrumentation (.NET)learn.microsoft.com · checked
  3. Collect a distributed trace (.NET)learn.microsoft.com · checked
  4. Distributed tracing concepts (.NET)learn.microsoft.com · checked
  5. .NET observability with OpenTelemetrylearn.microsoft.com · checked
  6. Monitor .NET and Node.js applications with Application Insights (Classic API 2.x, dependency collection)learn.microsoft.com · checked
  7. Add and modify OpenTelemetry in Application Insights (ActivitySource and ActivityKind)learn.microsoft.com · checked
  8. Application Insights .NET SDK concepts (SDK 3.x and ActivitySource)github.com · checked