Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Systems Programming with C# and .NET

You're reading from   Systems Programming with C# and .NET Building robust system solutions with C# 12 and .NET 8

Arrow left icon
Product type Paperback
Published in Jul 2024
Publisher Packt
ISBN-13 9781835082683
Length 474 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Dennis Vroegop Dennis Vroegop
Author Profile Icon Dennis Vroegop
Dennis Vroegop
Arrow right icon
View More author details
Toc

Table of Contents (18) Chapters Close

Preface 1. Overview of Systems Programming FREE CHAPTER 2. Chapter 1: The One with the Low-Level Secrets 3. Chapter 2: The One Where Speed Matters 4. Chapter 3: The One with the Memory Games 5. Chapter 4: The One with the Thread Tangles 6. Chapter 5: The One with the Filesystem Chronicles 7. Chapter 6: The One Where Processes Whisper 8. Chapter 7: The One with the Operating System Tango 9. Chapter 8: The One with the Network Navigation 10. Chapter 9: The One with the Hardware Handshakes 11. Chapter 10: The One with the Systems Check-Ups 12. Chapter 11: The One with the Debugging Dances 13. Chapter 12: The One with the Security Safeguards 14. Chapter 13: The One with the Deployment Dramas 15. Chapter 14: The One with the Linux Leaps 16. Index 17. Other Books You May Enjoy

Available logging frameworks

Logging has been around forever. In the early days of computing, operators would walk around the machines and note whatever they saw happening to them. If a light blinked when it should not have blinked or vice versa, they wrote it down a journal somewhere. Later, systems would log everything they could onto paper and punch cards. If systems did something unexpected, the operators could go to the paper trail and figure out what had caused the event. After that, people used serial monitors that logged everything onto a separate device.

These days, we hardly use punch cards anymore. However, we still log. There are many frameworks out there that help you get the job done. In this chapter, I will explain three of those frameworks. They all have pros and cons. I will highlight these as much as possible. That way, you can make your own decisions about what to use and when to use it.

Default logger in .NET

Microsoft offers a default logger. We have seen it before: if you create an ASP.Net application or, as we have done, a worker process, you will get a logger framework for free. This framework is surprisingly full-featured. This framework offers enough features to satisfy the needs of most developers. So, let’s have a look at it!

As I said, many of the templates in Visual Studio already include the standard Logger class. Some templates, however, do not have this. So, let’s have a look at how to add it. We’ll begin with a clean, empty Console application.

The first thing we need to do is add the correct NuGet package. In this case, you need to install Microsoft.Extensions.Logging in your project. Once you have done that, you will have access to the logging framework.

In your main project, you can set up the logging like this:

using Microsoft.Extensions.Logging;
ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
    builder.SetMinimumLevel(LogLevel.Information);
});
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("This is some information");

This code works. If you run it, you will not get any errors. However, you will also not get any output, so that is pretty useless, to be honest.

This is because the framework is quite flexible. It can handle all sorts of outputs to various destinations. However, you have to specify what you want.

Let’s fix this. Install another NuGet package; this time, we need the Microsoft.Extensions.Logging.Console package. Once you have installed that, we need to change the code in the LoggerFactory.Create() method to look like this:

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddConsole();
    builder.SetMinimumLevel(LogLevel.Information);
});

In the second line, we added the Console as a way to output.

If you run the program this time, you will get the desired information:

Figure 10.1: Output from the log

Figure 10.1: Output from the log

OK. We got something on our screen. Let’s see what we have done so far since I deliberately skipped over some steps.

LoggerFactory is a factory class that can create instances of a class that implements Ilogger<T>. We set up LoggerFactory by hooking up the desired outputs (in our case, Console; we’ll add others later). We also gave it the minimum log level we wanted.

Let’s dive into this. I want to discuss log levels and configuration, as well as the different tools we have.

Log levels

Not all messages are equally important. If you are starting out on your project, you will probably want to log a lot. You can output anything you want and you will probably do so. You can write the contents of variables, loop controls, where you are in the flow, and so on. Anything that can help you understand the flow of your program as you run it is a candidate for logging.

