CrowdStrike 2025 Global Threat Report: Adversaries have adapted. Have you? Download

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
ILogger
and
ILoggerProvider
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.

Learn More

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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
{<!-- --><p>"Logging": {<!-- --></p><p>"LogLevel": {<!-- --></p><p>"Default": "Information",</p><p>"Microsoft.AspNetCore": "Error"</p><p>}</p><p>}</p><p>}</p>
{<!-- --><p>"Logging": {<!-- --></p><p>"LogLevel": {<!-- --></p><p>"Default": "Information",</p><p>"Microsoft.AspNetCore": "Error"</p><p>}</p><p>}</p><p>}</p>
Expand
{

"Logging": {

"LogLevel": {

"Default": "Information",

"Microsoft.AspNetCore": "Error"

}

}

}

The above configuration sets the default logging of all categories at log level

Information
Information or higher, while categories beginning with
Microsoft.AspNetCore
Microsoft.AspNetCore log at the level
Error
Error or higher. An example of such a category is
Microsoft.AspNetCore.Routing.EndpointMiddleware
Microsoft.AspNetCore.Routing.EndpointMiddleware.

Creating a custom logger class requires implementing the

ILogger
ILogger interface in the
Microsoft.Extensions.Logging
Microsoft.Extensions.Logging namespace.
ILogger
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
CustomLogger:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using Microsoft.Extensions.Logging;<p>// Implement the ILogger interface</p><p>public class CustomLogger : ILogger</p><p>{<!-- --></p><p>public IDisposable BeginScope<TState>(TState state)</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>public bool IsEnabled(LogLevel logLevel)</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>}</p>
using Microsoft.Extensions.Logging;<p>// Implement the ILogger interface</p><p>public class CustomLogger : ILogger</p><p>{<!-- --></p><p>public IDisposable BeginScope<TState>(TState state)</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>public bool IsEnabled(LogLevel logLevel)</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>}</p>
Expand
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
BeginScope,
IsEnabled
IsEnabled and
Log
Log.

