.NET Core logging and tracing

Logging and tracing are really two names for the same technique. The simple technique has been used since the early days of computers. It simply involves instrumenting an application to write output to be consumed later.

Reasons to use logging and tracing

This simple technique is surprisingly powerful. It can be used in situations where a debugger fails:

  • Issues occurring over long periods of time, can be difficult to debug with a traditional debugger. Logs allow for detailed post-mortem review spanning long periods of time. In contrast, debuggers are constrained to real-time analysis.
  • Multi-threaded applications and distributed applications are often difficult to debug. Attaching a debugger tends to modify behaviors. Detailed logs can be analyzed as needed to understand complex systems.
  • Issues in distributed applications may arise from a complex interaction between many components and it may not be reasonable to connect a debugger to every part of the system.
  • Many services shouldn't be stalled. Attaching a debugger often causes timeout failures.
  • Issues aren't always foreseen. Logging and tracing are designed for low overhead so that programs can always be recording in case an issue occurs.

.NET Core APIs

The System.Console, System.Diagnostics.Trace, and System.Diagnostics.Debug classes each provide similar print-style APIs that are convenient for logging.

The choice of which print-style API to use is up to you. The key differences are:

  • System.Console
    • Always enabled and always writes to the console.
    • Useful for information that your customer may need to see in the release.
    • Because it's the simplest approach, it's often used for ad-hoc temporary debugging. This debug code is often never checked in to source control.
  • System.Diagnostics.Trace
    • Only enabled when TRACE is defined by adding #define TRACE to your source or specifying the option /d:TRACE when compiling.
    • Writes to attached Listeners, by default the DefaultTraceListener.
    • Use this API when creating logs that will be enabled in most builds.
  • System.Diagnostics.Debug
    • Only enabled when DEBUG is defined by adding #define DEBUG to your source or specifying the option /d:DEBUG when compiling.
    • Writes to an attached debugger.
    • On *nix writes to stderr if DOTNET_DebugWriteToStdErr or COMPlus_DebugWriteToStdErr is set.
    • Use this API when creating logs that will be enabled only in debug builds.

Logging events

The following APIs are more event oriented. Rather than logging simple strings they log event objects.

Distributed Tracing

Distributed Tracing is a diagnostic technique that helps engineers localize failures and performance issues within applications, especially those that may be distributed across multiple machines or processes. This technique tracks requests through an application correlating together work done by different application components and separating it from other work the application may be doing for concurrent requests.

ILogger and logging frameworks

The low-level APIs may not be the right choice for your logging needs. You may want to consider a logging framework.

The ILogger interface has been used to create a common logging interface where the loggers can be inserted through dependency injection.

For instance, to allow you to make the best choice for your application .NET offers support for a selection of built-in and third-party frameworks:

Performance considerations

String formatting can take noticeable CPU processing time.

In performance-critical applications, it's recommended that you:

  • Avoid lots of logging when no one is listening. Avoid constructing costly logging messages by checking if logging is enabled first.
  • Only log what's useful.
  • Defer fancy formatting to the analysis stage.