Technical Resources
Educational Resources
APM Integrated Experience
Connect with Us
Though there are many debuggers and profilers for C# and .NET, the venerable log message is often the best method of troubleshooting and uncovering insights into your apps and services. If a developer’s head was like a carpenter’s toolbox, log messages would be the well-worn chisel that fits perfectly in their hand, capable of cutting through the excess to uncover the object hidden inside. Log messages are an elegant and powerful device for understanding how your software works. And like a carpenter’s chisel, they’re an essential part of your toolbox.
Logging with C# apps and services is a common problem, and many developers have already devoted hours to coming up with solutions and best practices. To get the most out of your C# logging, you should follow some best practices. Below, I’ve collected a list of tips and tricks every developer needs to know.
It’s easy to get started with C# logging because Windows environments come with a built-in way to record and retrieve log messages: the Event Log. The Event Log is used by most software running on Windows, including Internet Information Services (IIS) and Microsoft SQL Server. You can use the EventLog.WriteEntry() method from your C# code to log messages and later view them using the Event Viewer.
Here’s an example of an app-specific message sent to the Event Log from an app named app01:
System.Diagnostics.EventLog appLog =
new System.Diagnostics.EventLog() ;
appLog.Source = "app01";
appLog.WriteEntry("This is an app message for the event log.");
The EventLog.WriteEntry() method also allows you to specify the type of event log you’re recording using the EventLogEntryType type. Since the Event Viewer can filter events based on event attributes, including type, using an EventLogEntryType makes it easier for you to find the events you’re looking for when troubleshooting.
.NET provides a rich standard library for writing log messages, including the common technique of printing to the console with Console.WriteLine(). But instead of using these interfaces to create your own logging methods, you might be better off taking advantage of the many libraries and frameworks available for C#.
Some libraries you can use include log4net, Nlog, and Serilog. Each of these libraries provides a battle-tested way to create and transmit log messages. With log4net, for example, you can use the RemoteSyslogAppender class to route your log messages to a remote syslog server. If you’re using SolarWinds® Loggly®, there’s a dedicated log4net-loggly package designed to help you automatically send your log messages to Loggly over HTTP/S. Recreating this level of sophistication by writing your own code would be a huge task. By using these existing logging libraries, you can spend more time writing code and less time reinventing the wheel.
Not all log messages are created equal, and log levels (or log severities) are used to distinguish between different categories of messages. You should use these to your advantage because it makes life much easier for your coworkers when they’re trying to figure out how to respond to an error or warning later on.
Broadly speaking, most libraries provide about six log levels. In order of increasing severity, they are as follows: TRACE, DEBUG, INFO, WARN, ERROR, and CRITICAL. By tagging log messages with the appropriate log level, you can reduce the volume of log data you need to search through when troubleshooting. If you’re analyzing an issue in production with your service, you probably don’t want to start by looking up messages marked DEBUG and INFO. Instead, you’re much more likely to need to review messages marked WARN, ERROR, and CRITICAL.
Here’s an example of using log4net to record critical messages when something fatal has occurred:
if (fatalCondition)
{
ILog Log = LogManager.GetLogger(typeof(LogTest));
ILog ErrorLog = LogManager.GetLogger("error");
Log.Fatal("Fatal message");
}
There’s nothing worse than finding the log message you need when troubleshooting only to discover it’s missing vital information. To save you and your teammates pain in the future, it’s a good idea to include as much context as possible in your log messages.
For example, when logging an error condition, you should include things like error codes, request identifiers, and even the exception message. The following code snippet shows you how to do so:
try
{
ProcessInput(request);
}
catch (RequestException e)
{
Log.Error(“Error processing input for request “+ request.id + “: “ + e.errorCode);
}
Other useful data such as session IDs, IP addresses, and transaction statistics can all be included in log messages to provide richer context when analyzing and diagnosing future problems. Perhaps the most essential information to include in your log messages is a timestamp, which allows you to chronologically order events. And if you use a standard timestamp format, Loggly can automatically parse it and use it to help you navigate your logs.
Writing messages with free-form text is sufficient when you’re part of a small team. But as your team grows or your log volume increases, you need a better way to manage and analyze log data. Structured logging is the go-to solution because it requires log messages to follow a specific structure, so you know what content should appear in a given field. But structured logging isn’t just helpful when you’re writing messages; it also allows log parsers and analyzers to easily ingest log files for querying. Most logging libraries include support for creating structured logs, though the amount of support varies between projects.
For example, the log4net code in the previous section doesn’t use structured logging when printing the error message because the error message doesn’t follow a strict format. Let’s rewrite the example to use log4net’s structured logging support and make life easier for log analysis tools:
try
{
ProcessInput(request);
}
catch (RequestException e)
{
Log.Error(“Error processing input for request,” new { errorType = 12, errorCode = e.errorCode, id = request.id });
}
The output of this log message has a specific layout you can easily convert to other formats such as JSON:
ERROR 2020-03-27 16:45:13 – Error processing input for request {"errorType":12, “errorCode”: 404, “id”: 67783 }
Structured logs provide you with consistency and force you to write log messages conforming to a specific format. This means you can always be sure you’ll have the data you need.
Any log message is better than none, but there’s a huge difference between logging minimal information and crafting log messages using industry best practices and tools. Proper logging is a fundamental requirement for every dev team, and it’s a problem many developers have already worked on. Instead of creating logging tools from scratch, you can use an existing, battle-tested option. There are many choices when picking a logging library or framework, and selecting any of them will save you countless hours you would otherwise spend reinventing the logging wheel.
Once you’ve chosen a logging library, you can take advantage of its support for log levels and structured logging. And don’t forget to include as much context as possible in your messages—your teammates and your future self with thank you.
As a wise developer, you can use the five tips in this article to make the most of your logging infrastructure, create actionable log messages, and include all the relevant data to help you diagnose and troubleshoot the most complex issues.