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); } } }