Wednesday, May 3, 2023

Custom logger for .Net Core for writing logs to Azure BLOB storage

If you create .Net Core app you may use standard console logger (from Microsoft.Extensions.Logging.Console nuget package). For development it works quite well but if app goes to production you probably want to store logs in some persistent storage in order to be able to check them when needed. In this post I will show how to create custom logger which will write logs to Azure BLOB storage.

At first need to mention that there is BlobLoggerProvider from MS (from Microsoft.Extensions.Logging.AzureAppServices nuget package) which creates BatchingLogger which in turn stores logs to BLOB storage as well. However it has dependency on Azure App Services infrastructure so on practice you may use it only if your code is running inside Azure App Service. If this limitation is important we may go with custom logger implementation.

First of all we need to implement logger itself which inherits Microsoft.Extensions.Logging.ILogger interface and writes logs to the BLOB:

public class BlobLogger : ILogger
{
	private const string CONTAINER_NAME = "custom-logs";
	private string connStr;
	private string categoryName;

	public BlobLogger(string categoryName, string connStr)
	{
		this.connStr = connStr;
		this.categoryName = categoryName;
	}

	public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
		Func<TState, Exception?, string> formatter)
	{
		if (!IsEnabled(logLevel))
		{
			return;
		}

		using (var ms = new MemoryStream(Encoding.UTF8.GetBytes($"[{this.categoryName}: {logLevel,-12}] {formatter(state, exception)}{Environment.NewLine}")))
		{
			var container = this.ensureContainer();
			var now = DateTime.UtcNow;
			var blob = container.GetAppendBlobClient($"{now:yyyyMMdd}/log.txt");
			blob.CreateIfNotExists();
			blob.AppendBlock(ms);
		}
	}

	private BlobContainerClient ensureContainer()
	{
		var container = new BlobContainerClient(this.connStr, CONTAINER_NAME);
		container.CreateIfNotExists();
		return container;
	}

	public bool IsEnabled(LogLevel logLevel) => true;

	public IDisposable BeginScope<TState>(TState state) => default!;
}

BlobLogger creates new container "custom-logs" (if it doesn't exist yet) in specified BLOB storage. Inside this container it also creates folders using current date as folder name yyyyMMdd (own folder for each day) and writes messages to the log.txt file inside this folder. Note that for working with Azure BLOB storage we used BlobContainerClient class from Azure.Storage.Blobs nuget package since. It will allow us to use instance of our logger as singleton (see below) because instance methods of this client class are guaranteed to be thread safe:

Thread safety
We guarantee that all client instance methods are thread-safe and independent of each other (guideline). This ensures that the recommendation of reusing client instances is always safe, even across threads.

In order to create BlobLogger we need to pass connection string to Azure storage and logging category name. It will be done in logger provider which will be responsible for creating BlobLogger:

public class BlobLoggerProvider : ILoggerProvider
{
	private string connStr;

	public BlobLoggerProvider(string connStr)
	{
		this.connStr = connStr;
	}

	public ILogger CreateLogger(string categoryName)
	{
		return new BlobLogger(categoryName, this.connStr);
	}

	public void Dispose()
	{
	}
}

Now everything is ready for using our logger in .Net Core app. If we want to use our BLOB logger together with console logger (which is quite convenient since logging is done both into console and into BLOB storage) we may use LoggingFactory and pass both standard ConsoleLoggerProvider and our BlobLoggerProvider:

var configuration = builder.GetContext().Configuration;
var azureStorageConnectionString = configuration.GetValue<string>("AzureWebJobsStorage");
var logger = new LoggerFactory(new ILoggerProvider[]
{
	new ConsoleLoggerProvider(new OptionsMonitor<ConsoleLoggerOptions>(new ConsoleLoggerOptions())),
	new BlobLoggerProvider(azureStorageConnectionString)
}).CreateLogger("CustomLogger");
builder.Services.AddSingleton(logger);

For creating instance of ConsoleLoggerProvider I used OptionsMonitor<T> helper class from How to create a ConsoleLoggerProvider without the full hosting framework (don't know why MS didn't provide this option OTB and made it so complex there):

public class OptionsMonitor<T> : IOptionsMonitor<T>
{
	private readonly T options;

	public OptionsMonitor(T options)
	{
		this.options = options;
	}

	public T CurrentValue => options;

	public T Get(string name) => options;

	public IDisposable OnChange(Action<T, string> listener) => new NullDisposable();

	private class NullDisposable : IDisposable
	{
		public void Dispose() { }
	}
}

After that you will have logs both in console and in Azure BLOB storage.

Update 2023-06-22: wrote another post how to add support of logging scopes which work in task-based asynchronous pattern (TAP) and multithread code: Custom logger for .Net Core for writing logs to Azure BLOB storage.

No comments:

Post a Comment