In this article, we will learn how to implement Log4net, a logging framework for C# applications, and how to use it effectively.
Knowing how to log properly helps us to be more efficient while debugging, monitoring, and maintaining our applications.
Let’s begin.
Why Log4net?
Log4net is a popular logging framework for .NET applications. It’s flexible and works with different output formats and destinations. It’s also fast and doesn’t slow down our application much. We can easily change how it logs without changing our code. Plus, it works well with many .NET frameworks and libraries.
While Log4net is a good choice, other popular .NET logging frameworks exist, such as Serilog and NLog.
Before we start, let’s talk about some good practices when using Log4net.
Good Practices When Using Log4net
First, we need to use the right log levels like DEBUG, INFO, WARN, ERROR, and FATAL. This helps us sort our logs easily.
It’s also important to add helpful details to our log messages. This way, we’ll know what is happening in our app when something goes wrong.
We should also set up log rotation. This keeps our log files from getting too big and taking up too much space.
Lastly, it’s a good idea to name our loggers in a consistent way. When we have a good naming system, it’s easier to tell which part of our app a log message is coming from. This is especially helpful in bigger projects.
If we stick to these practices, we’ll end up with a well-organized logging system. This will make our lives easier both while we’re developing and when our app is up and running.
Now that we know a bit about Log4net, let’s set it up in our project.
Setting Up Log4net
To start with Log4net, we first need to install it:
dotnet add package log4net
Then, we need to create a log4net.config
file in our project root. This file will contain the configuration for Log4net.
Configuring Log4net in C#
First, let’s start with a basic configuration:
<?xml version="1.0" encoding="utf-8" ?> <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 - %message%newline" /> </layout> </appender> <appender name="FileAppender" type="log4net.Appender.RollingFileAppender"> <file value="logs/application.log" /> <appendToFile value="true" /> <rollingStyle value="Size" /> <maxSizeRollBackups value="5" /> <maximumFileSize value="10MB" /> <staticLogFileName value="true" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" /> </layout> </appender> <root> <level value="ALL" /> <appender-ref ref="ConsoleAppender" /> <appender-ref ref="FileAppender" /> </root> </log4net> </configuration>
Here, we set up two appenders: a console appender, which will output logs to the console, and a file appender, which will write logs to a file.
The <appender>
elements define where the log messages will be sent. In this case, we’re using a ConsoleAppender
to output logs to the console and a RollingFileAppender
to write logs to a file. Within each appender, the <layout>
element specifies how each log message should be formatted. By using PatternLayout
, we can customize the format using a pattern string.
Next, the <conversionPattern>
element is where we define the actual format of our log messages. The value attribute contains placeholders that the system replaces with actual data when it writes a log.
The placeholders we use are:
- %date: The date and time when the log was created
- [%thread]: The name of the thread that generated the log
- %-5level: The log level (DEBUG, INFO, WARN, ERROR, FATAL)
- %logger: The name of the logger
- %message: The log message
- %newline: A new line character
Finally, the <root>
element sets up the main logger. By setting the level to “ALL”, we’re telling it to process all log levels and the <appender-ref>
element tells the root logger to use our ConsoleAppender.
File Appender Configuration in Log4net
For the file appender, we add some additional configuration. First, we set the location and name of the log file with <file value="logs/application.log" />
. Then, we have Log4net append to the existing file rather than overwrite it using <appendToFile value="true" />
. After that, we enable log rotation based on file size with <rollingStyle value="Size" />
.
We also specify that we want to keep up to 5 backup files using <maxSizeRollBackups value="5" />
. The maximum size of each of these log files is set to 10MB with <maximumFileSize value="10MB" />
; this is to prevent the files from taking up too much disk space. Then, we make sure that the filenames are consistent by using <staticLogFileName value="true" />
.
Now we have a good starting point to implement Log4net in our C# app. We’ll see detailed logs, helping us keep track of what’s happening in our app.
Implement Log4net in C#
With the configuration in place, it’s time to implement Log4net in a simple C# project.
Let’s start with our simple Calculator
example:
public static class Calculator { private static readonly ILog log = LogManager.GetLogger(typeof(Calculator)); public static int Add(int a, int b) { log.Debug($"Adding {a} and {b}"); int result = a + b; log.Info($"Result of {a} + {b} = {result}"); return result; } }
First, we initialize a logger for the Calculator
class using Log4net’s LogManager.GetLogger()
method. The typeof(Calculator)
parameter associates the logger with the Calculator
class, which helps us figure out the source of log messages.
In the Add()
method, we use log.Debug()
level to log the values being added.
After we calculate the result, the log.Info()
logs an informational message that indicates the result. We use the info level for significant events in the application’s flow.
Since we’re making a calculator, let’s also add our Subtract
, Multiply
, and Divide
methods:
public static int Subtract(int a, int b) { log.Debug($"Subtracting {b} from {a}"); if (b > a) { log.Warn($"Subtraction may result in a negative number: {a} - {b}"); } int result = a - b; log.Info($"Result of {a} - {b} = {result}"); return result; } public static int Multiply(int a, int b) { log.Debug($"Multiplying {a} and {b}"); int result = a * b; if (result == 0) { log.Warn($"Multiplication resulted in zero: {a} * {b}"); } else if (result < 0) { log.Warn($"Multiplication resulted in a negative number: {a} * {b} = {result}"); } log.Info($"Result of {a} * {b} = {result}"); return result; } public static int Divide(int a, int b) { log.Debug($"Dividing {a} by {b}"); if (b == 0) { log.Error("Division by zero attempted"); throw new DivideByZeroException("Cannot divide by zero"); } int result = a / b; log.Info($"Result of {a} / {b} = {result}"); return result; }
Here, we introduce some new logging levels.
The first is log.Warn()
, which we use when subtracting a larger number from a smaller one. In the Subtract()
method, we use it when b is greater than a, and in the Multiply()
method, if the result is zero or negative.
Then, in the Divide()
method, we utilize another new log level, log.Error()
before throwing an exception when dividing by zero. This lets us log the error before the program potentially crashes or moves to exception handling.
We are ready to move to the Program
class:
var log = LogManager.GetLogger(typeof(Program)); var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config")); log.Info("Application Started"); int addResult = Calculator.Add(5, 3); Console.WriteLine($"Result of 5 + 3 = {addResult}"); int subtractResult = Calculator.Subtract(5, 10); Console.WriteLine($"Result of 5 - 10 = {subtractResult}"); int multiplyResult = Calculator.Multiply(4, 6); Console.WriteLine($"Result of 4 * 6 = {multiplyResult}"); try { int divideResult = Calculator.Divide(10, 2); Console.WriteLine($"Result of 10 / 2 = {divideResult}"); Calculator.Divide(5, 0); } catch (DivideByZeroException ex) { log.Fatal("A critical error occurred during division", ex); Console.WriteLine($"Caught exception: {ex.Message}"); } log.Info("Application Ended");
First, we create a logger for the Program
class using LogManager.GetLogger(typeof(Program))
.
Next, the logger repository is configured with LogManager.GetRepository()
and XmlConfigurator.Configure()
. These lines read the configuration from the log4net.config
file and set up the logging system accordingly.
After that, an informational message log.Info("Application Started")
is logged to show that the application has started.
We have a try-catch block attempting to perform a division by zero so we can intentionally trigger DivideByZeroException
and log a fatal error using log.Fatal()
.
Finally, an informational message is logged to show that the application has ended with log.Info("Application Ended")
.
Log4net in the Console
After running our code, we should see something like this in the console:
2024-08-10 12:28:27,371 [1] INFO Program - Application Started 2024-08-10 12:28:27,376 [1] DEBUG LoggingInCSharp.Calculator - Adding 5 and 3 2024-08-10 12:28:27,377 [1] INFO LoggingInCSharp.Calculator - Result of 5 + 3 = 8 Result of 5 + 3 = 8 2024-08-10 12:28:27,377 [1] DEBUG LoggingInCSharp.Calculator - Subtracting 10 from 5 2024-08-10 12:28:27,377 [1] WARN LoggingInCSharp.Calculator - Subtraction may result in a negative number: 5 - 10 2024-08-10 12:28:27,377 [1] INFO LoggingInCSharp.Calculator - Result of 5 - 10 = -5 Result of 5 - 10 = -5 2024-08-10 12:28:27,377 [1] DEBUG LoggingInCSharp.Calculator - Multiplying 4 and 6 2024-08-10 12:28:27,377 [1] INFO LoggingInCSharp.Calculator - Result of 4 * 6 = 24 Result of 4 * 6 = 24 2024-08-10 12:28:27,378 [1] DEBUG LoggingInCSharp.Calculator - Dividing 10 by 2 2024-08-10 12:28:27,378 [1] INFO LoggingInCSharp.Calculator - Result of 10 / 2 = 5 Result of 10 / 2 = 5 2024-08-10 12:28:27,378 [1] DEBUG LoggingInCSharp.Calculator - Dividing 5 by 0 2024-08-10 12:28:27,378 [1] ERROR LoggingInCSharp.Calculator - Division by zero attempted 2024-08-10 12:28:27,378 [1] FATAL Program - A critical error occurred during division System.DivideByZeroException: Cannot divide by zero at LoggingInCSharp.Calculator.Divide(Int32 a, Int32 b) in C:\Users\user\Documents\GitHub\CodeMazeGuides\dotnet-logging\Log4NetImplementationCSharp\Log4NetImplementationCSharp\Calculator.cs:line 63 at Program.<Main>$(String[] args) in C:\Users\user\Documents\GitHub\CodeMazeGuides\dotnet-logging\Log4NetImplementationCSharp\Log4NetImplementationCSharp\Program.cs:line 26 Caught exception: Cannot divide by zero 2024-08-10 12:28:27,384 [1] INFO Program - Application Ended
Here, we see log messages that show our application’s flow.
The DEBUG messages show the Calculator class preparing to perform operations, while the INFO messages give us information about the results. When an error occurs, we see ERROR and FATAL log messages, along with the exception details.
Also, if we look at our log file it is almost identical to this console output. The only difference is that the file output won’t include the Console.WriteLine
statements.
Conclusion
In this article, we learned how to implement Log4net in C# applications, focusing on setup, configuration, and best practices. Log4net’s flexibility makes it a useful tool for logging, debugging, monitoring, and maintaining our applications.