Event logging is an important aspect of debugging and auditing applications. 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.
.NET Logging Part Three
In part three of the .NET logging guide series, we relate the common logging mistakes made in .NET and how to avoid them.
Log your data with CrowdStrike Falcon Next-Gen SIEM
Elevate your cybersecurity with the CrowdStrike Falcon® platform, the premier AI-native platform for SIEM and log management. Experience security logging at a petabyte scale, choosing between cloud-native or self-hosted deployment options. Log your data with a powerful, index-free architecture, without bottlenecks, allowing threat hunting with over 1 PB of data ingestion per day. Ensure real-time search capabilities to outpace adversaries, achieving sub-second latency for complex queries. Benefit from 360-degree visibility, consolidating data to break down silos and enabling security, IT, and DevOps teams to hunt threats, monitor performance, and ensure compliance seamlessly across 3 billion events in less than 1 second.