|
| 1 | +--- |
| 2 | +title: DiagnosticSource and DiagnosticListener |
| 3 | +description: An overview of DiagnosticSource/DiagnosticListener including guidance on logging events, instrumenting code, and consuming data. |
| 4 | +ms.date: 05/12/2022 |
| 5 | +--- |
| 6 | +# DiagnosticSource and DiagnosticListener |
| 7 | + |
| 8 | +**This article applies to: ✔️** .NET Core 3.1 and later versions **✔️** .NET Framework 4.5 and later versions |
| 9 | + |
| 10 | +<xref:System.Diagnostics.DiagnosticSource?displayProperty=nameWithType> is a module that allows code to be instrumented for production-time |
| 11 | +logging of rich data payloads for consumption within the process that was instrumented. At run time, consumers can dynamically discover |
| 12 | +data sources and subscribe to the ones of interest. <xref:System.Diagnostics.DiagnosticSource?displayProperty=nameWithType> was designed to allow in-process |
| 13 | +tools to access rich data. When using <xref:System.Diagnostics.DiagnosticSource?displayProperty=nameWithType>, the consumer is assumed |
| 14 | +to be within the same process and as a result, non-serializable types (for example, `HttpResponseMessage` or `HttpContext`) can be passed, |
| 15 | +giving customers plenty of data to work with. |
| 16 | + |
| 17 | +## Getting Started with DiagnosticSource |
| 18 | + |
| 19 | +This walkthrough shows how to create a DiagnosticSource event and instrument code with <xref:System.Diagnostics.DiagnosticSource?displayProperty=nameWithType>. |
| 20 | +It then explains how to consume the event by finding interesting DiagnosticListeners, subscribing to their events, and decoding event data payloads. |
| 21 | +It finishes by describing *filtering*, which allows only specific events to pass through the system. |
| 22 | + |
| 23 | +## DiagnosticSource Implementation |
| 24 | + |
| 25 | +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. |
| 26 | + |
| 27 | +:::code language="csharp" source="snippets/diagnosticsource/csharp/Program.cs" id="WholeProgram"::: |
| 28 | + |
| 29 | +Running the provided implementation prints to the console. |
| 30 | + |
| 31 | +```console |
| 32 | +New Listener discovered: System.Net.Http |
| 33 | +Data received: RequestStart: { Url = https://docs.microsoft.com/dotnet/core/diagnostics/ } |
| 34 | +``` |
| 35 | + |
| 36 | +## Log an event |
| 37 | + |
| 38 | +The `DiagnosticSource` type is an abstract base class that defines the methods needed to log events. The class that holds the implementation is `DiagnosticListener`. |
| 39 | +The first step in instrumenting code with `DiagnosticSource` is to create a |
| 40 | +`DiagnosticListener`. For example: |
| 41 | + |
| 42 | +:::code language="csharp" source="snippets/diagnosticsource/csharp/Program.cs" id="snippit1"::: |
| 43 | + |
| 44 | +Notice that `httpLogger` is typed as a `DiagnosticSource`. |
| 45 | +That's because this code |
| 46 | +only writes events and thus is only concerned with the `DiagnosticSource` methods that |
| 47 | +the `DiagnosticListener` implements. `DiagnosticListeners` are given names when they are created, |
| 48 | +and this name should be the name of a logical grouping of related events (typically the component). |
| 49 | +Later, this name is used to find the Listener and subscribe to any of its events. |
| 50 | +Thus the event names only need to be unique within a component. |
| 51 | + |
| 52 | +------------------------------------------- |
| 53 | + |
| 54 | +The `DiagnosticSource` logging |
| 55 | +interface consists of two methods: |
| 56 | + |
| 57 | +```csharp |
| 58 | + bool IsEnabled(string name) |
| 59 | + void Write(string name, object value); |
| 60 | +``` |
| 61 | + |
| 62 | +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. |
| 63 | + |
| 64 | +A typical call site will look like: |
| 65 | + |
| 66 | +:::code language="csharp" source="snippets/diagnosticsource/csharp/Program.cs" id="snippit3"::: |
| 67 | + |
| 68 | +Every event has a `string` name (for example, `RequestStart`), and exactly one `object` as a payload. |
| 69 | +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](../../csharp/fundamentals/types/anonymous-types.md) |
| 70 | +feature is typically used to create a type to pass 'on the fly', and makes this scheme very |
| 71 | +convenient. At the instrumentation site, you must guard the call to `Write()` with an `IsEnabled()` check on |
| 72 | +the same event name. Without this check, even when the instrumentation is inactive, the rules |
| 73 | +of the C# language require all the work of creating the payload `object` and calling `Write()` to be |
| 74 | +done, even though nothing is actually listening for the data. By guarding the `Write()` call, you |
| 75 | +make it efficient when the source is not enabled. |
| 76 | + |
| 77 | +Combining everything you have: |
| 78 | + |
| 79 | +:::code language="csharp" source="snippets/diagnosticsource/csharp/Program.cs" id="snippit4"::: |
| 80 | + |
| 81 | +------------------------------------------- |
| 82 | + |
| 83 | +### Discovery of DiagnosticListeners |
| 84 | + |
| 85 | +The first step in receiving events is to discover which `DiagnosticListeners` you are |
| 86 | +interested in. `DiagnosticListener` supports a way of discovering `DiagnosticListeners` that are |
| 87 | +active in the system at run time. The API to accomplish this is the <xref:System.Diagnostics.DiagnosticListener.AllListeners> property. |
| 88 | + |
| 89 | +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](https://github.com/dotnet/reactive) site. |
| 90 | +An `IObserver` has three callbacks, `OnNext`, `OnComplete`, |
| 91 | +and `OnError`. An `IObservable` has a single method called `Subscribe` that gets passed one of these |
| 92 | +Observers. Once connected, the Observer gets callbacks (mostly `OnNext` callbacks) when things |
| 93 | +happen. |
| 94 | + |
| 95 | +A typical use of the `AllListeners` static property looks like this: |
| 96 | + |
| 97 | +:::code language="csharp" source="snippets/diagnosticsource/csharp/Program.cs" id="snippit5"::: |
| 98 | + |
| 99 | +This code creates a callback delegate and, using the `AllListeners.Subscribe` method, requests |
| 100 | +that the delegate be called for every active `DiagnosticListener` in the system. The decision of whether or not to subscribe to the listener |
| 101 | +is made by inspecting its name. The code above is looking for the 'System.Net.Http' listener that you created previously. |
| 102 | + |
| 103 | +Like all calls to `Subscribe()`, this one returns an `IDisposable` that represents the subscription itself. |
| 104 | +Callbacks will continue to happen as long as nothing calls `Dispose()` on this subscription object. |
| 105 | +The code example never calls `Dispose()`, so it will receive callbacks forever. |
| 106 | + |
| 107 | +When you subscribe to `AllListeners`, you get a callback for ALL ACTIVE `DiagnosticListeners`. |
| 108 | +Thus, upon subscribing, you get a flurry of callbacks for all existing `DiagnosticListeners`, and as new ones |
| 109 | +are created, you receive a callback for those as well. You receive a complete list of everything it's possible |
| 110 | +to subscribe to. |
| 111 | + |
| 112 | +#### Subscribe to DiagnosticListeners |
| 113 | + |
| 114 | +A `DiagnosticListener` implements the `IObservable<KeyValuePair<string, object>>` interface, so you can |
| 115 | +call `Subscribe()` on it as well. The following code shows how to fill out the previous example: |
| 116 | + |
| 117 | +:::code language="csharp" source="snippets/diagnosticsource/csharp/Program.cs" id="snippit6"::: |
| 118 | + |
| 119 | +In this example, after finding the 'System.Net.Http' `DiagnosticListener`, an action is created that |
| 120 | +prints out the name of the listener, event, and `payload.ToString()`. |
| 121 | + |
| 122 | +> [!NOTE] |
| 123 | +> `DiagnosticListener` implements `IObservable<KeyValuePair<string, object>>`. This means |
| 124 | + on each callback we get a `KeyValuePair`. The key of this pair is the name of the event |
| 125 | + and the value is the payload `object`. The example simply logs this information |
| 126 | + to the console. |
| 127 | + |
| 128 | +It's important to keep track of subscriptions to the `DiagnosticListener`. In the previous code, the |
| 129 | +`networkSubscription` variable remembers this. If you form another `creation`, you must |
| 130 | +unsubscribe the previous listener and subscribe to the new one. |
| 131 | + |
| 132 | +The `DiagnosticSource`/`DiagnosticListener` code is thread safe, but the |
| 133 | +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` |
| 134 | +with the same name at the same time. To avoid race conditions, updates of shared variables are performed under the protection of a lock. |
| 135 | + |
| 136 | +Once the previous code is run, the next time a `Write()` is done on 'System.Net.Http' `DiagnosticListener` |
| 137 | +the information will be logged to the console. |
| 138 | + |
| 139 | +Subscriptions are independent of one another. As a result, other code |
| 140 | +can do exactly the same thing as the code example and generate two 'pipes' of the logging |
| 141 | +information. |
| 142 | + |
| 143 | +#### Decode Payloads |
| 144 | + |
| 145 | +The `KeyvaluePair` that is passed to the callback has the event name and payload, but the payload is typed simply as |
| 146 | +an `object`. There are two ways of getting more specific data: |
| 147 | + |
| 148 | +If the payload is a well known type (for example, a `string`, or an `HttpMessageRequest`), then you can simply |
| 149 | +cast the `object` to the expected type (using the `as` operator so as not to cause an exception if |
| 150 | +you are wrong) and then access the fields. This is very efficient. |
| 151 | + |
| 152 | +Use reflection API. For example, assume the following method is present. |
| 153 | + |
| 154 | +```csharp |
| 155 | + /// Define a shortcut method that fetches a field of a particular name. |
| 156 | + static class PropertyExtensions |
| 157 | + { |
| 158 | + static object GetProperty(this object _this, string propertyName) |
| 159 | + { |
| 160 | + return _this.GetType().GetTypeInfo().GetDeclaredProperty(propertyName)?.GetValue(_this); |
| 161 | + } |
| 162 | + } |
| 163 | +``` |
| 164 | + |
| 165 | +To decode the payload more fully, you could replace the `listener.Subscribe()` call with the following code. |
| 166 | + |
| 167 | +```csharp |
| 168 | + networkSubscription = listener.Subscribe(delegate(KeyValuePair<string, object> evnt) { |
| 169 | + var eventName = evnt.Key; |
| 170 | + var payload = evnt.Value; |
| 171 | + if (eventName == "RequestStart") |
| 172 | + { |
| 173 | + var url = payload.GetProperty("Url") as string; |
| 174 | + var request = payload.GetProperty("Request"); |
| 175 | + Console.WriteLine("Got RequestStart with URL {0} and Request {1}", url, request); |
| 176 | + } |
| 177 | + }); |
| 178 | +``` |
| 179 | + |
| 180 | +Note that using reflection is relatively expensive. However, using reflection is the only |
| 181 | +option if the payloads were generated using anonymous types. This overhead can be reduced by |
| 182 | +making fast, specialized property fetchers using either [PropertyInfo.GetMethod.CreateDelegate()](xref:System.Reflection.MethodInfo.CreateDelegate%2A) or |
| 183 | +xref<System.Reflection.Emit> namespace, but that's beyond the scope of this article. |
| 184 | +(For an example of a fast, delegate-based property fetcher, see the [PropertySpec](https://github.com/dotnet/runtime/blob/6de7147b9266d7730b0d73ba67632b0c198cb11e/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticSourceEventSource.cs#L1235) |
| 185 | +class used in the `DiagnosticSourceEventSource`.) |
| 186 | + |
| 187 | +#### Filtering |
| 188 | + |
| 189 | +In the previous example, the code uses the `IObservable.Subscribe()` method to hook up the callback. This |
| 190 | +causes all events to be given to the callback. However, `DiagnosticListener` has overloads of |
| 191 | +`Subscribe()` that allow the controller to control which events are given. |
| 192 | + |
| 193 | +The `listener.Subscribe()` call in the previous example can be replaced with the following code to demonstrate. |
| 194 | + |
| 195 | +```csharp |
| 196 | + // Create the callback delegate. |
| 197 | + Action<KeyValuePair<string, object>> callback = (KeyValuePair<string, object> evnt) => |
| 198 | + Console.WriteLine("From Listener {0} Received Event {1} with payload {2}", networkListener.Name, evnt.Key, evnt.Value.ToString()); |
| 199 | + |
| 200 | + // Turn it into an observer (using the Observer<T> Class above). |
| 201 | + Observer<KeyValuePair<string, object>> observer = new Observer<KeyValuePair<string, object>>(callback); |
| 202 | + |
| 203 | + // Create a predicate (asks only for one kind of event). |
| 204 | + Predicate<string> predicate = (string eventName) => eventName == "RequestStart"; |
| 205 | + |
| 206 | + // Subscribe with a filter predicate. |
| 207 | + IDisposable subscription = listener.Subscribe(observer, predicate); |
| 208 | + |
| 209 | + // subscription.Dispose() to stop the callbacks. |
| 210 | +``` |
| 211 | + |
| 212 | +This efficiently subscribes to only the 'RequestStart' events. All other events will cause the `DiagnosticSource.IsEnabled()` |
| 213 | +method to return `false` and thus be efficiently filtered out. |
| 214 | + |
| 215 | +##### Context-based filtering |
| 216 | + |
| 217 | +Some scenarios require advanced filtering based on extended context. |
| 218 | +Producers can call <xref:System.Diagnostics.DiagnosticSource.IsEnabled%2A?displayProperty=nameWithType> overloads and supply additional event properties as shown in the following code. |
| 219 | + |
| 220 | +```csharp |
| 221 | +//aRequest and anActivity are the current request and activity about to be logged. |
| 222 | +if (httpLogger.IsEnabled("RequestStart", aRequest, anActivity)) |
| 223 | + httpLogger.Write("RequestStart", new { Url="http://clr", Request=aRequest }); |
| 224 | +``` |
| 225 | + |
| 226 | +The next code example demonstrates that consumers can use such properties to filter events more precisely. |
| 227 | + |
| 228 | +```csharp |
| 229 | + // Create a predicate (asks only for Requests for certains URIs) |
| 230 | + Func<string, object, object, bool> predicate = (string eventName, object context, object activity) => |
| 231 | + { |
| 232 | + if (eventName == "RequestStart") |
| 233 | + { |
| 234 | + if (context is HttpRequestMessage request) |
| 235 | + { |
| 236 | + return IsUriEnabled(request.RequestUri); |
| 237 | + } |
| 238 | + } |
| 239 | + return false; |
| 240 | + } |
| 241 | + |
| 242 | + // Subscribe with a filter predicate |
| 243 | + IDisposable subscription = listener.Subscribe(observer, predicate); |
| 244 | +``` |
| 245 | + |
| 246 | +Producers are not aware of the filter a consumer has provided. `DiagnosticListener` |
| 247 | +will invoke the provided filter, omitting additional arguments if necessary, thus the filter |
| 248 | +should expect to receive a `null` context. |
| 249 | +If a producer calls `IsEnabled()` with event name and context, those calls are enclosed in an overload that takes |
| 250 | +only the event name. Consumers must ensure that their filter allows events without context |
| 251 | +to pass through. |
0 commit comments