← All source files

V3 / 536edbbdbb6e

Logger.cs

Lilith/Core/Logging/Logger.cs

Unchanged from the preceding version
CS
namespace Lilith.Core.Logging;

/// <summary>
/// Central logging hub. Subscribers receive every <see cref="LogEntry"/> via
/// <see cref="OnLog"/> or <see cref="Subscribe"/>.
/// </summary>
public class Logger(Action<LogEntry>? onLog = null)
{
    private readonly object _sync = new();
    private readonly List<Action<LogEntry>> _subscribers = onLog is null
        ? []
        : [onLog];

    private static Logger? _default;

    /// <summary>Shared logger for apps that want a single global instance.</summary>
    public static Logger Default => _default ??= new();

    /// <summary>Configure the shared <see cref="Default"/> logger (optional initial handler).</summary>
    public static Logger ConfigureDefault(Action<LogEntry>? onLog = null)
    {
        _default = new Logger(onLog);
        return _default;
    }

    /// <summary>
    /// Fired for every log entry. Use <c>+=</c> or <see cref="Subscribe"/>.
    /// </summary>
    public event Action<LogEntry>? OnLog
    {
        add
        {
            if (value == null)
            {
                return;
            }

            lock (_sync)
            {
                _subscribers.Add(value);
            }
        }
        remove
        {
            if (value == null)
            {
                return;
            }

            lock (_sync)
            {
                _subscribers.Remove(value);
            }
        }
    }

    /// <summary>Number of active subscribers.</summary>
    public int SubscriberCount
    {
        get
        {
            lock (_sync)
            {
                return _subscribers.Count;
            }
        }
    }

    /// <summary>
    /// The most recent log entry that used <see cref="LogType.Write"/> (same-line).
    /// Cleared when a <see cref="WriteLine"/> closes the line.
    /// </summary>
    public LogEntry? CurrentLog { get; private set; }

    /// <summary>Subscribe to all log output. Returns a disposable that unsubscribes.</summary>
    public IDisposable Subscribe(Action<LogEntry> handler)
    {
        ArgumentNullException.ThrowIfNull(handler);
        OnLog += handler;
        return new LoggerSubscription(this, handler);
    }

    /// <summary>Remove a subscriber added via <see cref="Subscribe"/> or <c>OnLog +=</c>.</summary>
    public void Unsubscribe(Action<LogEntry> handler)
    {
        if (handler == null)
        {
            return;
        }

        OnLog -= handler;
    }

    /// <summary>Remove every subscriber.</summary>
    public void ClearSubscribers()
    {
        lock (_sync)
        {
            _subscribers.Clear();
        }
    }

    public void Write(string sender, string description, string color = LogColors.White) =>
        Publish(CurrentLog = new LogEntry(sender, description, color, DateTime.Now, LogType.Write));

    public void WriteLine(string sender, string description = "", string color = LogColors.White)
    {
        var entry = new LogEntry(sender, description, color, DateTime.Now, LogType.WriteLine);
        CurrentLog = null;
        Publish(entry);
    }

    public void WriteInfo(string sender, string description) =>
        WriteLine(sender, description, LogColors.Cyan);

    public void WriteWarning(string sender, string description) =>
        WriteLine(sender, description, LogColors.Orange);

    public void WriteError(string sender, string description) =>
        WriteLine(sender, description, LogColors.Red);

    public void WriteException(string sender, Exception ex, string color = LogColors.Red)
    {
        WriteLine(sender, ex.Message, color);
        if (!string.IsNullOrWhiteSpace(ex.StackTrace))
        {
            WriteLine(sender, ex.StackTrace, LogColors.Gray);
        }
    }

    /// <summary>Overwrite-in-place line (progress / status).</summary>
    public void WriteSingleLine(string sender, string description, string color = LogColors.Green) =>
        Publish(new LogEntry(sender, description, color, DateTime.Now, LogType.Progress));

    public void WriteProgress(string sender, string description, string color = LogColors.Green) =>
        WriteSingleLine(sender, description, color);

    /// <summary>
    /// Progress bar (20 blocks): [██████░░░░░░░░░░░░░░]  30% (30 MB / 100 MB)
    /// </summary>
    public void WriteProgress(string sender, long current, long total, string color = LogColors.Green)
    {
        var percent = total > 0 ? (int)(current * 100L / total) : 0;
        var filled = Math.Clamp(percent / 5, 0, 20);
        var bar = new string('\u2588', filled) + new string('\u2591', 20 - filled);
        var size = total > 0
            ? $" ({current / 1_048_576} MB / {total / 1_048_576} MB)"
            : "";
        var description = $"[{bar}] {percent,3}%{size}";

        Publish(new LogEntry(sender, description, color, DateTime.Now, LogType.Progress)
        {
            ProgressCurrent = current,
            ProgressTotal = total,
        });
    }

    private void Publish(LogEntry entry)
    {
        Action<LogEntry>[] snapshot;
        lock (_sync)
        {
            snapshot = _subscribers.ToArray();
        }

        foreach (var handler in snapshot)
        {
            try
            {
                handler(entry);
            }
            catch
            {
                // Subscribers must not break the logging pipeline.
            }
        }
    }
}