However, once you have written and tested your software, you will probably not want all of that information anymore. You might want to log exceptional cases and errors, but that is about it.

To achieve that, you must remove all the logging code that you do not need anymore. Alternatively, you could wrap up the code in #IF / #ENDIF statements and thus effectively remove the calls when you recompile using a different #DEFINE. However, that means changing your code. That could lead to side effects. If you later find a bug and decide that you need that code in again, you will need to rewrite or recompile the system.

Loglevels eliminates that problem.

Each log message we write has a level. In the preceding example, we used Log.LogInformation(). That means that we want to write something informational. There are other levels we can use as well. What you use them for is entirely up to you. However, in general, there is meaning to each level. These are the levels we can use with ILogger:

Log level

Description

Trace

This refers to the most detailed messages. These messages may contain sensitive application data and are therefore not recommended to be enabled in a production environment unless they are necessary for troubleshooting and only for short periods.

Debug

This displays messages that are useful for debugging. It is less verbose than Trace, but more than Information.

Information

This allows the system to show informational messages that highlight the general flow of the application. It is useful for general application insights.

Warning

This is all about messages that highlight an abnormal or unexpected event in the application flow, but which do not otherwise cause the application execution to stop.

Error

These are messages 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

This is about messages describing an unrecoverable application, system crash, or catastrophic failure requiring immediate attention.

None

This results in no messages being logged. This level is used to turn off logging.

Table 10.1 Log levels in Microsoft Logger

There are two ways in which you can specify what level your message has to be. You can use one of the dedicated log methods (such as LogInformation, LogTrace, LogDebug, and so on) or the generic Log() method. That looks like this:

logger.Log(LogLevel.Critical, "This is a critical message");

You just call Log() and then give it the LogLevel. Whatever method you choose, you can decide what level the log is supposed to be on.

However, that only solves a part of the issue. We want to be flexible in what we output to the screen. That’s where the SetMinimumLevel() method on the ILoggingBuilder comes into play.

The method determines what the log is writing to the chosen output channels. If you set it to Information, all calls to the log are processed if they are of the Information level or higher. In other words, all calls to Log.LogTrace(), Log.Debug(), Log.Log(LogLevel.Trace), and Log.Log(LogLevel.Debug) are ignored. So you can, in one line, determine what you do and do not want to appear on the logs. You specify the level and all information on that level or above is outputted. The rest is ignored.

During development, you might want to set the level to Trace. After extensive testing, you might want to set it to Critical or maybe Error during production.

Using a Settings file

Of course, we are not there yet. If you want to change the log level, you still need to change the code and recompile the system. Let’s change that so we can use something else.

Add a new file to your program called appsettings.json. Make sure you change the Copy to output directory property to Copy if newer; you need this file next to the binaries.

The file should look like this:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

Now, we need to add a couple of NuGet packages. Install Microsoft.Extensions.Configuration.JSon and Microsoft.Extensions.Logging.Configuration.

When we have done that, we will add the following code that actually reads the configuration:

var configurationBuilder = new ConfigurationBuilder()
    .AddJsonFile(
        path: "appsettings.json",
        optional:true,
        reloadOnChange:true);
var configuration = configurationBuilder.Build();
var configurationSection=
    configuration.GetSection("Logging");

This code creates a ConfigurationBuilder and then adds the JSON file we just added. We set the optional parameter to true; if people decide to remove the file, our app will still work. We also specify that the reloadOnChange parameter is true. As you have probably guessed, the configuration is reloaded when the file changes.

The following is relatively straightforward: we call Build() to get the IConfiguration, then call GetSection (Logging) to load that specific part of our JSON file.

We need to do some work on our LoggerFactory as well. Change it to look like this:

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddConsole();
    builder.AddConfiguration(configurationSection);
});

Instead of hardcoding the log level, we will now give it the configuration section from the JSON file.

Lastly, let’s change the code that does the actual logging a bit. I will wrap it in a continuous loop:

