DiagnosticSource and DiagnosticListener

This article applies to: ✔️ .NET Core 3.1 and later versions ✔️ .NET Framework 4.5 and later versions

System.Diagnostics.DiagnosticSource is a module that allows code to be instrumented for production-time logging of rich data payloads for consumption within the process that was instrumented. At run time, consumers can dynamically discover data sources and subscribe to the ones of interest. System.Diagnostics.DiagnosticSource was designed to allow in-process tools to access rich data. When using System.Diagnostics.DiagnosticSource, the consumer is assumed to be within the same process and as a result, non-serializable types (for example, HttpResponseMessage or HttpContext) can be passed, giving customers plenty of data to work with.

Getting Started with DiagnosticSource

This walkthrough shows how to create a DiagnosticSource event and instrument code with System.Diagnostics.DiagnosticSource. It then explains how to consume the event by finding interesting DiagnosticListeners, subscribing to their events, and decoding event data payloads. It finishes by describing filtering, which allows only specific events to pass through the system.

DiagnosticSource Implementation

You will work with the following code. This code is an HttpClient class with a SendWebRequest method that sends an HTTP request to the URL and receives a reply.

using System.Diagnostics;
MyListener TheListener = new MyListener();
TheListener.Listening();
HTTPClient Client = new HTTPClient();
Client.SendWebRequest("https://learn.microsoft.com/dotnet/core/diagnostics/");

class HTTPClient
{
    private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
    public byte[] SendWebRequest(string url)
    {
        if (httpLogger.IsEnabled("RequestStart"))
        {
            httpLogger.Write("RequestStart", new { Url = url });
        }
        //Pretend this sends an HTTP request to the url and gets back a reply.
        byte[] reply = new byte[] { };
        return reply;
    }
}
class Observer<T> : IObserver<T>
{
    public Observer(Action<T> onNext, Action onCompleted)
    {
        _onNext = onNext ?? new Action<T>(_ => { });
        _onCompleted = onCompleted ?? new Action(() => { });
    }
    public void OnCompleted() { _onCompleted(); }
    public void OnError(Exception error) { }
    public void OnNext(T value) { _onNext(value); }
    private Action<T> _onNext;
    private Action _onCompleted;
}
class MyListener
{
    IDisposable networkSubscription;
    IDisposable listenerSubscription;
    private readonly object allListeners = new();
    public void Listening()
    {
        Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
        {
            Console.WriteLine($"Data received: {data.Key}: {data.Value}");
        };
        Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
        {
            Console.WriteLine($"New Listener discovered: {listener.Name}");
            //Subscribe to the specific DiagnosticListener of interest.
            if (listener.Name == "System.Net.Http")
            {
                //Use lock to ensure the callback code is thread safe.
                lock (allListeners)
                {
                    if (networkSubscription != null)
                    {
                        networkSubscription.Dispose();
                    }
                    IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
                    networkSubscription = listener.Subscribe(iobserver);
                }

            }
        };
        //Subscribe to discover all DiagnosticListeners
        IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
        //When a listener is created, invoke the onNext function which calls the delegate.
        listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
    }
    // Typically you leave the listenerSubscription subscription active forever.
    // However when you no longer want your callback to be called, you can
    // call listenerSubscription.Dispose() to cancel your subscription to the IObservable.
}

Running the provided implementation prints to the console.