BeginScope
BeginScope develops a new logging scope. A logging scope collects related log messages and links them to a particular operation or context.
IsEnabled
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
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:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using Microsoft.Extensions.Logging;<p>public class CustomDestinationLogger : ILogger</p><p>{<!-- --></p><p>// This is a client for an external log destination</p><p>private CustomLogDestinationClient logClient;</p><p>public CustomDestinationLogger()</p><p>{<!-- --></p><p>// initialization code</p><p>}</p><p>public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!;</p><p>public bool IsEnabled(LogLevel logLevel) => true;</p><p>public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)</p><p>{<!-- --></p><p>if(!IsEnabled(logLevel))</p><p>{<!-- --></p><p>return;</p><p>}</p><p></p><p>// This line emits logs to a custom destination</p><p>logClient.Emit(eventId, state, exception, formatter);</p><p>}</p><p>}</p>
using Microsoft.Extensions.Logging;<p>public class CustomDestinationLogger : ILogger</p><p>{<!-- --></p><p>// This is a client for an external log destination</p><p>private CustomLogDestinationClient logClient;</p><p>public CustomDestinationLogger()</p><p>{<!-- --></p><p>// initialization code</p><p>}</p><p>public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!;</p><p>public bool IsEnabled(LogLevel logLevel) => true;</p><p>public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)</p><p>{<!-- --></p><p>if(!IsEnabled(logLevel))</p><p>{<!-- --></p><p>return;</p><p>}</p><p></p><p>// This line emits logs to a custom destination</p><p>logClient.Emit(eventId, state, exception, formatter);</p><p>}</p><p>}</p>
Expand
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
Log
 method calls
logClient.Emit()
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
ILoggerProvider interface in the
Microsoft.Extensions.Logging
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
CustomLoggerProvider:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public interface CustomLoggerProvider: ILoggerProvider<p>{<!-- --></p><p>public ILogger CreateLogger(string categoryName)</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>public void Dispose()</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>}</p>
public interface CustomLoggerProvider: ILoggerProvider<p>{<!-- --></p><p>public ILogger CreateLogger(string categoryName)</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>public void Dispose()</p><p>{<!-- --></p><p>// Your code here</p><p>}</p><p>}</p>
Expand
public interface CustomLoggerProvider: ILoggerProvider

{

public ILogger CreateLogger(string categoryName)

{

// Your code here

}

public void Dispose()

{

// Your code here

}

}

The

ILoggerProvider
ILoggerProvider interface contains the
CreateLogger
CreateLogger and
Dispose
Dispose methods. The
CreateLogger
CreateLogger method initiates an
ILogger
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
CustomDestinationLogger:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using Microsoft.Extensions.Logging;<p>public class CustomLoggerProvider : ILoggerProvider</p><p>{<!-- --></p><p>private readonly ConcurrentDictionary<string, CustomDestinationLogger> _loggers;</p><p>public CustomLoggerProvider()</p><p>{<!-- --></p><p>// initialization code</p><p>}</p><p>public ILogger CreateLogger(string categoryName)</p><p>{<!-- --></p><p>var logger = _loggers.GetOrAdd(categoryName, new CustomDestinationLogger());</p><p>}</p><p>public void Dispose()</p><p>{<!-- --></p><p>_loggers.Clear();</p><p>}</p><p>}</p>
using Microsoft.Extensions.Logging;<p>public class CustomLoggerProvider : ILoggerProvider</p><p>{<!-- --></p><p>private readonly ConcurrentDictionary<string, CustomDestinationLogger> _loggers;</p><p>public CustomLoggerProvider()</p><p>{<!-- --></p><p>// initialization code</p><p>}</p><p>public ILogger CreateLogger(string categoryName)</p><p>{<!-- --></p><p>var logger = _loggers.GetOrAdd(categoryName, new CustomDestinationLogger());</p><p>}</p><p>public void Dispose()</p><p>{<!-- --></p><p>_loggers.Clear();</p><p>}</p><p>}</p>
Expand
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
CreateLogger method creates an instance of the
CustomDestinationLogger
CustomDestinationLogger for each category name, then stores it in the
ConcurrentDictionary
ConcurrentDictionary.

To free up memory, use the

Dispose
Dispose method to clear all the loggers in the
ConcurrentDictionary
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
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
var builder = Host.CreateDefaultBuilder(args);<p>builder.ConfigureServices();</p><p>builder.Services.AddSingleton<ILoggerProvider, CustomLoggerProvider>();</p><p>using var host = builder.Build();</p><p>host.Run();</p>
var builder = Host.CreateDefaultBuilder(args);<p>builder.ConfigureServices();</p><p>builder.Services.AddSingleton<ILoggerProvider, CustomLoggerProvider>();</p><p>using var host = builder.Build();</p><p>host.Run();</p>
Expand
var builder = Host.CreateDefaultBuilder(args);

builder.ConfigureServices();

builder.Services.AddSingleton<ILoggerProvider, CustomLoggerProvider>();

using var host = builder.Build();

host.Run();

We now have

CustomLoggerProvider
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
AccountsController
class that uses the logger we created:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
public class AccountsController {<!-- --><p></p><p>private ILogger _logger;</p><p>public AccountsController(ILoggerProvider loggerProvider)</p><p>{<!-- --></p><p>_logger = loggerProvider.CreateLogger(nameof(AccountsController));</p><p>}</p><p>public void Create() {<!-- --></p><p>// ... your implementation here</p><p>_logger.Log(logLevel, eventId, state, exception, formatter);</p><p>}</p><p>}</p>
public class AccountsController {<!-- --><p></p><p>private ILogger _logger;</p><p>public AccountsController(ILoggerProvider loggerProvider)</p><p>{<!-- --></p><p>_logger = loggerProvider.CreateLogger(nameof(AccountsController));</p><p>}</p><p>public void Create() {<!-- --></p><p>// ... your implementation here</p><p>_logger.Log(logLevel, eventId, state, exception, formatter);</p><p>}</p><p>}</p>
Expand
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
AccountsController constructor contains an
ILoggerProvider
ILoggerProvider instance from the dependency injection container. The constructor in this class also calls the
CreateLogger
CreateLogger method to fetch a logger for the
AccountsController
AccountsController
class. Now, we use the custom logger we created to log to a unique destination, similar to how the
Create
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.

Schedule Falcon Next-Gen SIEM Demo

Arfan Sharif is a product marketing lead for the Observability portfolio at CrowdStrike. He has over 15 years experience driving Log Management, ITOps, Observability, Security and CX solutions for companies such as Splunk, Genesys and Quest Software. Arfan graduated in Computer Science at Bucks and Chilterns University and has a career spanning across Product Marketing and Sales Engineering.