What Is Logging?
Logging is the process of recording events and messages that occur in a system. These messages are usually stored in a file, but they can be sent to other destinations such as databases or other monitoring software.
Logging is a general purpose, but we mostly use it for:
– Troubleshooting and debugging: Logging provides historical activities of a system, making it easy to find problems such as exceptions in the code. We can analyze the log to see if it happened and try making educated guesses.
– Auditing and security. Log can be used to audit security events, user activities and system changes.
– Performance monitoring: Log can capture performance-related data such as the response time of a system. We can use it to identify bottlenecks and potential performance problems.
Logging Level
Logging Levels are defined differently for each implementation. However, they usually contain these 4 levels:
– Debug: This level contains detailed information for developers to perform troubleshooting. We usually don’t turn on this level in the production environment because it could produce a log of logging data.
– Information: This level logs information of system operations.
– Warning: This level indicates potential issues that might require attention but are not critical issues yet.
– Error: This level indicates that an error occurred in the system.
In .NET Core, the framework defines more log levels to distinguish their intention even further. These log levels include: Trace, Debug, Information, Warning, Error, Critical, None.
In the following sections, we will put them into practice and have a better understanding of log level.
How logging is implemented in .NET Core?
ILogger, ILoggerProvider and ILoggerFactory
Logging in .NET core is built of so many components and classes. However, I will only introduce 3 main components that we usually interact with and use most of the time in the framework.
– ILogger is the interface that represents a type we interact with to perform the logging. In the real world, we don’t usually use this interface but ILogger instead. The ILogger eventually inherits the interface ILogger. This interface exposes a method that we can use to write logs with appropriate log levels such as Log, LogCritical, LogDebug, LogError, LogInformation, LogTrace, LogWarning… You can find more about the interface here.
– ILoggerProvider is used to create ILogger instances. Applications are not supposed to use this interface directly, they are only supposed to use ILoggerFactory to create instances of ILogger.
– ILoggerFactory is also used to create ILogger instances and is supposed to be used in the application code. We will register the ILoggerProvider with ILoggerFactory so that the framework will use it.
How is the Logger bootstrapped internally?
We can set a breakpoint in the Program.cs to see how it bootstrap the logging mechanism.
By inspecting the IServiceCollection, we get some insights of what the framework does under the hood. By default, the framework registers some ServiceDescriptor for all the interfaces that we mentioned in the previous section.
Figure 01: Code change to inspect IServiceCollection
Further analysis of the IServiceCollection at runtime reveals that the framework bootstraps an ILogger, ILoggerFactory with Singleton lifetime. We have 4 ILoggerProvider registered by default (ConsoleLoggerProvider, DebugLoggerProvider, EventSourceLoggerProvider, EventLogLoggerProvider). Calling services.Logging.ClearProviders() would clear all the ServiceDescriptor from the IServiceCollection.
Figure 02: Inspect IServiceCollection at runtime
Logging Scope
The interface ILogger comes with a very handy method BeginScope that could be used to log a portion of code that has the same attribute (state). This method doesn’t show any distinct output when we use the default console logger. However, it comes very handy when we use Serilog and Seq.
Figure 03: Logging Scope example
Log Filtering
The framework also allows us to configure log filtering by specifying the desired log level for a specific log category via the AddFilter method. If no log filter is specified, then the minimum log level is applied.
Figure 04: Log filter
What is Serilog and Structured Logging
What is Serilog?
Serilog is a logging library for .NET and .NET Core that supports structured logging. It has the ability to store (sink) logs using a plain text file, Database, or even Seq (another software that supports the GUI for log querying and visualization)…
How does Serilog work?
We won’t dig deep into how Serilog works internally. However, we will take a glance at the highest level, by using the same technique that we used to observe the way the framework bootstrap logging. We can see that Serilog simply just replaces the ILoggerFactory by adding another ServiceDescriptor for ILoggerFactory.
Figure 05: Serilog replaces ILoggerFactory using its implementation
Serilog Integration Guide.
In this guide, we will integrate the .NET 8 with Serilog and we will try to sink the log to the file and Console. We will use the template which contains all the code inside the Program.cs (without the Startup.cs file) file so that it would be easier to keep track of the post. First, we will bootstrap our project by using the following command in Windows Terminal:
dotnet new webapi -o SerilogIntegrationGuide
The SerilogIntegrationGuide is not mandatory, It’s the name of your project and you can name it whatever you want as long as it respects the naming convention of your operating system.
After the project is fully created, then we can use the following command to install Serilog:
dotnet add package Serilog.AspNetCore
Before we change our code to integrate with Serilog, let’s capture the output of our current program by hitting an endpoint of our web api.
Figure 06: Plain output of the default console logging
The output is plain text without any format and is very hard to read. Now let’s add the Serilog package and change some code before we observe the output again.
Figure 07: Serilog integration code change
Serilog integration is very easy, by creating the LoggerConfiguration and adding the line builder.Services.AddSerilog(), we have completed the integration with the logging library. Now let’s run the project, hit the endpoint and observe the output again.
Figure 08: Serilog console output
The output is nicely formatted.
Now let’s try sinking (save) the log into the file instead. To sink the log to the file, we have to install another Nuget package by running the following command in the terminal
dotnet add package Serilog.Sinks.File –version 5.0.0
After running the command, we have to make some changes to the code to make the logs dump into a file.
Figure 09: File rolling integration code change
Then, we can run the project again and observe that the log file is created inside our project folder. The second parameter “rollingInterval” indicates that a new log file will be created after each hour so the log file is guaranteed to be small and contains only the logs with the scope of 1 hour.
Conclusion
In conclusion, logging provides a vital mechanism for understanding and maintaining your C# applications. By leveraging Serilog, you gain a powerful and flexible tool to capture, enrich, and store log data. This approach empowers you to troubleshoot issues efficiently, monitor application health, and gain valuable insights into system behavior. As your application evolves, Serilog’s scalability and extensibility ensure it can continue to be a cornerstone of your observability strategy.
In this article, we explored the vital role of logging and structured logging with Serilog within .NET applications. For further assistance or to discuss your project needs, feel free to reach out to us at Saigon Technology. Our team is well-equipped to provide you with the latest .NET technologies and support. Contact us today to bring your vision to life.
Resources
Demo source code: https://github.com/saigontechnology/Blog-Resources/tree/main/DotNet/do.tran/LoggingInDotNet