In this article, we’ll look at different ways of collecting and inspecting events from the .NET Core runtime and base class library (BCL).
EventListener
The EventListener
class allows us to get events of the running application. Let’s learn how to use it with an example application. Our application performs an HTTP get and prints the length of the received response.
using var httpClient = new HttpClient(); string response = await httpClient.GetStringAsync("http://redhatloves.net/"); Console.WriteLine($"Received response with length {response.Length}");
The .NET Core base classes (like HttpClient
) record events using the EventSource
class. Each EventSource
has a name. EventSources
are created when we are using some functionality. To know what sources are available, we’ll create an EventListener
and override OnEventSourceCreated
.
sealed class EventSourceCreatedListener : EventListener { protected override void OnEventSourceCreated(EventSource eventSource) { base.OnEventSourceCreated(eventSource); Console.WriteLine($"New event source: {eventSource.Name}"); } }
When we instantiate this class, the base constructor will call OnEventSourceCreated
for all existing sources. When new sources are added (for example, when we first use HttpClient
) OnEventSourceCreated
will be called for them, too.
We add an instance to our application:
using var eventSourceListener = new EventSourceCreatedListener(); using var httpClient = new HttpClient(); ...
When we run the application this is this output:
New event source: Microsoft-Windows-DotNETRuntime New event source: System.Runtime New event source: Microsoft-System-Net-Http New event source: System.Diagnostics.Eventing.FrameworkEventSource New event source: Microsoft-Diagnostics-DiagnosticSource New event source: Microsoft-System-Net-Sockets New event source: Microsoft-System-Net-NameResolution New event source: System.Threading.Tasks.TplEventSource New event source: System.Buffers.ArrayPoolEventSource New event source: Microsoft-System-Net-Security New event source: System.Collections.Concurrent.ConcurrentCollectionsEventSource
The Microsoft-Windows-DotNETRuntime
and System.Runtime
are present when we create the EventSourceCreatedListener
. The other event sources are created when we use the HttpClient
. There is an event source for HTTP (Microsoft-System-Net-Http), we also see an event source for the name resolution (Microsoft-System-Net-NameResolution), security (Microsoft-System-Net-Security) and sockets (Microsoft-System-Net-Sockets). Let’s print events from the HTTP source to Console
with another listener.
We create an EventListener
that prints events for a specific EventSource
:
sealed class EventSourceListener : EventListener { private readonly string _eventSourceName; private readonly StringBuilder _messageBuilder = new StringBuilder(); public EventSourceListener(string name) { _eventSourceName = name; } protected override void OnEventSourceCreated(EventSource eventSource) { base.OnEventSourceCreated(eventSource); if (eventSource.Name == _eventSourceName) { EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All); } } protected override void OnEventWritten(EventWrittenEventArgs eventData) { base.OnEventWritten(eventData); string message; lock (_messageBuilder) { _messageBuilder.Append("<- Event "); _messageBuilder.Append(eventData.EventSource.Name); _messageBuilder.Append(" - "); _messageBuilder.Append(eventData.EventName); _messageBuilder.Append(" : "); _messageBuilder.AppendJoin(',', eventData.Payload); _messageBuilder.AppendLine(" ->"); message = _messageBuilder.ToString(); _messageBuilder.Clear(); } Console.WriteLine(message); } }
In OnEventSourceCreated
we enable the receiving of events for the specific source. We’re not filtering for a specific log level (EventLevel.LogAlways) or specific keywords (EventKeywords.All). In OnEventWritten
we print the event to the Console
.
Now we add an instance to our program for the Microsoft-System-Net-Http
EventSource:
using var httpEventListener = new EventSourceListener("Microsoft-System-Net-Http"); ...
When we run our program again we get a lot of events from the HTTP EventSource
. For example, we see that our request gets redirected and we see information about the SSL negotiation.
… Event Microsoft-System-Net-Http - HandlerMessage : 55530882,6044116,0,SendAsyncCore,Received response: StatusCode: 301, ReasonPhrase: 'Moved Permanently' … <- Event Microsoft-System-Net-Http - HandlerMessage : 16294043,41622463,0,TraceConnection,HttpConnection(HttpConnectionPool https://developers.redhat.com:443). SslProtocol:Tls12, NegotiatedApplicationProtocol:, NegotiatedCipherSuite:TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, CipherAlgorithm:Aes256, CipherStrength:256, HashAlgorithm:None, HashStrength:0, KeyExchangeAlgorithm:DiffieHellman, KeyExchangeStrength:0, LocalCertificate:, RemoteCertificate:[Subject] CN=openshift.redhat.com, O=Red Hat, L=Raleigh, S=North Carolina, C=US [Issuer] CN=GeoTrust RSA CA 2018, OU=www.digicert.com, O=DigiCert Inc, C=US …
The EventSource
named Microsoft-Windows-DotNETRuntime
provides events from the runtime like the Garbage Collector (GC), just in time compiler (JIT) and ThreadPool
.
dotnet-trace
dotnet-trace
is a new global tool with .NET Core 3.0. With dotnet-trace
we can create a trace from a running application.
To install dotnet-trace run:
dotnet tool install --global dotnet-trace
A trace can be created with the following command:
dotnet trace collect -p <pid> [--buffersize 256] [-o <outputpath>] [--providers <providerlist>] [--profile <profilename>] [--format NetTrace]
We need to specify the <pid>
of the program that we want to trace. Optionally, we can specify a buffer size (default: 256MB). The output path is the filename for the trace file. providers
and profile
allow us to specify the events we are interested in. A profile
is a pre-defined number of providers. You can see the available profiles using the list-profiles
subcommand:
$ dotnet trace list-profiles cpu-sampling - Useful for tracking CPU usage and general .NET runtime information. This is the default option if no profile or providers are specified. gc-verbose - Tracks GC collections and samples object allocations. gc-collect - Tracks GC collections only at very low overhead.
As indicated in the output, if you don’t specify providers
and a profile
, the cpu-samping
profile is used by default.
Multiple providers can be specified using a comma to separate them. By default, events will be captured for any keyword and level verbose. We can change this by appending a :[<keyword_hex_nr>]:<loglevel_nr>]
suffix to the provider name. The keyword_hex_nr
may be omitted to indicate all keywords. For example, to trace the Microsoft-System-Net-Http
EventSource
at EventLevel.Informational, we can specify: --providers Microsoft-System-Net-Http::4
. An asterisk may be used to trace all events (i.e. --providers '*'
).
Some providers can be configured with additional key-value pairs. These are added to the provider name :[key1=value1][;key2=value2]
. When the EventSource
has associated EventCounters
, the report interval can be set using the EventCounterIntervalSec
key.
The default format (nettrace) is meant for the Windows PerfView tool. If you are doing performance analysis, you can use the speedscope
format which can be opened on any platform using the https://www.speedscope.app/ website, or offline using the npm speedscopeapp package. When you are logging in the speedscope
format, the trace is first captured into the nettrace
format and then converted into the speedscope
format. This conversion can also be done on the command line using dotnet trace convert
.
For example, this is how you can trace a .NET Core application named web
for the default profile (cpu-sampling) and output a speedscope
file.
$ dotnet trace collect -p $(pidof web) --format speedscope Provider Name Keywords Level Enabled By Microsoft-System-Net-Http 0xFFFFFFFFFFFFFFFF Verbose(5) --providers Process : /tmp/web/bin/Debug/netcoreapp3.0/web Output File : /tmp/trace/trace.nettrace [00:00:00:08] Recording trace 2.3301 (MB) Press or to exit... Trace completed. Writing: /tmp/trace/trace.speedscope.json Conversion complete
As indicated by the tool, you can stop tracing using Ctrl+C or Enter. For more information about using speedscope, check out Adam Sitnick’s blog post.
EventPipe environment variables
dotnet-trace enables the trace output when an application is running. By using environment variables we can also enable tracing from the application start. This also allows us to trace a tree of processes.
Name | Description |
COMPlus_EnableEventPipe | Enable/disable event pipe. |
COMPlus_EventPipeOutputPath | The full path excluding filename for the trace file. |
COMPlus_EventPipeCircularMB | Circular buffer size in megabytes (default: 1024 MB). |
COMPlus_EventPipeConfig | Configuration for EventPipe. |
The EventPipeConfig
has the same format as the dotnet trace
providers
argument. The EventPipeOutputPath
refers to a folder. This allows multiple processes to write a trace file by including the process pid in the filename. The EventPipe
uses the nettrace
format.
For example, to capture all events from a console application, we can run:
COMPlus_EnableEventPipe=1 COMPlus_EventPipeConfig=* dotnet console.dll
TraceEvent library
If you want to write tools that process these trace files, you can use the Microsoft.Diagnostics.Tracing.TraceEvent library. This library also allows us to read the nettrace files on Linux:
# add the package: # dotnet add package Microsoft.Diagnostics.Tracing.TraceEvent using System; using Microsoft.Diagnostics.Tracing; using Microsoft.Diagnostics.Tracing.Etlx; using Microsoft.Diagnostics.Tracing.Parsers; namespace console { class Program { static void Main(string[] args) { if (args.Length < 1) { Console.WriteLine("You must specify a nettrace file."); return; } using (var traceLog = new TraceLog(TraceLog.CreateFromEventPipeDataFile(args[0]))) { var traceSource = traceLog.Events.GetSource(); traceSource.AllEvents += delegate (TraceEvent data) { Console.WriteLine("{0}", data); }; traceSource.Process(); } } } }
Microsoft.Extensions.Logging
ASP.NET Core uses the Microsoft.Extensions.Logging
package for logging. You can find more information about this in Logging in .NET Core and ASP.NET Core. Multiple sinks can be configured as Logging providers. The EventSource
logger provider (which is included by default) allows to include the ASP.NET Core log output in the trace output. The EventSource section describes how to use the Microsoft-Extensions-Logging
EventSource with dotnet-trace
, and control the format, and log level of the individual loggers.
dotnet-counters
dotnet-counters is a new global tool with .NET Core 3.0. With dotnet-counters
we can live monitor EventCounters of a running .NET Core application.
To install counters run:
dotnet tool install --global dotnet-counters
Monitoring can be performed with the following command.
dotnet-counters monitor -p <pid> [--refreshInterval <intervalSec>] [<counter_list>]
When no counter list is specified, a default set of .NET runtime counters are shown information about the GC, ThreadPool
, lock contention, active timers, and exception count.
<counter_list>
can be specified as a space separated list of provider_name[:counter_name]
. When the <counter_name>
is omitted, all counters of the provider are included.
Conclusion
In this article, you’ve learned how you can capture events from the .NET Core runtime and BCL using EventListener
, dotnet-trace
, and EventPipe
environment variables. Using the LoggingEventSource
we can including events from Microsoft.Extensions.Logging
(like ASP.NET Core). To view and analyze these events you can use PerfView on windows, the cross-platform speedscope, or build your own cross-platform tools using Microsoft.Diagnostics.Tracing.TraceEvent. With dotnet-counters
we can get a live view of EventCounters.