C# .Net8 WinFormsApp使用日志Serilog组件

发布于:2025-09-01 ⋅ 阅读:(18) ⋅ 点赞:(0)

  最近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日志组件,日志内容同步输出到窗口显示