File: Backchannel\BackchannelLoggerProvider.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Threading.Channels;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Backchannel;
 
internal class BackchannelLoggerProvider : ILoggerProvider
{
    private readonly Queue<BackchannelLogEntry> _replayBuffer = new();
    private readonly object _lock = new();
    private readonly Dictionary<int, Channel<BackchannelLogEntry>> _subscribers = [];
    private int _nextSubscriberId;
    private const int MaxReplayEntries = 1000;
 
    /// <summary>
    /// Gets a snapshot of buffered log entries and subscribes for new entries.
    /// The returned channel receives all future log entries until disposed.
    /// </summary>
    internal (List<BackchannelLogEntry> Snapshot, int SubscriberId, Channel<BackchannelLogEntry> Channel) Subscribe()
    {
        var channel = Channel.CreateUnbounded<BackchannelLogEntry>();
        lock (_lock)
        {
            var id = _nextSubscriberId++;
            _subscribers[id] = channel;
            // Snapshot under lock so no entries are missed between snapshot and subscribe
            return ([.. _replayBuffer], id, channel);
        }
    }
 
    internal void Unsubscribe(int subscriberId)
    {
        lock (_lock)
        {
            if (_subscribers.Remove(subscriberId, out var channel))
            {
                channel.Writer.TryComplete();
            }
        }
    }
 
    internal void WriteEntry(BackchannelLogEntry entry)
    {
        lock (_lock)
        {
            if (_replayBuffer.Count >= MaxReplayEntries)
            {
                _replayBuffer.Dequeue();
            }
            _replayBuffer.Enqueue(entry);
 
            foreach (var subscriber in _subscribers.Values)
            {
                subscriber.Writer.TryWrite(entry);
            }
        }
    }
 
    public ILogger CreateLogger(string categoryName)
    {
        return new BackchannelLogger(categoryName, this);
    }
 
    public void Dispose()
    {
        lock (_lock)
        {
            foreach (var subscriber in _subscribers.Values)
            {
                subscriber.Writer.TryComplete();
            }
            _subscribers.Clear();
        }
    }
}
 
internal class BackchannelLogger(string categoryName, BackchannelLoggerProvider provider) : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull
    {
        return default;
    }
 
    public bool IsEnabled(LogLevel logLevel)
    {
        return true;
    }
 
    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
    {
        if (IsEnabled(logLevel))
        {
            var entry = new BackchannelLogEntry
            {
                Timestamp = DateTimeOffset.UtcNow,
                CategoryName = categoryName,
                LogLevel = logLevel,
                EventId = eventId,
                Message = formatter(state, exception),
            };
 
            provider.WriteEntry(entry);
        }
    }
}