最近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/Fatal
IConfiguration configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory()) // 设置配置文件所在目录(通常是程序运行目录)
.AddJsonFile("serilog.json",
optional: false, //指示配置文件是否为 “可选” false(默认):文件不存在时会抛出异常, true:文件不存在时不抛异常,忽略该配置源
reloadOnChange: true) //指示文件修改后是否自动重新加载配置 false(默认):文件修改后不自动刷新
.Build();
// 初始化Serilog
Serilog.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日志组件,日志内容同步输出到窗口显示