while (true)
{
    logger.LogTrace("This is a trace");
    logger.LogDebug("This is debug");
    logger.LogInformation("This is information");
    logger.LogWarning("This is warning");
    logger.LogError("This is an error");
    logger.LogCritical("This is a critical message");
    await Task.Delay(1000);
}

Run your program and see all the different ways of displaying your message. Open another terminal window, navigate to the compiled application folder, and change the log setting in the appsettings.json file. As soon as you save the file, you will see a different behavior in the application. Depending on your desire, it will display more or fewer lines of logging.

Now, you can add all the logging you want to your application, use Trace during debugging and development, and then move to Critical or Error if your system is ready for production. You can quickly return to a more detailed debugging level as soon as something happens. All of that is done without recompiling!

Using EventId

Having different debugging levels is nice, but that is not enough to structure the information if you have a lot of messages. To help you create a bit of order in the logging chaos, you can use the EventId.

All log methods have an overload that allows you to add an EventId. An EventId is a class that contains an ID in the integer form and a name in the string form. What those are is entirely left up to you. The name is not even used in the logs, but it is there for your convenience during development. We can create an EventId, or multiple, as follows:

var initEventId = new EventId(1, "Initialization");
var shutdownEventId = new EventId(2, "Shutdown");
var fileReadingEventId = new EventId(3, "File Reading");

I just made up a bunch of categories: Initialization, Shutdown, and File Reading. This is just an example; I am sure that you can come up with much better names.

When you log something, you can use an EventId to indicate that the message you log has to do with a certain part of the system. That looks like this:

logger.LogInformation(initializationEventId, "Application started");
logger.LogInformation(shutdownEventId, "Application shutting down");
logger.LogError(fileReadingEventId, "File not found");

The output now looks a bit different:

FIgure 10.2: Output of logging with an EventId (or multiple)

FIgure 10.2: Output of logging with an EventId (or multiple)

Next to the Log type and the Program, you can see the number between brackets. That is the number of the EventId type. In our case, 1 was initialization, 2 was shut down, and 3 was file reading. Again, these strings are never used and, unfortunately, are not shown on the screen. However, having these numbers in there can help you find the areas that you are interested in.

Using Type information

There is one last thing you can use to organize your logs. I didn’t explain it earlier, but you must have noticed that when we created the instance of our logger, we gave it a Program type parameter:

var logger = loggerFactory.CreateLogger<Program>();

Since we called CreateLogger with the Program type, we see the Program string on the screen in the logs.

You can create several instances of the ILogger interface, each with its own type attached to it. That way, you can create different loggers for each application part. If you have a part of your system that handles printing and the main class is called Printer, you can create a logger of the Printer type like this:

var printLogger = loggerFactory.CreateLogger<Printer>();

All logs written to the printLogger instance will now show Printer in their log lines instead of Program. Of course, it doesn’t really matter what you pass in that parameter. You can use the Printer logger in your main program if you want to. It is just decoration that helps you organize the output of the logs. That’s it. There is no logic behind it.

Using categories wisely

I suggest you use these categories, but use them sparingly; too many will only clutter your logs. I usually create empty classes just for use in the logger creation. That way, I can get a nice set of logger instances without relying on internal code that nobody outside should see. However, I will leave that entirely up to you.

Now that we have basic logging out of the way, it is time to look at some popular alternatives that offer some other nifty tricks we can use. Let us begin with NLog!

NLog

Microsoft is not the only company that offers a logging framework. There are others out there, each with their own strengths and weaknesses. One of the more popular ones out there is NLog.

NLog was created by Jared Kowalski in 2006 as an alternative to the popular log4net solution, which is a port of the immensely popular log4j Java logging solution. Kowalski aimed to build a logging solution that was high in performance but also flexible in the configuration of the settings.

Setting up NLog

To use NLog, you need to install the corresponding NuGet package. The name of the package is simply NLog.

