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); } }}
Documentation
Logger
Subscribe-based logging hub.
Logger.cs
Logger—Subscribe,WriteLine, and sharedDefaultinstance for agents and tools.