Skip to content

Commit 8d732e4

Browse files
Create diagnosticsource-getting-started.md (#29255)
* Create diagnosticsource-getting-started.md * Alter formatting and clarify contents of the file * Fixing lint failures * Fix up the code and finish up the documentation * Combine into one file * Fixed suggestions from code review * Two additional edits * Response to suggestions Use you instead of we, shorten sentences to help with localization, use active voice, comments should be capitalized and have punctuation. * Update to working example Add clarifying comments, URLs for the APIs and responded to suggestions. * Updating from feedback on code review * Add DiagnosticSource and DiagnosticListener to toc * Add .csproj and .cs files * Add links to the .cs and .csproj folders * Move the file to the correct place * move location, add blocking * Linking the code to the snippits * Correct filepath * Responding to feedback * Added missing character * removing links
1 parent 71fc75f commit 8d732e4

File tree

4 files changed

+349
-0
lines changed

4 files changed

+349
-0
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net6.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
</Project>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// <WholeProgram>
2+
using System.Diagnostics;
3+
MyListener TheListener = new MyListener();
4+
TheListener.Listening();
5+
HTTPClient Client = new HTTPClient();
6+
Client.SendWebRequest("https://docs.microsoft.com/dotnet/core/diagnostics/");
7+
8+
// <snippit4>
9+
class HTTPClient
10+
{
11+
// <snippit1>
12+
private static DiagnosticSource httpLogger = new DiagnosticListener("System.Net.Http");
13+
// </snippit1>
14+
public byte[] SendWebRequest(string url)
15+
{
16+
// <snippit3>
17+
if (httpLogger.IsEnabled("RequestStart"))
18+
{
19+
httpLogger.Write("RequestStart", new { Url = url });
20+
}
21+
// </snippit3>
22+
//Pretend this sends an HTTP request to the url and gets back a reply.
23+
byte[] reply = new byte[] { };
24+
return reply;
25+
}
26+
}
27+
// </snippit4>
28+
// <snippit5>
29+
class Observer<T> : IObserver<T>
30+
{
31+
public Observer(Action<T> onNext, Action onCompleted)
32+
{
33+
_onNext = onNext ?? new Action<T>(_ => { });
34+
_onCompleted = onCompleted ?? new Action(() => { });
35+
}
36+
public void OnCompleted() { _onCompleted(); }
37+
public void OnError(Exception error) { }
38+
public void OnNext(T value) { _onNext(value); }
39+
private Action<T> _onNext;
40+
private Action _onCompleted;
41+
}
42+
class MyListener
43+
{
44+
// <snippit6>
45+
IDisposable networkSubscription;
46+
IDisposable listenerSubscription;
47+
private readonly object allListeners = new();
48+
public void Listening()
49+
{
50+
Action<KeyValuePair<string, object>> whenHeard = delegate (KeyValuePair<string, object> data)
51+
{
52+
Console.WriteLine($"Data received: {data.Key}: {data.Value}");
53+
};
54+
Action<DiagnosticListener> onNewListener = delegate (DiagnosticListener listener)
55+
{
56+
Console.WriteLine($"New Listener discovered: {listener.Name}");
57+
//Suscribe to the specific DiagnosticListener of interest.
58+
if (listener.Name == "System.Net.Http")
59+
{
60+
//Use lock to ensure the callback code is thread safe.
61+
lock (allListeners)
62+
{
63+
if (networkSubscription != null)
64+
{
65+
networkSubscription.Dispose();
66+
}
67+
IObserver<KeyValuePair<string, object>> iobserver = new Observer<KeyValuePair<string, object>>(whenHeard, null);
68+
networkSubscription = listener.Subscribe(iobserver);
69+
}
70+
71+
}
72+
};
73+
//Subscribe to discover all DiagnosticListeners
74+
IObserver<DiagnosticListener> observer = new Observer<DiagnosticListener>(onNewListener, null);
75+
//When a listener is created, invoke the onNext function which calls the delegate.
76+
listenerSubscription = DiagnosticListener.AllListeners.Subscribe(observer);
77+
}
78+
// </snippit6>
79+
// Typically you leave the listenerSubscription subscription active forever.
80+
// However when you no longer want your callback to be called, you can
81+
// call listenerSubscription.Dispose() to cancel your subscription to the IObservable.
82+
}
83+
// </snippit5>
84+
// </WholeProgram>

docs/fundamentals/toc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,10 @@ items:
654654
href: ../core/diagnostics/eventsource-collect-and-view-traces.md
655655
- name: Activity IDs
656656
href: ../core/diagnostics/eventsource-activity-ids.md
657+
- name: DiagnosticSource and DiagnosticListener
658+
items:
659+
- name: Getting started
660+
href: ../core/diagnostics/diagnosticsource-diagnosticlistener.md
657661
- name: EventPipe
658662
href: ../core/diagnostics/eventpipe.md
659663
- name: Metrics

0 commit comments

Comments
 (0)