.NET has been steadily adding support for improved cross-platform diagnostics tracing for applications. In .NET Core 3.0, we saw the introduction of EventCounters, used for observing metric measurements over time. These counters can be consumed out-of-process as well as in-process and are cross-platform in their design.
I’ve used counters from ASP.NET Core in a few applications, to track the number of HTTP requests handled by a service over time.
As .NET 5 has been progressing, I’ve been watching some of the work in the runtime repository which adds new telemetry counters and events to some of the core components involved in making external HTTP requests. This includes, HttpClient, Sockets, DNS and Security.
In this post, I’ll demonstrate how some of this information can be consumed at runtime. Stuart Blackler has a good article which dives into listening to event counters upon which some of this code is based.
NOTE: The code in this blog post is not intended to be production-ready and is for illustration only. There may be performance overheads to consider in practical applications.
Defining an EventListener
.NET includes an abstract type named EventListener. We can derive from this type to define a listener for our sample.
Next, we can override the OnEventSourceCreated method to handle attaching to specific sources of events.
In this code, we check the name of the EventSource and filter to the specific sources we are interested in. Event sources are marked with the EventSource attribute which defines their name. For example, the HttpTelemetry class defines an event source with the name System.Net.Http.
In this example, we are interested in events and counters from four event sources.
- NameResolution Telemetry – DNS lookups
- Sockets Telemetry – Underlying network connections to a server
- Security Telemetry – Establish TLS
- Http Telemetry – HttpClient
When the EventSource matches one of the names we want to listen to, we call the EnableEvents method. In this sample code, we are accepting all event levels and keywords. We can also pass an IDictionary<string, string>, which may provide additional arguments. When consuming EventCounters, we can set how often we which to receive updated counter information. The preceding code specifies that we want the counters to send us information every two seconds.
The next method we will override is OnEventWritten. This method is called whenever an event has been written by an event source for which the event listener has enabled events.
Inside this method, we’ll add some code to first listen to event counter updates and log the current values to the console.
The first conditional checks to see if the eventData parameter (EventWrittenEventArgs) contains the expected properties which we need for our logging. If not, we return early. You’ll notice this uses the new C# 9 negated “not” pattern for some of the condition expressions.
The next piece of code uses a C# 8 switch expression to determine the final metric value. EventCounters payloads may include either an increment value or a mean value, depending on the metric type.
The main switch statement then logs to the console for the event counter names which we are interested in for this example.
This is all we need to write out the current event counter values to the console, once every two seconds. In more realistic scenarios, you may choose to send these metric values to your preferred metric service. In the past, I’ve pushed some event counter values out to DataDog, for example.
The final block of code in the OnEventWritten method deals with any events which are not event counters. Again, this is pretty basic code and simply writes the events and their payloads to the console for demonstration purposes.
Using the EventListener
We’ll utilise our TelemetryListener in a simple console application.
In the main method, we create an instance of the TelemetryListener so that we begin listening to events from the framework. Since this is IDisposable, I’ve used the C# 8 using declaration syntax. We then use a HttpClient to send a GET request to my blog homepage. After that completes, we wait for 2 seconds before exiting the application. This allows enough time for the events to fire and be received by our listener.
After running the application, we can inspect the console output.
Initially, we see several events written from each of the four sources we’ve subscribed to. HttpClient starts a request to my blog. This requires DNS resolution to occur to identify the server IP address for the connection. The socket connection is then established, and the TLS handshake begins. After we have a TLS connection, the HTTP request is transmitted, and the response is received.
We also subscribed to event counters which are included in the output. We can see that a total of one outbound HTTP request has occurred, which established one connection. There are two DNS lookups which may be surprising. The first is caused by the Socket’s static constructor which causes an empty hostname lookup. After that, our actual lookup to my blog URL was made. We also see a cumulative count of the HTTP bytes sent and received. There was also one TLS handshake to establish the HTTPS connection to my blog.
This post serves to highlight that the .NET team are actively adding new telemetry in the form of events and event counters which may be useful when diagnosing and fixing problematic application behaviour. These events and counters can be collected in-process at runtime, and may be used to push metrics to external monitoring. They are also supported cross-platform for out-of-process tracing and monitoring of application behaviour.
In future posts, I hope to dive further into tracing, observability and usage of this data.
As a reminder, the full source used in this blog post can be found here.