C# .Net8 WinFormsApp使用日志Serilog组件
最近C# .Net8 WinFormsApp使用日志Serilog组件,找了一堆的资料,发现坑太多,记录下来方便以后参考。
使用Serilog特别需要注意的事,它是一个组件系列,不同接口方法需要不同的包,没有添加正确,编译时就会报错。
我的需求是,在Winform界面中,使用Serilog组件输出日志,但日志同时需要显示在窗体中,方便运行监控。根据豆包推荐,使用TextBox替换RichBox,配置Multiline=true,ScrollBars=Both,可以加快显示速度。
本例程中,需要使用的包如下:
<ItemGroup><None Update="serilog.json"><CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory></None><PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.8" /><PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.8" /><PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.8" /><PackageReference Include="Serilog" Version="4.3.0" /><PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" /><PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" /><PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" /><PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" /><PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" /><PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /><PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" /><PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /><PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /><PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" /><PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.8" /></ItemGroup>
配置文件使用serilog.json,较新则覆盖,内容如下:
{"Serilog": {"MinimumLevel": {"Default": "Information","Override": {"Microsoft": "Warning","System": "Warning"}},"WriteTo": [{"Name": "Console","Args": {"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}","restrictedToMinimumLevel": "Information"}},{"Name": "Debug","Args": {"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level}] {Message}{NewLine}{Exception}"}},{"Name": "File","Args": {"path": "logs\\log-.txt","outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}","restrictedToMinimumLevel": "Information","encoding": "System.Text.UTF8Encoding, System.Private.CoreLib","rollingInterval": "Day","fileSizeLimitBytes": 10485760,"retainedFileCountLimit": 30,"rollOnFileSizeLimit": true}}],"Enrich": ["FromLogContext","WithMachineName","WithThreadId"]}
}
日志输出到队列,需要按Serilog要求定义数据结构,使用文件QueueLogSink.cs,完整代码如下:
using Serilog;
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Events;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;namespace WinFormsAppSerilog;/// <summary>
/// 封装Serilog日志事件的对象类
/// </summary>
public class LogItem
{/// <summary>/// 日志时间戳(UTC时间)/// </summary>public DateTime Timestamp { get; set; }/// <summary>/// 日志级别(Verbose/Debug/Information/Warning/Error/Fatal)/// </summary>public LogEventLevel Level { get; set; }/// <summary>/// 日志消息(已渲染的文本)/// </summary>public string Message { get; set; }/// <summary>/// 日志消息模板(原始模板,如"User {UserId} logged in")/// </summary>public string MessageTemplate { get; set; }/// <summary>/// 异常信息(若有)/// </summary>public Exception Exception { get; set; }/// <summary>/// 日志属性(如上下文信息、自定义属性等)/// </summary>public object Properties { get; set; }/// <summary>/// 从Serilog的LogEvent转换为LogItem对象/// </summary>/// <param name="logEvent">Serilog日志事件</param>/// <returns>封装后的日志对象</returns>public static LogItem FromLogEvent(LogEvent logEvent){return new LogItem{Timestamp = logEvent.Timestamp.UtcDateTime,Level = logEvent.Level,Message = logEvent.RenderMessage(), // 渲染消息模板为实际文本MessageTemplate = logEvent.MessageTemplate.Text,Exception = logEvent.Exception,Properties = logEvent.Properties.ToDictionary(p => p.Key,p => p.Value.ToString() // 将属性值转换为字符串(可根据需求扩展))};}
}
/// <summary>
/// 日志队列管理器,用于存储和管理LogItem对象
/// </summary>
public class LogQueueManager
{// 线程安全的队列,用于存储日志对象private readonly ConcurrentQueue<LogItem> _logQueue = new ConcurrentQueue<LogItem>();/// <summary>/// 队列中当前日志数量/// </summary>public int Count => _logQueue.Count;/// <summary>/// 将日志对象加入队列/// </summary>/// <param name="logItem">日志对象</param>public void Enqueue(LogItem logItem){if (logItem != null){_logQueue.Enqueue(logItem);}}/// <summary>/// 从队列中取出一个日志对象(出队)/// </summary>/// <param name="logItem">取出的日志对象</param>/// <returns>是否成功取出(队列非空则成功)</returns>public bool TryDequeue(out LogItem logItem){return _logQueue.TryDequeue(out logItem);}/// <summary>/// 批量取出队列中的日志对象(出队)/// </summary>/// <param name="maxCount">最大取出数量</param>/// <returns>取出的日志对象列表</returns>public List<LogItem> DequeueBatch(int maxCount){var batch = new List<LogItem>();for (int i = 0; i < maxCount; i++){if (_logQueue.TryDequeue(out var item)){batch.Add(item);}else{break; // 队列已空,停止取数}}return batch;}/// <summary>/// 清空队列/// </summary>public void Clear(){while (_logQueue.TryDequeue(out _)) { }}
}/// <summary>
/// Serilog自定义Sink,将日志事件输出到LogQueueManager队列
/// </summary>
public class QueueLogSink : ILogEventSink
{private readonly LogQueueManager _logQueue;/// <summary>/// 初始化Sink,关联日志队列/// </summary>/// <param name="logQueue">日志队列管理器</param>public QueueLogSink(LogQueueManager logQueue){_logQueue = logQueue;}/// <summary>/// 处理Serilog输出的日志事件,转换后写入队列/// </summary>/// <param name="logEvent">Serilog日志事件</param>public void Emit(LogEvent logEvent){// 将LogEvent转换为LogItem并加入队列var logItem = LogItem.FromLogEvent(logEvent);_logQueue.Enqueue(logItem);}
}
public static class QueueLogSinkExtensions
{/// <summary>/// 向Serilog添加队列Sink/// </summary>/// <param name="sinkConfiguration">Serilog Sink配置</param>/// <param name="logQueue">日志队列管理器</param>/// <returns>配置对象(链式调用)</returns>public static LoggerConfiguration Queue(this LoggerSinkConfiguration sinkConfiguration,LogQueueManager logQueue){return sinkConfiguration.Sink(new QueueLogSink(logQueue));}
}
界面样式如下:
代码如下:
using Microsoft.Extensions.Configuration;
using Serilog;namespace WinFormsAppSerilog
{public partial class Form1 : Form{LogQueueManager logQueue = new LogQueueManager();// 设定最大行数(根据需求调整,如10000行)private const int MaxLineCount = 20_000;// 每次超出时删除的行数(避免频繁操作UI)private const int LinesToRemoveWhenOver = 1000;private Int64 TotalLogCount = 0;public Form1(){InitializeComponent();}void InitSerilog(){//日志级别:Serilog 提供了多个日志级别,从低到高依次为:Verbose/Debug/Information/Warning/Error/FatalIConfiguration configuration = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()) // 设置配置文件所在目录(通常是程序运行目录).AddJsonFile("serilog.json",optional: false, //指示配置文件是否为 “可选” false(默认):文件不存在时会抛出异常, true:文件不存在时不抛异常,忽略该配置源reloadOnChange: true) //指示文件修改后是否自动重新加载配置 false(默认):文件修改后不自动刷新.Build();// 初始化SerilogSerilog.Log.Logger = new LoggerConfiguration().ReadFrom.Configuration(configuration) // 从配置对象读取Serilog设置.WriteTo.Queue(logQueue) // 将日志写入队列.CreateLogger();}/// <summary>/// TextBox 删除指定范围的行/// </summary>/// <param name="textBox">目标 TextBox 控件</param>/// <param name="startLineIndex">起始行索引(0 开始,包含此行列)</param>/// <param name="endLineIndex">结束行索引(0 开始,包含此行列)</param>/// <exception cref="ArgumentOutOfRangeException">行索引越界时抛出</exception>private static void DeleteLineRange(TextBox textBox, int startLineIndex, int endLineIndex){// 1. 基础校验(TextBox 未初始化或无文本)if (textBox == null || string.IsNullOrEmpty(textBox.Text))return;// 2. 验证行范围有效性(WPF 用 LineCount 获取总行数)int totalLines = textBox.Lines.Length;if (startLineIndex < 0 || endLineIndex >= totalLines || startLineIndex > endLineIndex){throw new ArgumentOutOfRangeException(message: $"行索引范围无效!当前总行数:{totalLines},请确保 0 ≤ 起始行 ≤ 结束行 < {totalLines}",paramName: startLineIndex > endLineIndex ? nameof(startLineIndex) : nameof(endLineIndex));}// 3. 计算删除范围的字符索引(关键步骤)int endCharIndex = 0; // 结束行最后一个字符的索引(含换行符)// 3.1 计算起始行的起始索引(累加前 startLineIndex 行的长度 + 换行符长度)for (int i = startLineIndex; i < endLineIndex; i++){string lineText = textBox.Lines[i];endCharIndex += lineText.Length + Environment.NewLine.Length;}// 如果不是最后一行,需要包含结束行的换行符(最后一行没有换行符)if (endLineIndex < totalLines - 1){endCharIndex += Environment.NewLine.Length;}// 4. 执行删除(WPF 同样通过 Selection 操作)textBox.SelectionStart = 0;textBox.SelectionLength = endCharIndex - 2;textBox.SelectedText = "";}private void Form1_Load(object sender, EventArgs e){InitSerilog();timer1.Enabled = true;Serilog.Log.Information("软件启动完成!");}private void Form1_FormClosing(object sender, FormClosingEventArgs e){timer1.Enabled = false;Serilog.Log.CloseAndFlush(); // 关闭日志并刷新到文件}private void timer1_Tick(object sender, EventArgs e){try{var logItems = logQueue.DequeueBatch(50);TotalLogCount += logItems.Count;foreach (var item in logItems){textBox1.AppendText($"[{item.Timestamp.ToLocalTime():yyyy-MM-dd HH:mm:ss.fff}] {item.Level}: {item.Message}" + Environment.NewLine);}// 检查是否超出最大行数,超出则删除旧内容if (textBox1.Lines.Length > MaxLineCount){DeleteLineRange(textBox1, 0, LinesToRemoveWhenOver);}// 自动滚动到最新内容textBox1.ScrollToCaret();label1.Text = $"总日志数: {TotalLogCount}, 显示日志数: {textBox1.Lines.Length}, 当前队列数: {logItems.Count}";}catch (Exception ex){label1.Text = $"发生错误: {ex.Message}";}}private void button1_Click(object sender, EventArgs e){//测试日志输出(验证配置是否生效)Serilog.Log.Verbose("Verbose 级别(仅调试窗口可见)");Serilog.Log.Debug("Debug 级别(仅调试窗口可见)");Serilog.Log.Information("Information 级别(控制台+调试窗口可见)");Serilog.Log.Warning("Warning 级别(控制台+调试窗口+文件可见)");Serilog.Log.Error(new Exception("测试异常"), "Error 级别(控制台+调试窗口+文件可见)");Serilog.Log.Fatal("Fatal 级别(控制台+调试窗口+文件可见)");}}
}
运行效果:
完整代码托管地址:GitHub - PascalMing/WinFormsAppSerilog: .Net9 WinFrom下使用Serilog日志组件,日志内容同步输出到窗口显示