New Listener discovered: System.Net.Http
Data received: RequestStart: { Url = https://learn.microsoft.com/dotnet/core/diagnostics/ }

Log an event

The DiagnosticSource type is an abstract base class that defines the methods needed to log events. The class that holds the implementation is DiagnosticListener. The first step in instrumenting code with DiagnosticSource is to create a DiagnosticListener. For example:

private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");

Notice that httpLogger is typed as a DiagnosticSource. That's because this code only writes events and thus is only concerned with the DiagnosticSource methods that the DiagnosticListener implements. DiagnosticListeners are given names when they are created, and this name should be the name of a logical grouping of related events (typically the component). Later, this name is used to find the Listener and subscribe to any of its events. Thus the event names only need to be unique within a component.


The DiagnosticSource logging interface consists of two methods:

    bool IsEnabled(string name)
    void Write(string name, object value);

This is instrument site specific. You need to check the instrumentation site to see what types are passed into IsEnabled. This provides you with the information to know what to cast the payload to.

A typical call site will look like:

if (httpLogger.IsEnabled("RequestStart"))
{
    httpLogger.Write("RequestStart", new { Url = url });
}

Every event has a string name (for example, RequestStart), and exactly one object as a payload. If you need to send more than one item, you can do so by creating an object with properties to represent all its information. C#'s anonymous type feature is typically used to create a type to pass 'on the fly', and makes this scheme very convenient. At the instrumentation site, you must guard the call to Write() with an IsEnabled() check on the same event name. Without this check, even when the instrumentation is inactive, the rules of the C# language require all the work of creating the payload object and calling Write() to be done, even though nothing is actually listening for the data. By guarding the Write() call, you make it efficient when the source is not enabled.

Combining everything you have:

class HTTPClient
{
    private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
    public byte[] SendWebRequest(string url)
    {
        if (httpLogger.IsEnabled("RequestStart"))
        {
            httpLogger.Write("RequestStart", new { Url = url });
        }
        //Pretend this sends an HTTP request to the url and gets back a reply.
        byte[] reply = new byte[] { };
        return reply;
    }
}

Discovery of DiagnosticListeners

The first step in receiving events is to discover which DiagnosticListeners you are interested in. DiagnosticListener supports a way of discovering DiagnosticListeners that are active in the system at run time. The API to accomplish this is the AllListeners property.

Implement an Observer<T> class that inherits from the IObservable interface, which is the 'callback' version of the IEnumerable interface. You can learn more about it at the Reactive Extensions site. An IObserver has three callbacks, OnNext, OnComplete, and OnError. An IObservable has a single method called Subscribe that gets passed one of these Observers. Once connected, the Observer gets callbacks (mostly OnNext callbacks) when things happen.

A typical use of the AllListeners static property looks like this:

class Observer<T> : IObserver<T>
{
    public Observer(Action<T> onNext, Action onCompleted)
    {
        _onNext = onNext ?? new Action<T>(_ => { });
        _onCompleted = onCompleted ?? new Action(() => { });
    }
    public void OnCompleted() { _onCompleted(); }
    public void OnError(Exception error) { }
    public void OnNext(T value) { _onNext(value); }
    private Action<T> _onNext;
    private Action _onCompleted;
}
class MyListener
{
    IDisposable networkSubscription;
    IDisposable listenerSubscription;
    private readonly object allListeners = new();
    public void Listening()
    {
        Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
        {
            Console.WriteLine($"Data received: {data.Key}: {data.Value}");
        };
        Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
        {
            Console.WriteLine($"New Listener discovered: {listener.Name}");
            //Subscribe to the specific DiagnosticListener of interest.
            if (listener.Name == "System.Net.Http")
            {
                //Use lock to ensure the callback code is thread safe.
                lock (allListeners)
                {
                    if (networkSubscription != null)
                    {
                        networkSubscription.Dispose();
                    }
                    IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
                    networkSubscription = listener.Subscribe(iobserver);
                }

            }
        };
        //Subscribe to discover all DiagnosticListeners
        IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
        //When a listener is created, invoke the onNext function which calls the delegate.
        listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
    }
    // Typically you leave the listenerSubscription subscription active forever.
    // However when you no longer want your callback to be called, you can
    // call listenerSubscription.Dispose() to cancel your subscription to the IObservable.
}

This code creates a callback delegate and, using the AllListeners.Subscribe method, requests that the delegate be called for every active DiagnosticListener in the system. The decision of whether or not to subscribe to the listener is made by inspecting its name. The code above is looking for the 'System.Net.Http' listener that you created previously.

Like all calls to Subscribe(), this one returns an IDisposable that represents the subscription itself. Callbacks will continue to happen as long as nothing calls Dispose() on this subscription object. The code example never calls Dispose(), so it will receive callbacks forever.

When you subscribe to AllListeners, you get a callback for ALL ACTIVE DiagnosticListeners. Thus, upon subscribing, you get a flurry of callbacks for all existing DiagnosticListeners, and as new ones are created, you receive a callback for those as well. You receive a complete list of everything it's possible to subscribe to.

Subscribe to DiagnosticListeners

A DiagnosticListener implements the IObservable<KeyValuePair<string, object>> interface, so you can call Subscribe() on it as well. The following code shows how to fill out the previous example:

IDisposable networkSubscription;
IDisposable listenerSubscription;
private readonly object allListeners = new();
public void Listening()
{
    Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
    {
        Console.WriteLine($"Data received: {data.Key}: {data.Value}");
    };
    Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
    {
        Console.WriteLine($"New Listener discovered: {listener.Name}");
        //Subscribe to the specific DiagnosticListener of interest.
        if (listener.Name == "System.Net.Http")
        {
            //Use lock to ensure the callback code is thread safe.
            lock (allListeners)
            {
                if (networkSubscription != null)
                {
                    networkSubscription.Dispose();
                }
                IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
                networkSubscription = listener.Subscribe(iobserver);
            }

        }
    };
    //Subscribe to discover all DiagnosticListeners
    IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
    //When a listener is created, invoke the onNext function which calls the delegate.
    listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
}

In this example, after finding the 'System.Net.Http' DiagnosticListener, an action is created that prints out the name of the listener, event, and payload.ToString().

Note

DiagnosticListener implements IObservable<KeyValuePair<string, object>>. This means on each callback we get a KeyValuePair. The key of this pair is the name of the event and the value is the payload object. The example simply logs this information to the console.

It's important to keep track of subscriptions to the DiagnosticListener. In the previous code, the networkSubscription variable remembers this. If you form another creation, you must unsubscribe the previous listener and subscribe to the new one.

The DiagnosticSource/DiagnosticListener code is thread safe, but the callback code also needs to be thread safe. To ensure the callback code is thread safe, locks are used. It is possible to create two DiagnosticListeners with the same name at the same time. To avoid race conditions, updates of shared variables are performed under the protection of a lock.

Once the previous code is run, the next time a Write() is done on 'System.Net.Http' DiagnosticListener the information will be logged to the console.

Subscriptions are independent of one another. As a result, other code can do exactly the same thing as the code example and generate two 'pipes' of the logging information.

Decode Payloads

The KeyvaluePair that is passed to the callback has the event name and payload, but the payload is typed simply as an object. There are two ways of getting more specific data:

If the payload is a well known type (for example, a string, or an HttpMessageRequest), then you can simply cast the object to the expected type (using the as operator so as not to cause an exception if you are wrong) and then access the fields. This is very efficient.

Use reflection API. For example, assume the following method is present.

    /// Define a shortcut method that fetches a field of a particular name.
    static class PropertyExtensions
    {
        static object GetProperty(this object _this, string propertyName)
        {
            return _this.GetType().GetTypeInfo().GetDeclaredProperty(propertyName)?.GetValue(_this);
        }
    }

To decode the payload more fully, you could replace the listener.Subscribe() call with the following code.

    networkSubscription = listener.Subscribe(delegate(KeyValuePair<string, object> evnt) {
        var eventName = evnt.Key;
        var payload = evnt.Value;
        if (eventName == "RequestStart")
        {
            var url = payload.GetProperty("Url") as string;
            var request = payload.GetProperty("Request");
            Console.WriteLine("Got RequestStart with URL {0} and Request {1}", url, request);
        }
    });

Note that using reflection is relatively expensive. However, using reflection is the only option if the payloads were generated using anonymous types. This overhead can be reduced by making fast, specialized property fetchers using either PropertyInfo.GetMethod.CreateDelegate() or System.Reflection.Emit namespace, but that's beyond the scope of this article. (For an example of a fast, delegate-based property fetcher, see the PropertySpec class used in the DiagnosticSourceEventSource.)

Filtering

In the previous example, the code uses the IObservable.Subscribe() method to hook up the callback. This causes all events to be given to the callback. However, DiagnosticListener has overloads of Subscribe() that allow the controller to control which events are given.

The listener.Subscribe() call in the previous example can be replaced with the following code to demonstrate.

    // Create the callback delegate.
    Action<KeyValuePair<string, object>> callback = (KeyValuePair<string, object> evnt) =>
        Console.WriteLine("From Listener {0} Received Event {1} with payload {2}", networkListener.Name, evnt.Key, evnt.Value.ToString());

    // Turn it into an observer (using the Observer<T> Class above).
    Observer<KeyValuePair<string, object>> observer = new Observer<KeyValuePair<string, object>>(callback);

    // Create a predicate (asks only for one kind of event).
    Predicate<string> predicate = (string eventName) => eventName == "RequestStart";

    // Subscribe with a filter predicate.
    IDisposable subscription = listener.Subscribe(observer, predicate);

    // subscription.Dispose() to stop the callbacks.

This efficiently subscribes to only the 'RequestStart' events. All other events will cause the DiagnosticSource.IsEnabled() method to return false and thus be efficiently filtered out.

Note

Filtering is only designed as a performance optimization. It is possible for a listener to receive events even when they do not satisfy the filter. This could occur because some other listener has subscribed to the event or because the source of the event didn't check IsEnabled() prior to sending it. If you want to be certain that a given event satisfies the filter you will need to check it inside the callback. For example:

    Action<KeyValuePair<string, object>> callback = (KeyValuePair<string, object> evnt) =>
        {
            if(predicate(evnt.Key)) // only print out events that satisfy our filter
            {
                Console.WriteLine("From Listener {0} Received Event {1} with payload {2}", networkListener.Name, evnt.Key, evnt.Value.ToString());
            }
        };
Context-based filtering

Some scenarios require advanced filtering based on extended context. Producers can call DiagnosticSource.IsEnabled overloads and supply additional event properties as shown in the following code.

//aRequest and anActivity are the current request and activity about to be logged.
if (httpLogger.IsEnabled("RequestStart", aRequest, anActivity))
    httpLogger.Write("RequestStart", new { Url="http://clr", Request=aRequest });

The next code example demonstrates that consumers can use such properties to filter events more precisely.

    // Create a predicate (asks only for Requests for certain URIs)
    Func<string, object, object, bool> predicate = (string eventName, object context, object activity) =>
    {
        if (eventName == "RequestStart")
        {
            if (context is HttpRequestMessage request)
            {
                return IsUriEnabled(request.RequestUri);
            }
        }
        return false;
    }

    // Subscribe with a filter predicate
    IDisposable subscription = listener.Subscribe(observer, predicate);

Producers are not aware of the filter a consumer has provided. DiagnosticListener will invoke the provided filter, omitting additional arguments if necessary, thus the filter should expect to receive a null context. If a producer calls IsEnabled() with event name and context, those calls are enclosed in an overload that takes only the event name. Consumers must ensure that their filter allows events without context to pass through.