.NET Logging Guide:
Advanced Concepts

October 31, 2022

Event logging is an important aspect of debugging and auditing applications. In Part One of this .NET Logging Guide Overview, we covered the crucial role of logging APIs in .NET. Console logging in .NET is difficult to format and introduces performance issues. On the other hand, with logging APIs, you can leverage logging levels, scopes, providers, libraries, and several different ways to implement logging in .NET, depending on your requirements.

In Part Two, we will introduce concepts like exception handling, high-performance logging using LoggerMessage, logging target types, log aggregators, and some best practices for logging in .NET.

Logging Exceptions in .NET

Exceptions occur during application execution and interrupt the normal flow of execution. An example is the DivideByZeroException, which occurs when an application tries to divide a number by zero.

public void divide(){
   ...
   int x = 7 - 7;
   int y = 5 / x;  // This would cause a DivideByZeroException
}

Exception handling is the process of addressing an exception when it occurs. To handle the exception, we would wrap the preceding code in a try-catch block.

public void divide(){

   // Already initialized logger
   ...

   try
   {
     int x = 7 - 7;
     int y = 5 / x;  
     logger.Log(LogLevel.Information, $"The value of y is: {y}");
   }
   catch(Exception ex)
   {
      logger.Log(LogLevel.Error, $"An exception occurred: {ex.Message}");
   }
}

The code above breaks out of the try block and executes the statements in the catch block. If there wasn’t an error, then the code would have run the following line:

logger.Log(LogLevel.Information, $"The value of y is: {y}")

However, in its attempt to divide five by zero, execution flowed into the catch block, executing the following line instead:

logger.Log(LogLevel.Error, $"An exception occurred: {ex.Message}")

Logging exceptions is good for debugging. If an application crashes, logs are the entry point for investigation. Viewing logs can help software developers to identify the cause of a bug, which is the first step in finding a solution. This is useful in third-party APIs. An API might return a generic error code when an exception occurs. However, the actual application developer should log the exception instead of just throwing it. This helps software developers record important information to use later in debugging. You should send exception logs to files, error monitoring tools, databases, or other destinations that your team uses to analyze error logs.

Caught Exceptions

Caught exceptions are exceptions that are already handled in code. An example is when you try to access an invalid array index. In a case like this, .NET will throw an IndexOutOfRangeException that a software developer can catch to capture the logs and prevent application termination.

The following code shows an example:

public void test(){

   // Already initialized logger
   ...

   try{
    int[] numbers = {1, 4, 2};
    int x = numbers[5];
   }
   catch(IndexOutOfRangeException ex){
    logger.LogError("EventId", ex, ex.Message);
   }
}

When logging an error, you can pass the entire exception object. You have access to all the properties in the exception, and the stack trace is one of those properties. A stack trace is the list of sequential method calls made by the application until the exception.

Uncaught exceptions

When there is a possibility of an exception, software developers can wrap code in try-catch blocks. However, other lines not enclosed in a try-catch block may throw exceptions. These exceptions are called uncaught exceptions. In this case, an error occurs, and the application crashes. Logging uncaught exceptions is essential for software developers to find the root cause of errors that cause unexpected crashes.

The following code snippet handles uncaught exceptions:

public class Test
{
   // Already initialized logger

   public static void Main()
   {
      AppDomain appDomain = AppDomain.CurrentDomain;
      appDomain.UnhandledException += new UnhandledExceptionEventHandler(CustomHandler);

      throw new Exception("Sample Exception");
   }

   static void CustomHandler(object sender, UnhandledExceptionEventArgs args)
   {
      Exception e = (Exception) args.ExceptionObject;
      logger.LogError(“EventId”, e, e.Message);
   }
}

The Main method in the above code snippet handles the UnhandledException event by registering UnhandledExceptionEventHandler(CustomHandler) in the current application scope. The job of CustomHandler is to read the exception and write an error log message.

High-Performance Logging with LoggerMessage

LoggerMessage allows software developers to create cached delegates. This speeds up both logging and performance.

LoggerMessage has some advantages over Logger extension methods:

  • Logger extension methods require boxing value types, such as int, to object. LoggerMessage bypasses boxing by using static Action fields and expansion methods with strongly typed parameters.
  • Logger extension methods parse the message template for every log. LoggerMessage only parses a template once when the message is defined. After that, the template is cached.

Defining a LoggerMessage

You can define a LoggerMessage using the LoggerMessage.Define() method. This method creates a logging Action delegate. The delegate specifies the log level, event identifier, and string message template. The following code snippet creates a LoggerMessage to capture when users enter a chatroom:

Action<ILogger, string, Exception> enteredRoom = LoggerMessage.Define<string>(
    LogLevel.Information, 
    new EventId(2, "EnteredChatRoom"), 
    "User 'userId' entered the chat room");