Once you have installed that package, we must create a configuration file. To do that, add a new XML file to your project (do not forget to set the properties to Copy if newer so that the project can find the file when it runs). By convention, this file is called NLog.config, but you can choose any name. The file should look like this:

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns=http://www.nlog-project.org/schemas/NLog.xsd
      xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
      >
  <targets>
    <target
      name="logfile"
      xsi:type="File"
      fileName="${basedir}/logs/logfile.txt"
      layout="${date:format=HH\:mm\:ss} ${logger} ${uppercase:${level}} ${message}" />
    <target
      name="logconsole"
      xsi:type="Console" />
  </targets>
  <rules>
    <logger name="*"
            minlevel="Info"
            writeTo="logfile,logconsole" />
  </rules>
</nlog>

You can control almost all of NLog through this configuration file. You can set up all parameters in code, but that kind of defeats the purpose of NLog. I suggest that you use the configuration file and avoid setting things in the code. That is unless you have a really good reason to do otherwise, of course. After all, it is still your code, not mine.

Now, it is time to start logging. In your program, add the following code:

using NLog;
LogManager.Configuration =
    new XmlLoggingConfiguration(
        "NLog.config"
    );
try
{
    Logger logger = LogManager.GetCurrentClassLogger();
    logger.Trace("This is a trace message");
    logger.Debug("This is a debug message");
    logger.Info("Application started");
    logger.Warn("This is a warning message");
    logger.Error("This is an error message");
    logger.Fatal("This is a fatal error message");
}finally{
    LogManager.Shutdown();
}

First, we will load the configuration in the LogManager. You usually have one setup for all your logging needs in your entire application, so you might as well do this first.

Then, we will call GetCurrentClassLogger(). This call is the equivalent of the call to CreateLogger<T> in the Microsoft framework. It ties the current class name to the logger so you can categorize your logs.

If you want other loggers to be associated with different classes, you can do so by calling something like this:

var otherLogger  = LogManager.GetLogger("OtherLogger");

This call creates another logger with the same configuration but will show "OtherLogger" in the output this time.

The rest of the code is self-explanatory, except for the line that says LogManager.Shutdown(). This line is needed to flush out all logs in the code and ensure that no message is left behind.

Log levels in NLog logging

As with the Microsoft framework, you can specify which level you want to see in the log files. The levels for NLog are comparable, but there are minor differences. The following table shows the available options:

NLog level

Description

Trace

This provides the most detailed information. Use this for the most low-level debug information.

Debug

This provides coarse-grained debugging information. It is less detailed than Trace.

Info

Informational messages that highlight the general flow of the application come with this level.

Warn

Potentially harmful situations of interest to end users or system managers that indicate potential problems are flagged at this level.

Error

Error events of considerable importance that will prevent normal program execution but might still allow the application to continue running are flagged here.

Fatal

This level focuses on very severe error events that will presumably lead the application to abort.

Off

This involves no logging at all.

Table 10.2: Log levels in NLog

As you can see, the levels are almost the same; they are just named differently. That makes it harder to remember when you switch from one framework to another, but we can do nothing about that. We have to memorize the terms, I guess.

NLog targets

You control NLog through the configuration file. That is one of the two main principles that drove the development of NLog (the other being that NLog should be highly performant).

In the sample we have worked on, we wrote the logs in both the Console and a file. In the settings file, we have defined different targets where NLog writes the logs. Currently, more than 100 different targets are available, some of which are part of the core package and some of which require a NuGet package.

Let’s have a look at another target. We currently use Console, but we can replace that with ColoredConsole. That is part of the default package, so we do not have to add a NuGet package.

In the configuration, add a new target to the targets section. It looks like this:

<target
  name="logcolorconsole"
  xsi:type="ColoredConsole"
  header="Logfile for run ${longdate)"
  footer="-----------"
  layout="${date:format=HH\:mm\:ss} ${logger}
${uppercase:${level}} ${message}" />

This segment tells NLog that we want to use a new target of the ColoredConsole type. We can call it logcolorconsole.

