Some time ago I wrote a post how to implement custom logger which writes logs to Azure storage blob container: Custom logger for .Net Core for writing logs to Azure BLOB storage. This logger implements ILogger interface from Microsoft.Extensions.Logging namespace. It works quite well but doesn't support logging scopes:
public IDisposable BeginScope<TState>(TState state) => default!;
Logging scopes are quite useful - they allow to specify additional information e.g. from each method log record was added, etc:
public void Foo() { using (logger.BeginScope("Outer scope")) { ... using (logger.BeginScope("Inner scope")) { } } }
Important requirement for logging scopes is that it should work properly in Task-based asynchronous pattern (TAP) and multithread code (which is widely used nowadays). For that we will use AsyncLocal<T> class from .NET. For implementing scopes themselves we will use linked list (child-parent relation).
For implementing it we will create LogScopeProvider class which implements Microsoft.Extensions.Logging.IExternalScopeProvider interface (for creating this custom LogScopeProvider I used code from Microsoft.Extensions.Logging.Console namespace as base example):
public class LogScopeProvider : IExternalScopeProvider { private readonly AsyncLocal<LogScope> currentScope = new AsyncLocal<LogScope>(); public object Current => this.currentScope.Value?.State; public LogScopeProvider() {} public void ForEachScope<TState>(Action<object, TState> callback, TState state) { void Report(LogScope current) { if (current == null) { return; } Report(current.Parent); callback(current.State, state); } Report(this.currentScope.Value); } public IDisposable Push(object state) { LogScope parent = this.currentScope.Value; var newScope = new LogScope(this, state, parent); this.currentScope.Value = newScope; return newScope; } private class LogScope : IDisposable { private readonly LogScopeProvider provider; private bool isDisposed; internal LogScope(LogScopeProvider provider, object state, LogScope parent) { this.provider = provider; State = state; Parent = parent; } public LogScope Parent { get; } public object State { get; } public override string ToString() { return State?.ToString(); } public void Dispose() { if (!this.isDisposed) { this.provider.currentScope.Value = Parent; this.isDisposed = true; } } } }
Note that LogScopeProvider stores scopes as AsyncLocal<LogScope> which allows to use it in TAP code. So e.g. if we have await inside using(scope) it will be handled correctly:
public async Task Foo() { using (logger.BeginScope("Outer scope")) { var result = await Bar(); ... } }
Now returning to our BlobStorage: all we have to is to pass LogScopeProviderto its constructor, add current scope to the log record and return new scope when it is requested (check bold code):
public class BlobLogger : ILogger { private const string CONTAINER_NAME = "custom-logs"; private string connStr; private string categoryName; private LogScopeProvider scopeProvider; public BlobLogger(string categoryName, string connStr, LogScopeProvider scopeProvider) { this.connStr = connStr; this.categoryName = categoryName; this.scopeProvider = scopeProvider; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) { if (!IsEnabled(logLevel)) { return; } string scope = this.scopeProvider.Current as string; using (var ms = new MemoryStream(Encoding.UTF8.GetBytes($"[{this.categoryName}: {logLevel,-12}] {scope} {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) => this.scopeProvider.Push(state); }
That's it: now our logger also supports logging scopes.