using log4net;
using log4net.Appender;
using log4net.Core;
namespace DeviceService.Logging;
/// 内存缓冲中的一条日志。
public sealed class LogEntry
{
public DateTime Time { get; set; }
public string Level { get; set; } = string.Empty;
public string Logger { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public string Display => $"{Time:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Logger} - {Message}";
public override string ToString() => Display;
}
///
/// 自定义 Appender:只在内存环形缓冲中保留最近 条日志,
/// 通过 与 供日志窗口查看。
/// 文件输出由 log4net.config 中的 RollingFileAppender 负责,本 Appender 不写任何文件。
/// 实例由 log4net 依据 Config/log4net.config 创建(appender name="Memory")。
///
public sealed class MemoryAppender : AppenderSkeleton
{
/// 内存中最多保留的日志条数,可由 log4net.config 的 capacity 参数覆盖。
public int Capacity { get; set; } = 1000;
/// log4net.config 中配置的 MemoryAppender 实例。
public static MemoryAppender Current =>
LogManager.GetRepository(typeof(MemoryAppender).Assembly)
.GetAppenders()
.OfType()
.FirstOrDefault()
?? throw new InvalidOperationException(
"未找到 MemoryAppender,请检查 Config/log4net.config 中是否配置了 name=\"Memory\" 的 appender,并在 root 中引用。");
private readonly object _sync = new object();
private readonly Queue _entries = new Queue();
/// 新日志到达(可能在后台线程触发,UI 订阅方需切换回 UI 线程)。
public event Action? EntryAppended;
/// 当前内存中日志的快照。
public IReadOnlyList Snapshot
{
get
{
lock (_sync)
{
return _entries.ToList();
}
}
}
public MemoryAppender()
{
Name = "Memory";
}
/// 清空内存缓冲(不影响文件)。
public void Clear()
{
lock (_sync)
{
_entries.Clear();
}
}
protected override void Append(LoggingEvent loggingEvent)
{
if (loggingEvent == null)
{
return;
}
string message = loggingEvent.RenderedMessage ?? string.Empty;
if (loggingEvent.ExceptionObject != null)
{
message += Environment.NewLine + loggingEvent.ExceptionObject;
}
var entry = new LogEntry
{
Time = loggingEvent.TimeStamp,
Level = loggingEvent.Level?.Name ?? string.Empty,
Logger = loggingEvent.LoggerName,
Message = message
};
lock (_sync)
{
_entries.Enqueue(entry);
while (_entries.Count > Capacity)
{
_entries.Dequeue();
}
// Monitor 锁可重入,回调内部若调用 Snapshot/Clear 不会死锁
EntryAppended?.Invoke(entry);
}
}
}