V10 / 66cebf3528e0
Logger.cs
Lilith/Core/Logging/Logger.cs
Unchanged from the preceding versionCS
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.
}
}
}
}