Last updated: August 2024
Like most developers, you’ve seen the benefits of logging first-hand—the right log message can be the key to unlocking the trickiest of software issues. However, not all logging is created equal, and actionable logs don’t magically appear. If you want the best logs, you must optimize your approach using tried-and-true best practices.
In this article, we’ll look at ways to improve your .NET error logging and make it easy to write logs containing vital information to help you troubleshoot your .NET apps and services.
Prerequisites
Before diving into optimizing your .NET error logging, make sure you have the following prerequisites in place:
Basic Knowledge of .NET Framework: Understand the .NET framework and how to integrate logging libraries into your applications.
Understanding of Logging Concepts: Familiarize yourself with logging concepts, including log levels, log messages, and log destinations.
Use a Third-Party Logging Library
While it’s possible to use the built-in logging facilities that come with the .NET SDK (such as the Event Viewer) there are a few drawbacks. For one, it can be difficult to track events across multiple servers because each host writes to its own event log; wading through each log can be time consuming and error-prone. Second, viewing the event log requires administrative privileges, which can be a considerable burden for large-scale deployments.
A much better option is to use one of the many .NET third-party logging libraries. These libraries allow logs in various formats to be routed to a range of destinations, and they also provide logging levels so that you can easily categorize the severity of individual messages.
The two popular logging frameworks for .NET are log4net and NLog.
Writing Log Messages with log4net
log4net is a port of the popular java logging framework, log4j, to the .NET framework. Configuring log4net requires two steps: additions to your application’s .config file and modifications to your app.
Here is an example configuration for log4net that writes all log messages to the console:
<configuration>
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
</configSections>
<log4net>
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
</layout>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="ConsoleAppender" />
</root>
</log4net>
</configuration>
And the following short code snippet shows how to use log4net in your application:
using log4net;
using log4net.Config;
class LogTest
{
private static readonly ILog log = LogManager.GetLogger(typeof(LogTest));
static void Main(string[] args)
{
XmlConfigurator.Configure();
log.Info(“Info message”);
}
}
Using configuration files to control which log messages are sent to which destinations is a common technique for logging frameworks. And the second library we’re going to look at, NLog, works exactly the same way.
Writing Log Messages With NLog
The first step in using NLog is to create an NLog.config file. Here’s an example that sends all log messages to the log file file.txt:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets>
<target name="logfile" xsi:type="File" fileName="file.txt" />
</targets>
<rules>
<logger name="*" minlevel="Trace" writeTo="logfile" />
</rules>
</nlog>
To use NLog in your code, you need to acquire the current logger and call one of the Logger class methods:
using NLog;
public class MyClass
{
private static Logger logger = LogManager.GetCurrentClassLogger();
public void MyMethod()
{
logger.Info("Hello, this is an info message");
}
}
The available methods for logging are (in ascending order): Trace, Debug, Info, Warn, Error, and Fatal. Each of these methods creates a log message with a corresponding log level—an indicator of the message’s importance. Loggers either accept or ignore messages with a specific log level. If the message is accepted, the logger routes the message to one or more destinations.
Route Your Logs with Log Levels
Directing log messages based on their log level is a handy technique for making sure the right person sees a particular message. For example, when debugging your application, it’s helpful to see all Debug messages on the console. But when that code is running in production, you’ll probably want to send Error log messages directly to the operations team so they can catch potential issues before your users do.
With NLog, you can route messages to different destinations (targets) using the message log level. All of the configuration is done entirely in the NLog.config file and you don’t need to make any code modifications to get it working.
Here’s an example NLog.config that routes all messages at or above Error to a mongoDB database using the NLog.Mongo target:
<targets>
<target name="logfile" xsi:type="File" fileName="file.txt" />
<target name="mongoDB" xsi:type="Mongo"
connectionString="mongodb://localhost/Logging"
collectionName="DefaultLog"
cappedCollectionSize="26214400">
<property name="ThreadID" layout="${threadid}" bsonType="Int32" />
<property name="ThreadName" layout="${threadname}" />
<property name="ProcessID" layout="${processid}" bsonType="Int32" />
<property name="ProcessName" layout="${processname:fullName=true}" />
<property name="UserName" layout="${windows-identity}" />
</target>
</targets>
<rules>
<logger name="*" minlevel="Trace" writeTo="logfile" />
<logger name="*" minlevel="Error" writeTo="mongoDB" />
</rules>
</nlog>
With this updated config file, any Error or Fatal log messages will be automatically routed to the mongodb database.
There are times, however, when you need information from your logs that you never considered adding. One example is when an unexpected condition occurs, and your application throws an exception. This can happen in almost any code path, and being able to log as much contextual information as you can will help you diagnose why the error occurred.
Log Exceptions
There’s nothing as fatal as an application crash. And when things go wrong you need all the information you can get your hands on to effectively diagnose the cause.
In ASP.NET you can send exceptions to a logger using error handlers. To capture exceptions in your log messages, add the following to your Global.asax file:
protected void Application_Error() {
Exception lastException = Server.GetLastError();
NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
logger.Fatal(lastException);
}
Now, whenever an error occurs with your ASP.NET application a FATAL log message will be created containing the exception string.
Aggregate Your Logs
Logging is an important activity for troubleshooting and diagnosing issues with your apps and services, but as your app scales to multiple servers it becomes harder to search through all the data to find the vital piece of information you’re looking for. Modern software designs like microservices can actually make this problem worse.
Log aggregation tools pool all your logs into a central location and make it easier to search and filter the data. SolarWinds® Papertrail™ is an example of a log aggregation tool.
You can send your logs to Papertrail using the syslog protocol by installing the open-source NLog extension: NLog.Targets.Syslog. To install it, place NLog.Targets.Syslog.dll in the same folder as the NLog.dll and NLog.config files. Next, you need to add a syslog target in NLog.config. Here’s an example config file.
<nlog xmlns="http://nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<extensions>
<add assembly="NLog.Targets.Syslog" />
</extensions>
<targets>
<target name="syslog" type="Syslog">
<messageCreation>
<facility>Local7</facility>
</messageCreation>
<messageSend>
<protocol>TCP</protocol>
<tcp>
<server>logsN.papertrailapp.com</server>
<port>XXXXX</port>
<tls>
<enabled>true</enabled>
</tls>
</tcp>
</messageSend>
</target>
</targets>
<rules>
<logger name="*" minLevel="Trace" appendTo="syslog" />
</rules>
</nlog>
Note: The facility element isn’t actually used, so it’s best that you leave the value as Local7.
To get the above configuration working for your environment you’ll need to replace the values in the server and port elements with the corresponding values from your Papertrail account’s Add Systems page.
Once all your logs are available in Papertrail, you can take advantage of advanced features such as tail logging, which allows you to watch logs in real time, and Event Viewer highlighting, which helps you spot trends in your log data.
Why Optimize .NET Error Logging?
Optimizing your .NET error logging is essential for several reasons:
- Effective Troubleshooting: Well-optimized logs provide vital information to help troubleshoot .NET apps and services effectively.
- Enhanced Debugging: Properly configured logs facilitate easier debugging by providing contextual information about application behavior.
- Insightful Analytics: Optimized logging allows for better insights into application performance and user behavior. This helps in making informed decisions for future enhancements.
Conclusion
When troubleshooting issues and attempting to understand app behavior, all applications benefit from context-rich logs. .NET error logging can be as simple as writing to the console but optimizing your logging and using .NET and ASP.NET error logging best practices will make sure your logs contain the right information when you need them.
Those best practices include using tried and tested third-party logging libraries such as log4net and NLog instead of frameworks in the .NET SDK, using log levels to route log messages to the best destination, and capturing critical context when exceptions occur.
Finally, aggregating those logs into a single location—with tools such as SolarWinds Papertrail—allows you to run advanced filters and searches on them, and uncover insights hidden in your log data.