Using this delegate involves two things. The first is to create a method in the ILogger instance that calls the Action delegate. The code snippet below receives the userID value and passes it to the Action delegate:

public static void EnteredRoom(this ILogger logger, string userId)
{
    enteredRoom(logger, userId, null);
}

After creating the method, you can use it like this:

public async Task<User> OnUserJoinAsync(string userId)
{
    var activeUser = await _userManager.EnterRoom(userId);

    _logger.EnteredRoom(userId);

    return activeUser;
}

The code snippet above shows a user joining a chatroom, which triggers LoggerMessage after that event.

Logging Target Types

As logs are captured, they go to various destinations. A log target is where the log is stored and managed. In .NET, there are six standard logging target types, which are as follows:

Databases

To write logs to a database, you will create a DatabaseLoggerProvider by extending the ILoggerProvider and writing logic for integrating with your database.

public class DatabaseLoggerProvider: ILoggerProvider  
{   
}

Error monitoring tools

Streaming logs to a custom error monitoring tool requires integrating with that tool. You will also create an implementation of ILoggerProvider to stream the logs to the monitoring service.

Log files

A common way of logging is to stream logs to a log file. This process involves specifying the path of the log file and creating a logger provider that streams logs to files.

Standard output (Console) and debug output (Trace)

.NET has a ConsoleLoggerProvider and a DebugLoggerProvider that streams logs to the application console and debug console, respectively.

Event viewers

Event viewers allow users to view event logs. To stream event logs, .NET has a Microsoft.Extensions.Logging.EventSource provider.

Event Tracing for Windows (ETW)

ETW is a tool for logging events from applications and kernel drivers. There is a kernel-mode API for publishing ETW logs for administrative, analytic, and operational purposes.

Log Aggregators

Log aggregators allow you to retain records of application activity. This tool is useful when your application has multiple microservices because it can help with gathering, investigating, searching, and visualizing log data from various software applications.

Distributed applications capture logs in high volumes. Log aggregation makes data management in a central location seamless, which in turn aids searchability. Without log aggregation, software developers will encounter challenges in searching through and understanding voluminous log data.

A recommended way to do log aggregation in .NET is by using SeriLog. The code snippet below is a sample SeriLog configuration for writing to Logstash:

var log = new LoggerConfiguration()
         .WriteTo.Http(logstashUrl)
         .CreateLogger();

Once Logstash gathers the logs, they need to be stored somewhere. Elasticsearch can be used to store, index, and query the logs. 

For example, the following shows a query for three recent posts from janedoe:

"query": {
    "match": {
      "user": "janedoe"
    }
  },
  "aggregations": {
    "top_3_posts": {
      "terms": {
        "field": "posts",
        "size": 3
      }
    }
  }

A Kibana integration provides interactive visualizations and dashboards. The Kibana dashboards can include data resident in Elasticsearch.

Beyond the ELK stack, there are other log aggregation solutions you can use in .NET. An example is the EFK stack, which is similar to the ELK stack. The difference is that it uses Fluentbit or Fluentd instead of Logstash for collecting logs.

Best Practices for Logging in .NET

To improve logging in .NET, we recommend adopting the following best practices:

Use log levels

Using log levels allows software developers to group logs in explicit types. For debugging, a developer can filter for error-level logs. To monitor overall behavior of the application, one would look at information-level logs. Without log levels, all logs would be written at a single level. Analyzing the data would be difficult because you would not be able to filter specific classes of logs.

Another advantage of using log levels is to increase performance. By using log levels, you only store logs at severe levels. Applications can discard logs they don’t need to store. This also impacts the cost of log storage space.

Log exceptions

Logging exceptions helps with troubleshooting and debugging. Exception messages describe an application’s failure, so logging them is important. If an error occurs and you didn’t log exceptions, then finding the root cause of that error would be difficult.

Use structured logging

Write logs for readability in mind. Structured logs allow you to easily query and analyze them.

Avoid logging sensitive information

If you do not use secure coding practices, then cyber attackers can gain access to sensitive data and break into your application. You can use log masking techniques to protect data when logging.

Conclusion

In Part Two of this .NET Logging Guide Overview, we introduced some advanced concepts in the .NET logging API. We covered exception handling, logging target types, log aggregators, and some best practices when logging in .NET. These advanced techniques help ensure that you write high-performance logs that are secure and easy to analyze.

Log Everything, Answer Anything – For Free

Falcon LogScale Community Edition (previously Humio) offers a free modern log management platform for the cloud. Leverage streaming data ingestion to achieve instant visibility across distributed systems and prevent and resolve incidents.

Falcon LogScale Community Edition, available instantly at no cost, includes the following:

  • Ingest up to 16GB per day
  • 7-day retention
  • No credit card required
  • Ongoing access with no trial period
  • Index-free logging, real-time alerts and live dashboards
  • Access our marketplace and packages, including guides to build new packages
  • Learn and collaborate with an active community

Get Started Free