Because logging in ASP.net core is so simple to use, it’s also simple to overlook key crucial diagnostics capabilities or to misuse these potent logging tools. I’ll walk you through the fundamentals of ASP.net core Structured Logging both with and without Serilog in this tutorial. You can easily understand the fundamental ideas with the help of this guide. Let’s start by exploring these 4 Structured Logging Techniques in ASP.NET Core.
Basic Logging ASP.NET Core
As I mentioned before, the default logging framework that comes with Asp.net core is already set up when you start a web application builder. You don’t need to do anything because the default configuration adds console, debug, event source, and event log as described in the documentation.
Use of a logging provider (Serilog as an example)
Let’s discuss about providers before getting into the meat of this piece. Since we’ll discuss both serilog and the standard ASP.net core logging system. Other logging providers that will improve your logging can be used with ASP.net core. You should make use of the robust features offered by these companies. We will utilize serilog as our supplier in this article.
Adding Serilog to your project
Serilog configuration is simple. Add the “Serilog.AspNetCore” nugget package to your project. Serilog must be configured after the nugget package has been included. Use this code fragment in your program.cs (Dotnet 6+) to add console logging, make serilog the default provider, and load configurations from our appsettings file.
Log.Logger = new LoggerConfiguration() .WriteTo.Console() .ReadFrom.Configuration(builder.Configuration) .CreateLogger(); builder.Host.UseSerilog();
"Serilog": { "MinimumLevel": "Debug", }
"Serilog": { "MinimumLevel": { "Override": { "Default": "Debug", "Microsoft.AspNetCore": "Information" } } }
1- Use scopes to delimit your logs properly.
A logging scope is defined. I’ll use Microsoft’s explanation. “A series of logical operations can be grouped into a scope. The same data can be attached to each log that is produced as part of a set using this grouping.
Here, we’ll add scopes that cover whole HTTP requests as well as a user-id or client-id scope, allowing us to identify which user or client of our API is executing which action at any given time. This eliminates the need to repeatedly type the user id or client id in each log for the appropriate logs and clearly delimits the logs, even in the inner level logs of the framework. It also results in cleaner code. Here’s how we go about it:
NB: Scopes are used in using statements because they need the release of some resources when they are finished.
– We must first intercept http requests and determine whether they are real. If so, the scope is defined using either the user’s ID or the API client’s ID. If not, we just carry on with the request flow. We’ll need a middleware to accomplish this.
public class CurrentCallerScopeLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<CurrentCallerScopeLoggingMiddleware> _logger; public CurrentCallerScopeLoggingMiddleware(RequestDelegate next, ILogger<CurrentCallerScopeLoggingMiddleware> logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { if (context.User.Identity is { IsAuthenticated: true }) { //TODO: It might be a good idea to add both user id and client id at the same time. //If the caller is authenticated and he is a client like an API client this value will not be null. var clientId = context.User?.Claims?.FirstOrDefault(claim => claim.Type == "client_id")?.Value; if (context.User?.HasClaim(c => c.Type == ClaimTypes.NameIdentifier) ?? false) { var userId = context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; //Let the current user be taken as the request scope. using (_logger.BeginScope("User Id: {UserId}", userId)) { await _next(context); } } else if (!string.IsNullOrEmpty(clientId)) { //Let the current user be taken as the request scope. using (_logger.BeginScope("Client Id: {ClientId}", clientId)) { await _next(context); } } } else { await _next(context); } } }
– Our middleware must be included to the project. Note that if it is introduced before the middleware for authentication, authentication will not take place until it is called. And that’s not what we want.
app.UseAuthentication(); app.UseMiddleware<CurrentCallerScopeLoggingMiddleware>(); app.UseAuthorization();
Your logs will be properly defined after doing this, which will make diagnosis easier.
2. Make use of structured logging features: Logging in JSON Format
The second ASP.NET core structured logging mechanism is presented here. Let’s define structured logging first. This is the process of logging application events in a well-organized and consistent manner, facilitating reading, searching, and interpreting the logs. Compared to simply logging strings in an output, this is superior. In light of this, structuring our logs is the most effective technique to log effectively. What could be more ideal than JSON?
Configuring JSON format for your logs in ASP.net core
It is simple to configure the output format of your log entries with ASP.net core. You may accomplish this by using the “FormatterName” and “FormatterOptions” parameters found in the appsettings.json file. Here’s an illustration:
"Console": { "FormatterName": "json", "FormatterOptions": { "SingleLine": true, "IncludeScopes": true, "UseUtcTimestamp": true, "TimestampFormat": "HH:mm:ss", "JsonWriterOptions": { "Indented": true } } }
In the aforementioned example, I define that each console log shall be indented, contain scopes, and be in json format. Your logs will then show up in the console with the json format when you provide this.
Setting up Serilog’s JSON format for your logs
Naturally, serilog enables you to give your logs a specified structure. We’ll create a json format for our console logs in our example. Keep in mind the code above that configures Serilog? Simply make a small modification to it:
.WriteTo.Console(formatter: new JsonFormatter())
The output template can then be modified to either log solely json logs or to include more information in the log output. Here’s how to add the JSON message to logs, followed by a newline and any potential exceptions:
"Serilog": { "MinimumLevel": ... ... "WriteTo": [ { "Name": "Console", "Args": { "theme": ..., "outputTemplate": "{Message:lj}{NewLine}{Exception}" } } }
3. How to appropriately use logging parameters and destructure them
It is crucial to pass parameters when logging, and doing so is extremely simple. The parameter name only has to be enclosed in curly brackets and added to the logging procedure. You don’t need to preface your strings with a “$” sign. The mechanism for logging already takes care of that. The code below, for instance, logs the user’s age.
_logger.LogInformation("User's age: {UserAge}", user.Age);
If structured logging with Json was enabled when this option was supplied, the output would be clearly documented, making it simple to query the logs using the user’s age parameter I passed. The possible output is shown in part below:
_logger.LogInformation("Silly info: {@Info}", new Info { AccountId = 32, Description = "kfdoa" });
The aforementioned code will generate a log entry in json format with the Info parameter. When structured logging is turned on, it will appear as follows:
"MessageTemplate":"Silly info: {@Info}","Properties":{"Info":{"_typeTag":"Info","AccountId":32,"Description":"kfdoa","Id":0,"Label":".....
Now, if I use the $ sign for destructuring, as shown below:
_logger.LogInformation("Silly info: {$Info}", new Info
{
AccountId = 32,
Description = "kfdoa"
});
I get the following output:
"MessageTemplate":"Silly info: {$Info}","Properties":{"Info":"DemoApp.Api.Info","SourceContext":"…
With the help of this free, serilog-compatible open-source software, https://github.com/destructurama/attributed, you can customize how your parameters are destructured completely. It enables you to cover confidential data with “***” in the event that it is a password, for example, or to hide sensitive data when destroying objects.
You can add custom configurations to your appsettings.json file to configure destructuring. This is an illustration of the configurations I included. I’ve included links at the conclusion of this article that you may use to learn more about this feature.
"Serilog": { "Using": .., "MinimumLevel": ... "Enrich": ..., "WriteTo": ..., "Destructure": [ { "Name": "ToMaximumDepth", "Args": { "maximumDestructuringDepth": 4 } }, { "Name": "ToMaximumStringLength", "Args": { "maximumStringLength": 100 } }, { "Name": "ToMaximumCollectionCount", "Args": { "maximumCollectionCount": 10 } } ], "Properties": { "Application": "YouScribe.Affiliates.Api" } },
4. Remember to choose the appropriate log level and to define the logging category.
public class DemoController : BaseController { public DemoController(IMediator mediator, ILogger<DemoController> logger) { } }
_logger.LogInformation("Silly info logged");
The output will contain the category and will be:
MyApi.DemoController: Information: Silly info logged
This is quite beneficial while conducting diagnostics. When an error happens, you want to know where your logs are coming from.
It is simple to specify the log level. The documentation does an excellent job of outlining how to do that here. However, what log level should you pick? The various log levels and their definitions are listed below.
- Trace: The smallest log level, used to record the most information possible. It is jam-packed with knowledge about how the system works. Write logs at this level if you wish to include a lot of specific details about how your product works internally. Additionally, due to the excessive amount of information it contains, this log level in the configurations should NEVER be activated by default in production. When attempting to resolve intricate problems that have arisen in your software, you should turn it on.
- Debug: The following level is the bottommost one. It is used to log debug information and also contains a lot of information, as the name suggests. This ought to only be used in test settings. To determine when something went wrong, you might activate it during manufacturing, but be sure to rewind to a higher level.
- Information: This is used to transmit data about how the software is utilized.
- Warning: This is utilized when your software exhibits unexpected behavior but not in a way that your system will fail. For instance, when the user’s search results are not returned.
- Error: When a system failure occurs, use this log level. failures that are unique to a particular user or usage circumstance. These errors shouldn’t prevent your machine from functioning properly.
- Critical: The last level is this one. When something goes wrong that prohibits the correct operation of your entire system, such as when you can’t access your database or when you run out of memory, it is utilized.
Conclusion
What do you think of these 4 ASP.NET Core structured logging techniques? These are, in my opinion, the key ideas you need to understand before adding logging functionality to your ASP.NET Core application. With these, you can log expertly and cut down on your time spent on troubleshooting and diagnostics. I hope this is useful to you as you pursue software development.
Get your ASP.NET hosting as low as $1.00/month with ASPHostPortal. Our fully featured hosting already includes
- Easy setup
- 24/7/365 technical support
- Top level speed and security
- Super cache server performance to increase your website speed
- Top 9 data centers across the world that you can choose.
Andriy Kravets is writer and experience .NET developer and like .NET for regular development. He likes to build cross-platform libraries/software with .NET.