Logging/Logger/Logger.cscsharp

Documentation

Logger

Subscribe-based logging hub.

Logger.cs

  • LoggerSubscribe, WriteLine, and shared Default instance for agents and tools.
namespace Agent.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        ? new List<Action<LogEntry>>()        : new List<Action<LogEntry>> { 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.            }        }    }    private sealed class LoggerSubscription(Logger logger, Action<LogEntry> handler) : IDisposable    {        private int _disposed;        public void Dispose()        {            if (Interlocked.Exchange(ref _disposed, 1) == 1)            {                return;            }            logger.Unsubscribe(handler);        }    }}