We also specified a header that should display the Logfile for run text and then the current data. I also added a footer that consists of a simple line. The layout section is the same as the one we used with the file: we display the time (in the HH:mm:ss format), the name of the logger (which is Program or OtherLogger, depending on the line we are on), the level of the log in uppercase, and finally the message itself.

You can vary this as much as you want and add or remove elements at will. You can also set up rules on what to display depending on various factors, such as the level.

We must also add it to the rules. Just for simplicity, I removed the file and console as a target and used the new logcolorconsole one:

<rules>
  <logger name="*"
          minlevel="Info"
          writeTo="logcolorconsole" />
</rules>

If you run the sample after making these changes, you will see a set of colorful lines. Yes, you can change or alter the colors based on the level. The options are almost endless.

As I said, there are over 100 targets available. Let me give you a shortened list of some of the more commonly used targets:

Target name

Description

NuGet package

File Target

Logs data to files on a disk with options for filenames, directories, rotations, and archiving

NLog

Console Target

Sends log messages to the standard output or error streams; useful during development

NLog

ColoredConsole Target

Sends log messages to the Console with color coding based on log level

NLog

Database Target

Logs messages to a database using parameterized SQL commands

NLog

EventLog Target

Writes log entries to the Windows Event Log; ideal for Windows apps

NLog.WindowsEventLog

Mail Target

Sends log entries as email messages; suitable for alerts and monitoring

NLog.MailKit

Network Targets

Includes WebService, TCP, and UDP targets for logging over networks

NLog

Trace Target

Sends log messages to .NET trace listeners, integrating with other diagnostics tools

NLog

Memory Target

Logs messages to an in-memory list of strings, mainly for debugging purposes

NLog

Null Target

A target that does nothing; useful for disabling logging in certain scenarios

NLog

Table 10.3: Targets in NLog

I recommend you look at the documentation at https://nlog-project.org/config/ to see the different options and the settings per option. It is pretty extensive!

Rules in NLog

In addition to the targets, you can set rules in NLog. The rules define which target is used under which circumstances.

In our example, we used one rule: all logs should go to the Console and file targets or the ColoredConsole target, which we named logcolorconsole.

Let’s change that a bit; I want to make it more intelligent. Change the rules section so that it looks like this:

<rules>
  <logger name="*"
          minlevel="Trace"
          writeTo="logfile" />
  <logger name="Program"
          minLevel="Warn"
          writeTo="logcolorconsole" />
  <logger name="OtherLogger"
          minLevel="Info"
          writeTo="logconsole" />
</rules>

We now have three rules:

  • The first is the catch-all. By writing name="*", we tell the system to take all loggers. The minimum level we want is Trace, the lowest level, so we want all messages (yes, you can also define a maximum level). We define the target as a logfile. This target is the one that writes to a file.
  • The second rule only applies to the logger that has the name Program. Thus, all loggers are created by calling GetCurrentClassLogger() using our Main method. We raise the minimum level to Warn; we are not interested in anything below that. The file catches this. We want to see them in nice colors, so we specify the writeTo parameter as logcolorconsole.
  • All messages sent to the logger named OtherLogger are the subject of the third rule. We want all messages of the Info level or above, and we want to see them processed by our colorless, default Console logger.

Run the sample. See how messages on different loggers get sent to the right place.

Asynchronous logging

Remember when I said that anything that takes longer than a few clock cycles should be done asynchronously? Well, NLog allows you to log to databases or network connections. They definitely have long-running operations. Unfortunately, there is no such method as LogAsync() in NLog. However, there is another solution to this.

There is a target called AsyncWrapper. As the name suggests, this is a wrapper around other targets that make them work asynchronously. All you have to do is add that to the configuration like this:

<target
  name="asyncWrapper"
  xsi:type="AsyncWrapper">
  <target
    name="logfile"
    xsi:type="File"
    fileName="${basedir}/logs/logfile.txt"
    layout="${date:format=HH\:mm\:ss} ${logger} ${uppercase:${level}} ${message}" />
  </target>

