In the previous articles in this series, we covered core concepts for logging in .NET, including frameworks, exception logging, common mistakes, security considerations and best practices. In part four, we discuss custom loggers and how to use ILogger
and ILoggerProvider
to build a logger that logs to other destinations beyond the .NET defaults.
A log provider is a component that facilitates logging to a particular destination, such as a file, database or cloud service. Many organizations use other sophisticated platforms to store their logs, so it is important to know how to integrate custom logger providers into your .NET application.
The Importance of Custom Loggers
Custom loggers are unique classes or functions for handling message logging from a single module or section of an application. They enable programmers to define custom behaviors such as message formatting and log filtering. By using custom loggers to help control the flow of log messages in distributed applications, software developers can more easily spot and fix problems when they occur.
.NET applications that need to log to particular destinations, such as cloud-based services or private databases, can leverage custom log providers to achieve this. Engineers can take advantage of the built-in logging capabilities of the .NET framework while creating custom log providers to easily integrate with their preferred logging destination.
Incorporating organization-specific tools into logging providers is also possible with custom loggers. These tools can be additional services that the organization uses for managing and analyzing log data in addition to the built-in features of a logging provider. For example, an organization might have a custom tool for searching and filtering log messages or creating alerts when specific types of log messages occur. This integration can enhance the overall effectiveness and efficiency of an organization's logging and monitoring procedures.
How to Create a Custom Logger
Before creating the custom logger, you must configure logging in your project. The Logging section of the app settings typically offers logging configuration. The code snippet below shows a sample configuration in JSON:
{"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Error"
}
}
}
The above configuration sets the default logging of all categories at log level Information
or higher, while categories beginning with Microsoft.AspNetCore
log at the level Error
or higher. An example of such a category is Microsoft.AspNetCore.Routing.EndpointMiddleware
.
Creating a custom logger class requires implementing the ILogger
interface in the Microsoft.Extensions.Logging
namespace. ILogger
defines a set of functions, properties and events that deliver a uniform logging interface for diverse application logging providers. This interface allows software developers to use a single logging API to write log messages which can then route to different logging providers, such as a file, the console, or a third-party logging service.
The following code snippet shows the structure for creating a logger called CustomLogger
:
using Microsoft.Extensions.Logging;// Implement the ILogger interface
public class CustomLogger : ILogger
{
public IDisposable BeginScope<TState>(TState state)
{
// Your code here
}
public bool IsEnabled(LogLevel logLevel)
{
// Your code here
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
// Your code here
}
}
The three functions to implement are BeginScope
, IsEnabled
and Log
.
BeginScope
develops a new logging scope. A logging scope collects related log messages and links them to a particular operation or context.IsEnabled
verifies that a specified log level can write log messages. This method aids in optimizing code by bypassing the overhead of formatting and writing invalid log messages.Log
is the main logging method in .NET. This method writes a log message with a specified log level, event ID and state.To implement your custom logger, you would override the three methods above.
The code snippet below shows the implementation of a custom logger that emits logs to a custom destination:
using Microsoft.Extensions.Logging;public class CustomDestinationLogger : ILogger
{
// This is a client for an external log destination
private CustomLogDestinationClient logClient;
public CustomDestinationLogger()
{
// initialization code
}
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if(!IsEnabled(logLevel))
{
return;
}
// This line emits logs to a custom destination
logClient.Emit(eventId, state, exception, formatter);
}
}
In the preceding code, the Log
method calls logClient.Emit()
, a function that emits your application logs to an external log destination beyond the .NET defaults. This destination can be anywhere of your choosing. Depending on the SDK provided for that destination, you would use the appropriate method to emit your logs.
After creating the custom logger, the next step is to wire up the custom logger provider.
How to Create a Custom Logger Provider
Creating a custom logger provider requires implementing the ILoggerProvider
interface in the Microsoft.Extensions.Logging
namespace. It defines a set of methods a logger provider must implement to instantiate a logger.
The code snippet below shows the structure for creating a logger provider called CustomLoggerProvider
:
public interface CustomLoggerProvider: ILoggerProvider{
public ILogger CreateLogger(string categoryName)
{
// Your code here
}
public void Dispose()
{
// Your code here
}
}
The ILoggerProvider
interface contains theCreateLogger
and Dispose
methods. The CreateLogger
method initiates an ILogger
for a specified category. The Dispose method removes the logger provider resources when they are no longer needed. To implement your custom logger provider, override the two methods above.
The code snippet below shows the implementation of a custom logger provider that creates and returns instances of CustomDestinationLogger
:
using Microsoft.Extensions.Logging;public class CustomLoggerProvider : ILoggerProvider
{
private readonly ConcurrentDictionary<string, CustomDestinationLogger> _loggers;
public CustomLoggerProvider()
{
// initialization code
}
public ILogger CreateLogger(string categoryName)
{
var logger = _loggers.GetOrAdd(categoryName, new CustomDestinationLogger());
}
public void Dispose()
{
_loggers.Clear();
}
}
In the code above, the CreateLogger
method creates an instance of the CustomDestinationLogger
for each category name, then stores it in the ConcurrentDictionary
.
To free up memory, use the Dispose
method to clear all the loggers in the ConcurrentDictionary
.
How to Use the Custom Logger
In .NET, dependency injection is a design pattern that supplies the object dependencies for a class in a structured way. Instead of creating object dependencies directly within the class, the class gets its dependencies through a constructor or method parameter. This technique makes the class more adaptable and testable; the design implies that mocking the dependencies or replacing them with alternative implementations is possible.
To use a custom .NET logger in your application, register the logger with the dependency injection system in your app. This process typically involves creating a class that implements ILogger
, and then adding an instance of that class to the dependency injection container. After registering the logger, use dependency injection to obtain a reference to the custom logger in any .NET class that needs it.
The code snippet below shows how to add the custom logger provider to the dependency injection container.
var builder = Host.CreateDefaultBuilder(args);builder.ConfigureServices();
builder.Services.AddSingleton<ILoggerProvider, CustomLoggerProvider>();
using var host = builder.Build();
host.Run();
We now have CustomLoggerProvider
in our dependency injection container. Next, we import it into any class in our .NET project. The following code snippet shows a sample AccountsController
class that uses the logger we created:
public class AccountsController {private ILogger _logger;
public AccountsController(ILoggerProvider loggerProvider)
{
_logger = loggerProvider.CreateLogger(nameof(AccountsController));
}
public void Create() {
// ... your implementation here
_logger.Log(logLevel, eventId, state, exception, formatter);
}
}
The AccountsController
constructor contains an ILoggerProvider
instance from the dependency injection container. The constructor in this class also calls the CreateLogger
method to fetch a logger for the AccountsController
class. Now, we use the custom logger we created to log to a unique destination, similar to how the Create
method uses the logger above.
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.