This content originally appeared on Telerik Blogs and was authored by Assis Zang
In this article we will cover important topics regarding logs—how to implement them effectively, best practices and ensuring relevance for future data analysis.
Something indispensable in modern applications and that needs special attention is logs. This is especially true when it comes to web development with ASP.NET Core, where the possibilities for integration between microservices and APIs are practically endless, and monitoring these connections can become a great challenge.
Analyzing some code, I noticed that the logs were always present, but when looking in detail, I saw that these logs were often irrelevant, as they carried little or no important information. Perhaps due to lack of time, distraction or lack of knowledge, we let details that can make a lot of difference escape, especially when we have a problem in the production environment.
In this article, we will cover some important topics regarding logs and how to implement them effectively, using best practices and ensuring they are relevant for future data analysis.
1. Log Levels and .NET Logging
LogLevel in the .NET context is a C# Enum that defines logging severity levels. It provides extension methods to indicate log levels and is available in the Assembly Microsoft.Extensions.Logging.Abstractions.
Below is the table of log levels in .NET Core.
Log Level | Severity | Extension Method | Description |
---|---|---|---|
Trace | 0 | LogTrace() | Logs that contain the most detailed messages. These messages may contain sensitive application data. These messages are disabled by default and should never be enabled in a production environment. |
Debug | 1 | LogDebug() | Logs that are used for interactive investigation during development. These logs should primarily contain information useful for debugging and have no long-term value. |
Information | 2 | LogInformation() | Logs that track the general flow of the application. These logs should have long-term value. |
Warning | 3 | LogWarning() | Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application execution to stop. |
Error | 4 | LogError() | Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a failure in the current activity, not an application-wide failure. |
Critical | 5 | LogCritical() | Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires immediate attention. |
None | 6 | None | Not used for writing log messages. Specifies that a logging category should not write any messages. |
These are the log levels, which should be used according to the context where they fit best. Below are some practical examples for each level.
Usage Examples for Each Level
public class LogService
{
private readonly ILogger<LogService> _logger;
public LogService(ILogger<LogService> logger)
{
_logger = logger;
}
public void ProccessLog()
{
var user = new User("John Smith", "smith@mail.com", "Kulas Light", null, null);
//Trace
_logger.LogTrace("Processing request from the user: {Name} - {ProccessLog}", user.Name, nameof(ProccessLog));
//Debug
var zipcodeDefault = "92998-3874";
if (user.Zipcode == zipcodeDefault)
_logger.LogDebug("The zip code is default for the user: {Name} - {ProccessLog}", user.Name, nameof(ProccessLog));
//Information
_logger.LogInformation("Starting execution... - {ProccessLog}", nameof(ProccessLog));
//Warning
if (string.IsNullOrEmpty(user.Zipcode))
_logger.LogWarning("The zip code is null or empty for the user: {Name} - {ProccessLog}", user.Name, nameof(ProccessLog));
//Error
try
{
var zipcodeBase = "92998-3874";
var result = false;
if (user.Zipcode == zipcodeBase)
result = true;
}
catch (Exception ex)
{
_logger.LogError(ex.Message, "Error while processing request from the user: {Name} the zipcode is null or empty. - {ProccessLog}", user.Name, nameof(ProccessLog));
}
//Critical
try
{
var userPhone = user.Phone;
}
catch (Exception ex)
{
_logger.LogCritical(ex.Message, "The phone number is null or empty for the user: {Name}. Please contact immediately the support team! - {ProccessLog}", user.Name, nameof(ProccessLog));
throw;
}
//None
//Not used for writing log messages
}
}
2. Logging Frameworks or Libraries
The functions available in Microsoft Logging Assembly fulfill the basic needs for logging and can be used on any system large or small. However, some scenarios require more detailed logs and more customization options. For these cases, there are libraries that can help and that are well known and accepted in the development community.
Below we will see Serilog, which is one of the more well-known libraries, and some usage examples.
Serilog
Serilog is currently the most downloaded log library on the NuGet website. You can find it here: Serilog NuGet.
Like other libraries, Serilog provides diagnostic logging for files, consoles and even more places. Its configuration is easy and it has a wide variety of functions for modern applications.
A practical example of its use will be presented below, with some of the various customization options available in Serilog.
Practical Example
- Create a new console app with .NET 6.
You can do this through Visual Studio 2022 or via the console with the following command:
dotnet new console --framework net6.0
- Install the following libraries in the latest stable version:
- Serilog
- Serilog.Expressions
- Serilog.Formatting.Compact
- Serilog.Sinks.Console
- Serilog.Sinks.File
- Create a class called Offer and paste this code in it:
public record Offer(int Id, int ProductId, string Description, decimal Value, decimal Quantity);
- Replace the Program class code with the code below:
using Serilog;
using Serilog.Templates;
ExecuteLogs();
void ExecuteLogs()
{
LogToConsole();
LogToFile();
}
void LogToConsole()
{
var offer = FillOffer();
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("offerId", offer.Id)
.Enrich.WithProperty("productId", offer.ProductId)
.Enrich.WithProperty("quantity", offer.Quantity)
.WriteTo.Console(new ExpressionTemplate("{ {@t, @mt, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n"))
.CreateLogger();
Log.Information("Information about the Offer");
}
void LogToFile()
{
var offer = FillOffer();
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("offerId", offer.Id)
.Enrich.WithProperty("productId", offer.ProductId)
.Enrich.WithProperty("quantity", offer.Quantity)
.WriteTo.File(new ExpressionTemplate(
"{ {@t, @mt, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n"),
"Logs\\log.txt",
rollingInterval: RollingInterval.Day)
.CreateLogger();
Log.Information("Information about the Offer");
}
Offer FillOffer() =>
new Offer(5488, 100808, "Book", 109, 3);
- Execute the app.
If you followed the steps above, you will see the following result in the application console:
{"@t":"2021-11-19T18:53:57.9627579-03:00","@mt":"Information about the Offer","offerId":5488,"productId":100808,"quantity": 3}
And in the folder: “\bin\Debug\net6.0\Logs” is the created file. Inside it will be the same data that was displayed in the console.
In this example we created two methods:
-
“LogToConsole()” – It creates an object called “Offer,” then uses a new instance of “LoggerConfiguration” and adds the properties values to logging with the method “Enrich.WithProperty.” Then the method “WriteTo.Console” displays the data logging in the console, and makes the configuration of the template through the class “ExpressionTemplate.”
-
“LogToFile()” – It does the same as the previous method, but uses the “WriteTo.File” method to create a “Logs” folder if it doesn’t exist, and inside it a text file that will store the log data. The “rollingInterval” rule determines the interval in which a new file will be created—in this example, one day—that is, the logs will be written to the same file until the day ends, and then a new file will be created.
This was a simple demonstration of using Serilog, but this library has many valuable features. Feel free to explore them.
3. Best Practices and Recommendations
Structured Logs
The creation of structured logs is recommended when the application uses the Microsoft Logging Assembly and also if it uses some advanced filtering to search for logs. It is necessary because the log recording mechanism needs to receive the string with the placeholders and their values separately.
Below is an example of a structured log:
_logger.LogWarning( "The zip code is null or empty for the user: {Name}", user.Name;
You can use string interpolation—however, you should make sure that the registration service is prepared to have access to the message template and property values even with the replacement.
Below is an example of a structured log with string interpolation:
_logger.LogWarning( $"The zip code is null or empty for the user: {user.Name}";
Enable Only Appropriate Logs in Production
Before publishing something in the production environment, you must consider the real needs of using each of the log levels. For example, logs that contain too much information can overload the server or slow down the running of the system.
Therefore, before publishing anything, a good practice is to analyze all the logs and leave only those that are relevant and will not bring any kind of overhead. Logs of type Trace and Debug must be disabled in a production environment.
Using a Third-Party Logging Library
If you’re starting a new project, always find out if the client you’re working with uses a third-party library and what it is, as you may be wasting your time in something that may not be able to go into production due to the use of private property.
Logging Sensitive Information
Never put sensitive or private information in production logs—for example, user-related data such as passwords, credit card numbers, or any information about something that cannot be made public. In addition to being visible to anyone with access to logged data, this information usually does not have any encryption and so is exposed if the system suffers some kind of hacker attack.
Writing Relevant Log Messages
Having a log message at an important point in the code doesn’t mean that the system is prepared for a detailed analysis, because if the message doesn’t make much sense for that context, its use will be unnecessary.
So when writing log messages, think about what would be really important information for future analysis of the execution. For example, in messages inside exception blocks, always add the message generated in the “Catch” method.
Another recommendation that can be seen in the following example is to use the name of the method executed when writing the log. In the case below, we are using in the log the name of the method responsible for the execution through the command “nameof(ProccessLog)&rdquo.;
public void ProccessLog()
{
try
{
//execution...
}
catch (Exception ex)
{
//The exception message "ex.Message" is being used in the log
_logger.LogError(ex.Message, "Error while processing request from the user: {Name} the zipcode is null or empty. - {ProccessLog}", user.Name, nameof(ProccessLog));
}
}
Conclusion
In this article, we covered some good practice tips for writing efficient logs in C# and .NET. Something to consider is that there are no absolute rules for writing logs—it will all depend on the context in which you are developing, but if you follow these tips surely your code will improve a lot.
This content originally appeared on Telerik Blogs and was authored by Assis Zang
Assis Zang | Sciencx (2022-01-11T12:40:00+00:00) Tips for More Efficient .NET Logs. Retrieved from https://www.scien.cx/2022/01/11/tips-for-more-efficient-net-logs/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.