Although the methods are still synchronous, NLog places all the log messages in a queue on a separate thread and writes them to the target on that thread instead of on the calling thread. You can set several variables to determine how long the delay must be, how long the queue can become, and so on. However, we have eliminated our delay when writing to a file, a database, or a network connection. I strongly suggest that you use that wrapper for anything besides Console!

Two useful but often neglected additional settings

There are two more things I want to show you in the configuration file.

The root element, NLog, can have a property named autoReload=true. If you set that, you can have NLog pick up changes in the log file while the application runs. We saw a similar option with the Microsoft logger; it is good to know that NLog also supports this.

With all the available rules, targets, variables, and other things you can set in the configuration file, you might wonder what to do if things go wrong.

The people behind NLog thought of that as well. You can turn on logging for NLog itself. All you have to do is change the root entry to look like this:

<nlog xmlns=http://www.nlog-project.org/schemas/NLog.xsd
      xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
      autoReload="true"
      internalLogFile="${basedir}/logs/internallog.txt"
      internalLogLevel="Trace"
      >

I have added the internalLogFile and internalLogLevel properties. Adding these properties results in NLog logging its internal logs to the given file. Doing this might help you find issues in your logging. It is all becoming a bit metaphysical, but you can log better by logging the workings of the log. Give it a try!

Serilog

There is one more framework I want to share with you. Serilog is a popular logging framework that first saw the light of day in 2013.

The idea behind Serilog is that it allows for structured logging. So far, all the logs we have seen have all just been one-liners with some text. Serilog is built around the idea that structure can bring clarity.

Let me show you what I mean by that. Let’s build a sample.

Although Serilog can (and should) be controlled by the settings in a configuration file, I will control this final example exclusively through code. I want to show you how to do that so you have at least seen it once.

However, again, since you want to change logging depending on the state of the system, you are better off having a configuration file that you can change without recompiling.

Of course, we will begin by creating a new Console application and adding some NuGet packages.

Standard logging with Serilog

NLog has targets, and Serilog has sinks. You have to install all the sinks you need from different packages. I will only use Console and File in my sample, but there are others: SQL Server, HTTP, AWS, and so on.

You need to install the Serilog, Serilog.Sinks.Console and Serilog.Sinks.File NuGet packages.

Let’s write the code:

using Serilog;
var logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console()
    .WriteTo.File(path:
        "logs\\log.txt",
        rollingInterval: RollingInterval.Day)
    .CreateLogger();
try
{
    logger.Verbose("This is verbose");
    logger.Debug("This is debug");
    logger.Information("This is information");
    logger.Warning("This is warning");
    logger.Error("This is error");
    logger.Fatal("This is fatal");
}
finally
{
    await Log.CloseAndFlushAsync();
}

This code should look familiar. We create a configuration, this time all in code; we create a logger and log our messages. We end with a CloseAndFlushAsync() to ensure nothing is left in some buffer.

There is nothing special about this code. OK, the new thing here is the RollingInterval. This property determines when the system should create a new file. You can set that to anything from a minute to a year. If you do not want to create a new file at any point, you can also set it to Infinite. That way, the system creates the file once and never again (unless you delete it, of course).

Apart from that, there is nothing remarkable about Serilog. However, let’s change that. Change the parameters in the call to WriteTo.File() so that it looks like the following:

var logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Console(new JsonFormatter())
    .WriteTo.File(
        new JsonFormatter(),
        "logs\\log.txt",
        rollingInterval: RollingInterval.Day)
    .CreateLogger();

In this code sample, I added a JsonFormatter to the output of both the console and the file. When you add a formatter, you tell Serilog to output the logs a certain way. The JsonFormatter formatter forces the output to be in (well, you guessed it) the JSON format.

To truly use the structure log, we must change how we log the messages. Let’s add one line to the part where we write the logs:

logger.Information(
    "The user with userId {userId} logged in at {loggedInTime}",
    42,
    DateTime.UtcNow.TimeOfDay);

As you can see, we log a line of text, but instead of building that string beforehand, we do it in the message. In this case, we give it named parameters, userId, and loggedInTime, and then pass in the values that we want to display.

If you run it now, that last line, after formatting, results in this:

