Logs are essential for companies building software applications. They provide detailed information about an application’s execution and performance, helping with troubleshooting, debugging, and testing your application. Additionally, the data from logs can inform better business or technical decisions for your application.
In part one of this .Net logging guide series, we will introduce you to logging facilities in .NET applications, the available frameworks for .NET, and the configuration settings for logging frameworks (such as log level, log scopes, and filter functions). Let’s begin with a brief explanation of .NET logging frameworks.
Understanding .NET Logging Frameworks
.NET provides a logging API with Microsoft.Extensions.Logging
. Microsoft-supplied and third-party plugins extend this framework across multiple logging systems. You can use this framework when logging in .NET applications written with C#, F#, and Visual Basic. The framework includes various components that help with logging, which include:
- ILogger
- LoggerFactory
- ILoggerFactory
- ILoggerProvider
ILogger
The ILogger
interface represents a type that performs logging actions. It contains method definitions for writing log events. The following code snippet shows some of the included methods:
// Writes a log message void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter); // Checks if a log level is enabled bool IsEnabled(LogLevel logLevel); // Begins a logical operation scope IDisposable BeginScope<TState>(TState state);
ILoggerFactory
ILoggerFactory
represents a type that registers an ILoggerProvider
and instantiates an ILogger
. It contains method definitions for two operations:
// Instantiates an ILogger that logs events ILogger CreateLogger(string categoryName); // Add provider to list of providers void AddProvider(ILoggerProvider provider);
.NET also provides a class called LoggerFactory
, which implements ILoggerFactory
. This factory class creates an ILogger
for your application.
ILoggerProvider
The ILoggerProvider
contains definitions that instantiate a logger with the specified category name. This interface has one method:
ILogger CreateLogger(string categoryName);
Extensibility of the .NET Logging API
You can extend the logging API by implementing any of the interfaces in Microsoft.Extensions.Logging
. To create a custom logger, you can extend the ILogger
class and override the methods with your custom implementation.
The following code snippet shows an example of how a DefaultLogger
class extends the ILogger
interface:
using Microsoft.Extensions.Logging; public class DefaultLogger : ILogger { ... public IDisposable BeginScope<TState>(TState state) => default!; public bool IsEnabled(LogLevel logLevel) => true; public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { //Write code to log to destination } }
Likewise, you can extend the ILoggerFactory
and ILoggerProvider
interfaces with this approach.
Default Log File Location
Logging with .NET allows you to specify the default location for your logs throughout the lifecycle of your application. You can start Visual Studio (Microsoft’s development environment for .NET) and log all activities to a specified file with the following command:
Devenv /log LogFile
In the above code snippet, LogFile
should be the full valid path to an existing file. Logs will subsequently be written to that file.
Logging Providers in .NET
A logging provider saves logs to a specified destination. .NET has four logging providers, which are as follows:
Console
.NET provides a console provider library called Microsoft.Extensions.Logging.Console
. This provider logs the output to the console. ConsoleLoggerProvider
can be used to log while testing an application locally so that you can see the status of your application in real time. Logs written to the console can also help with debugging.
Debug
.NET provides a library for the debug provider called Microsoft.Extensions.Logging.Debug
. This provider writes logs using the Debug.WriteLine
method in the System.Diagnostics.Debug
class. DebugLoggerProvider
creates instances of DebugLogger
, a class that implements the ILogger
interface. Logs written to the debug console can also help with debugging.
EventSource
.NET provides an event source provider library called Microsoft.Extensions.Logging.EventSource
. This provider writes to a cross-platform event source called Microsoft-Extensions-Logging
. You can use this provider when you set up event listeners in different parts of your schedule and want to log these events when they are triggered.
EventLog
.NET provides a library for the event source provider named Microsoft.Extensions.Logging.EventLog
. EventLog
allows you to access or customize Windows event logs. These logs are information about critical software or hardware events, such as when a user logs into a virtual machine. EventLog
is valuable for microservices deployed to different machines because it can specify the computer that owns the event, the log message, and the event ID. With EventLog
, you can read from existing logs, write logs, manage event sources, and delete logs.
Logging Frameworks in .NET
The .NET Logging API provides fundamental features for logging. However, these are building blocks for more advanced features that you will need when building applications that collect different types of log messages. Third-party logging frameworks, such as Log4Net, SeriLog, and NLog, have implemented additional logging functions like structured logging and layout log renderers in .NET.
Log4Net
Log4Net helps programmers log messages to various outputs. Log4Net has been the first choice for .NET logging since its inception in 2001. However, it’s an older project, and developers haven’t released a new major version for several years.
SeriLog
SeriLog is a modern framework for structured logging. Structured logging helps programmers write queryable logs. SeriLog has a wide user base and is easy to integrate. Many .NET projects have been using SeriLog since its inception in 2013.
NLog
NLog has been popular since its inception in 2006. It allows programmers to write to multiple destinations. NLog also supports structured logging and, when compared to other logging libraries, is widely used in various .NET projects today.
Implementing Logging with the Logging API
Create and use Loggers
To create and use a logger, pass an instance of a Logger
object through dependency injection so that you can use it from your constructor.
public class CustomClass { private readonly ILogger _logger; public CustomClass(ILogger<CustomClass> logger) { _logger = logger; } }
To write logs using the Logger
you created, invoke a log method for the specified level. The following code snippet writes an information-level log:
public class CustomClass { private readonly ILogger _logger; public CustomClass(ILogger<CustomClass> logger) { _logger = logger; } public void CustomMethod() { _logger.LogInformation("Logging a custom event"); } }
Configure logging
Logging configurations are in the Logging
section of the appsettings.[environment].json
file, where environment refers to the environment in which the application runs. An example file, named appsettings.production.json
, is shown below:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Debug" } } }
Structured logging
Structured logging simplifies storing and querying log events because the log event maintains the message template and parameters instead of transforming them to a formatted message.
The .NET String.Format() method only accepts input strings like this:
... var emailAddress = "janedoe@website.com"; var name = "Jane Doe" logger.LogInformation("Error from:{0} with email-address:{1}", name, emailAddress);
When you store these log events, it’s challenging to query for information retrieval. Log message templates facilitate structured logging. They can contain placeholders for arguments.
logger.LogInformation("Error from:{username} with email-address:{email}", name, emailAddress);
With the code snippet above, you can query logs with query parameters such as username or email.
Using log levels
Knowing log levels and when to use them can be incredibly helpful. Log levels allow software developers to group logs into different classes. Log data is easier to filter and analyze when you specify the correct log level with the log message. For example, you should not write a success log message at the Error log level because this will misrepresent your logs and make debugging difficult.
The following table shows the various log levels for .NET, in increasing severity (except for None):
Log Level | Description |
---|---|
Trace | These logs include elaborate messages about an application's execution trail. Disable them in production as they retain sensitive application data and increase logging volume. |
Debug | These logs contain data for debugging during development. |
Information | These logs monitor typical application flows. |
Warning | These logs indicate anomalous behavior during program execution. |
Error | These logs signify errors during program execution. |
Critical | These logs display critical errors that demand prompt mitigation. |
None | This log level establishes that a logging category should not log messages. |
Defining a logging level enables logging of that level and any levels more severe. For example, if the log level is Warning, only events marked as Warning, Error, or Critical will fire.
The following code snippet writes a log message at the Error level:
logger.Log(LogLevel.Error, "This is an error!");
Using log scopes
In some cases, you may want to add the same value to every log message. For example, if you are capturing logs for an ecommerce backend service, you might want to add an order ID to every log message until an order is completed. You can manually write the ID to every message, but .NET provides the concept of log scopes to make this easier. A scope can group several logical operations. The BeginScope method of ILogger returns the scope.
To utilize a scope, wrap logger calls in a using block as illustrated below:
public void TestMethod() { ... using (_logger.BeginScope("test method scope")) { // code _logger.Log(LogLevel.Information, "This is a log message"); } }
Log your data with CrowdStrike Falcon Next-Gen SIEM
Elevate your cybersecurity with the CrowdStrike Falcon® platform, the premier AI-native platform for SIEM and log management. Experience security logging at a petabyte scale, choosing between cloud-native or self-hosted deployment options. Log your data with a powerful, index-free architecture, without bottlenecks, allowing threat hunting with over 1 PB of data ingestion per day. Ensure real-time search capabilities to outpace adversaries, achieving sub-second latency for complex queries. Benefit from 360-degree visibility, consolidating data to break down silos and enabling security, IT, and DevOps teams to hunt threats, monitor performance, and ensure compliance seamlessly across 3 billion events in less than 1 second.