{
    "Timestamp": "2024-04-20T11:47:31.5139218+02:00",
    "Level": "Information",
    "MessageTemplate": "The user with userId {userId} logged in at {loggedInTime}",
    "Properties": {
        "userId": 42,
        "loggedInTime": "09:47:31.5125828"
    }
}

As you can see, a lot more information is suddenly available. The structure of the logline is such that if we store it in a system somewhere, we can easily query the lines. Later in this chapter, I will show you how this is done.

So, Serilog is comparable to the other two frameworks until you use one of the many formatters. The ability to store the log information to easily query it makes it a very powerful tool to have in your toolbelt!

Log levels in Serilog logging

As you will probably expect by now, Serilog also has levels. Those levels should look very familiar to you. This table shows the levels that Serilog offers and what they are meant to do.

Serilog level

Description

Verbose

This contains the most detailed information. These messages may contain sensitive application data and are therefore not recommended for production unless hidden.

Debug

This level contains information that is useful in development and debugging.

Information

This level contains informational messages that highlight the general flow of the application. It is useful for general application insights.

Warning

Indications of possible issues or service and functionality degradation are included at this level.

Error

Errors and exceptions that cannot be handled or are unexpected are included here.

Fatal

This level focuses on critical errors causing complete failure of the application and requiring immediate attention.

Silent

This is the level for no logging at all (Serilog does not explicitly define a Silent level, but logging can effectively be turned off).

Table 10.4: Serilog log levels

Again, there are no surprises here. As with the other frameworks, you can use this however you want: no one can stop you from adding lots of debug information to the Error level. It is just not a very good idea.

Comparing the logging frameworks

After having seen all of these frameworks, you might wonder: which one should I pick? The answer is simple: choose whichever one you feel most comfortable with.

All frameworks have pros and cons; none are bad or extremely good. They do have different use cases and areas of attention. The following table highlights some of those:

Feature

.NET Logger

NLog

Serilog

Overview

Reliable and integrates seamlessly with .NET

Rich in features, great for a wide range of applications

Excels in structured logging, making data meaningful and searchable

Integration

Deeply integrated with .NET Core, supports dependency injection and configuration settings

Flexible, can be used in various .NET applications, supports multiple targets

Great with .NET applications, especially for structured data stores such as Seq or Elasticsearch

Pros

Minimal setup

Supports structured logging

Advanced log routing and filtering; logs to multiple targets simultaneously

Exceptional at structured logging; supports enrichers for additional context

Cons

Less feature-rich without third-party providers

Configuration can get complex

Might be overkill for simple needs; best features require compatible logging targets

Best for

Projects that need straightforward logging with minimal setup

Applications requiring detailed control over logging, or when logging into multiple places

Projects where structured logging and data querying are priorities

Table 10.5: Comparison between the logging frameworks

You just have to look at your own needs and determine your scenario and way of working best. Pick that tool. My advice is to give the others a go. You might find a new favorite logging framework!

So, we have now looked at logging. We have seen the most commonly used frameworks and how to use them. We have looked at default Microsoft logging; we have had an in-depth look at NLog and its robust collection of targets and rules. Finally, we have looked at Serilog’s structured logging approach.

You should be able to use logging from now on. However, logging is part of your application. What if you do not get all the information you need from logging? That is where monitoring comes into play. Let’s have a look at that next!

A word of caution

Logging is very useful. In fact, I would suggest that you cannot do serious development on systems without a UI if you do not have extensive logging. However, you must be careful: it is too easy to leak sensitive information about your system. Consider things such as connection strings, credentials, and other sensitive information. Also, you might sometimes accidentally disclose information about the inner workings of your system or even about the organization that this system runs. Be careful. Do not assume that people will not try to move the log level to Trace to see what is happening. Log as much as possible, but be mindful of the dangers!

Logging is one of the best things you can do to solve development and production issues. However, there is more that we can do. We need insights into these logs, but we must also monitor things such as memory usage, CPU usage, and much more. Let’s talk about monitoring next!

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image