找回密码
 立即注册
首页 业界区 业界 Serilog 日志库简单实践(五)数据库 Sinks(.net8) ...

Serilog 日志库简单实践(五)数据库 Sinks(.net8)

睿哝 3 天前
〇、前言

前文已经介绍过什么是 Serilog,以及其核心特点,详见:https://www.cnblogs.com/hnzhengfy/p/19167414/Serilog_basic。
本文继续对各种类型的 Sink 进行简单的实践,主题是数据库 Sinks,供参考。
Serilog 本身不直接内置数据库 Sink,但社区和官方提供了多个高质量的数据库 Sinks。
以下是几种常用且成熟的 Serilog 数据库 Sinks 的简单对比:
Sink数据库类型结构化支持批量写入自动建表适用场景MySQLMySQL(TEXT/JSON)(支持)(支持)Web 应用、轻量部署MSSqlServerSQL Server(JSON 列)(支持)(支持)企业 Windows/.NET 生态PostgreSQLPostgreSQL(JSONB)(支持)(部分支持)开源栈、高级查询ElasticsearchNoSQL(ES)(原生 JSON)(支持)(索引模板)大数据日志分析本文就前两种进行了简单的实践。
一、Serilog.Sinks.MySQL

1.1 简单实现日志写入 MySQL 数据库

在 .NET 生态中,若要将 Serilog 日志写入 MySQL 数据库,通常有以下两种主流方案:

  • 使用 Serilog.Sinks.MariaDB:这是目前社区最推荐的方案。因为 MariaDB 与 MySQL 高度兼容,该包支持通过 MySQL 连接字符串写入 MySQL 数据库。
  • 使用 Serilog.Sinks.ADO.NET:这是一个通用的 ADO.NET 接收器,可以配置为连接 MySQL(需要安装 MySql.Data 或 Pomelo.EntityFrameworkCore.MySql 驱动)。
下面是一个基于 Serilog.Sinks.MariaDB 的完整 .NET8 控制台应用程序示例,这是目前最简单且维护最好的方式。
1)首先要创建数据库和用于存储日志信息的表
  1. CREATE DATABASE IF NOT EXISTS loggingdb;
  2. USE loggingdb;
  3. CREATE TABLE IF NOT EXISTS Logs (
  4.     Id INT AUTO_INCREMENT PRIMARY KEY,
  5.     Message TEXT NULL,
  6.     MessageTemplate TEXT NULL,
  7.     LogLevel VARCHAR(32) NULL,
  8.     TimeStamp DATETIME NOT NULL,
  9.     Exception TEXT NULL,
  10.     Properties TEXT NULL
  11. );
复制代码
2)创建一个 .NET 8 控制台应用,并安装必要的 NuGet 包
  1. # 安装 Serilog 核心包
  2. dotnet add package Serilog
  3. # 安装 MariaDB Sink (它兼容 MySQL)
  4. dotnet add package Serilog.Sinks.MariaDB
  5. # 安装 MySQL 驱动 (Sink 依赖此驱动连接 MySQL)
  6. dotnet add package MySql.Data
复制代码
3)代码实现(Program.cs)
  1. using Serilog;
  2. using Serilog.Sinks.MariaDB;
  3. using Serilog.Sinks.MariaDB.Extensions;
  4. using System;
  5. using System.Threading.Tasks;
  6. // 1. 配置 MySQL 连接字符串
  7. // 请替换为您的实际数据库信息
  8. var connectionString = "Server=localhost;Port=3306;Database=loggingdb;Uid=root;Pwd=1234Zxcv;";
  9. //   关键排查步骤:开启 SelfLog,将内部错误输出到控制台
  10. Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine($"[Serilog Internal Error]: {msg}"));
  11. // 2. 配置 Serilog
  12. Log.Logger = new LoggerConfiguration()
  13.     .MinimumLevel.Information() // 设置最低日志级别
  14.     .Enrich.FromLogContext()    //  enrich 日志上下文
  15.     .WriteTo.Console()          // 同时输出到控制台,方便调试
  16.     .WriteTo.MariaDB(
  17.         connectionString: connectionString,
  18.         tableName: "logs",
  19.         autoCreateTable: true, // 如果表已创建,设为 false;若想让代码自动建表,设为 true
  20.         formatProvider: null,
  21.         restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information
  22.     )
  23.     .CreateLogger();
  24. try
  25. {
  26.     Log.Information("应用程序启动,开始测试 MySQL 日志写入...");
  27.     // 模拟一些业务逻辑和日志记录
  28.     int userId = 1001;
  29.     string userName = "Alice";
  30.     Log.Information("用户 {UserId} ({UserName}) 已登录", userId, userName);
  31.     // 模拟一个警告
  32.     Log.Warning("用户 {UserId} 的操作响应时间超过阈值 ({Threshold}ms)", userId, 200);
  33.     // 模拟一个错误(带异常)
  34.     try
  35.     {
  36.         throw new InvalidOperationException("模拟的数据库连接超时错误");
  37.     }
  38.     catch (Exception ex)
  39.     {
  40.         Log.Error(ex, "处理用户 {UserId} 请求时发生严重错误", userId);
  41.     }
  42.     Log.Information("测试完成,等待日志异步写入数据库...");
  43.     // 稍微等待一下,确保异步日志写入完成(在生产环境中通常不需要手动等待,程序退出时会刷新)
  44.     await Task.Delay(2000);
  45. }
  46. catch (Exception ex)
  47. {
  48.     Log.Fatal(ex, "应用程序意外终止");
  49. }
  50. finally
  51. {
  52.     // 3. 关闭并刷新日志
  53.     await Log.CloseAndFlushAsync();
  54.     Console.WriteLine("日志已刷新到数据库,按任意键退出...");
  55.     Console.ReadKey();
  56. }
复制代码
注意:

  • autoCreateTable 参数:如果设置为 true,Sink 会在首次运行时尝试根据默认架构创建表。但在生产环境中,建议手动执行 SQL 创建表(如上文“前置准备”所示),以便精确控制字段类型、索引和字符集(推荐 utf8mb4)。
  • 异步刷新(CloseAndFlushAsync):Serilog 的数据库写入通常是异步批处理的。在控制台应用程序结束前,必须调用 Log.CloseAndFlushAsync(),否则最后几条日志可能还在内存缓冲区中,未写入数据库程序就退出了。
  • 结构化日志:注意代码中的 Log.Information("用户 {UserId} ({UserName}) 已登录", userId, userName);。Serilog 会将 UserId 和 UserName 作为单独的列(存储在 Properties 字段的 JSON 中)保存,而不是简单的字符串拼接。这使得后续在数据库中查询特定用户的日志变得非常容易。
4)最后运行程序,查看运行结果
如下图为成功的结果:
1.png

数据库中的数据:
2.png
 
1.2 问题处理:代码运行没有看到报错,但是日志信息未写入

1)最常见原因:autoCreateTable: false 但表不存在或结构不匹配
若在代码中设置了 autoCreateTable: false。这意味着 Serilog 不会尝试创建表,也不会检查表结构是否正确。
如果表不存在,或者表中的列名与 Serilog 预期的不一致(例如列名大小写敏感、缺少 Message 列等),写入操作会在内部静默失败(或者抛出异常被吞掉),导致没有数据。
解决方法:

  • 确认表是否存在:登录 MySQL,执行 SHOW TABLES; 查看是否有 Logs 表。
  • 验证表结构:Serilog.Sinks.MariaDB 对列名有严格要求(默认区分大小写,取决于 MySQL 配置,但通常建议完全匹配)。
2)内部异常被吞没(Silent Failure)
Serilog 的默认行为是“尽力而为”,如果写入数据库失败,它通常只会在内部记录一个 SelfLog 消息,而不会抛出异常中断主程序
关键排查步骤:开启 SelfLog。
在 Main 函数的第一行(配置 Logger 之前)加入以下代码,将内部错误输出到控制台:
  1. // 在 new LoggerConfiguration() 之前添加
  2. Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine($"[Serilog Internal Error]: {msg}"));
复制代码
重新运行程序,观察控制台输出。如果看到类似 Failed to emit a log event... 或 MySqlException: Table 'loggingdb.Logs' doesn't exist 的错误,就能直接定位问题。如果看到 Connection error...,则是网络或账号密码问题。
如下图,表中字段名不匹配的错误提示:
3.png

二、Serilog.Sinks.MSSqlServer

2.1 简介

Serilog.Sinks.MSSqlServer 是 Serilog 的一个官方支持的接收器(Sink),它允许将结构化日志直接写入 Microsoft SQL Server 数据库中的指定表。该 Sink 支持 .NET 6+(包括 .NET 8),并提供丰富的配置选项,如自定义列、批量写入、自动建表、使用连接字符串等。
核心特点:

  • 允许自动创建日志表:若目标表不存在,可自动创建默认结构。
  • 自定义列映射:可以将日志属性(如 Level、Message、Timestamp、Exception 等)映射到数据库列。
  • 支持结构化日志字段:通过 LogEvent 的 Properties 自动序列化为 JSON 或拆分为独立列。
  • 批量写入(Batching):提高性能,减少数据库连接次数。
  • 支持异步写入:避免阻塞主线程。
  • 兼容 Azure SQL Database 和本地 SQL Server。
2.2 简单示例:将日志信息写入本地的 MSSQL

1)添加必要的包:
  1. dotnet add package Serilog
  2. dotnet add package Serilog.Sinks.Console
  3. dotnet add package Microsoft.Extensions.Hosting
  4. dotnet add package Serilog.Extensions.Hosting
  5. dotnet add package Serilog.Sinks.MSSqlServer
复制代码
4.png

2)修改 Program.cs
  1. using Microsoft.Extensions.DependencyInjection;
  2. using Microsoft.Extensions.Hosting;
  3. using Microsoft.Extensions.Logging;
  4. using Serilog;
  5. using Serilog.Context;
  6. using Serilog.Events;
  7. using Serilog.Sinks.MSSqlServer;
  8. using System.Data;
  9. var builder = Host.CreateApplicationBuilder(args);
  10. // 配置 Serilog
  11. Log.Logger = new LoggerConfiguration()
  12.     .MinimumLevel.Debug()
  13.     .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
  14.     .Enrich.FromLogContext()
  15.     .Enrich.WithProperty("MachineName", Environment.MachineName) // 全部日志记录都带上参数:MachineName
  16.     // 注意:.Enrich.WithProperty(...) 是一个一次性 enricher,适用于静态值
  17.     // 如果需要动态或更复杂的 enricher,可以实现 ILogEventEnricher
  18.     .WriteTo.Console() // 可选:同时输出到控制台
  19.     .WriteTo.MSSqlServer(
  20.         connectionString: "Server=localhost;Database=SerilogDemo;Trusted_Connection=True;TrustServerCertificate=True;",
  21.         sinkOptions: new Serilog.Sinks.MSSqlServer.MSSqlServerSinkOptions
  22.         {
  23.             TableName = "Logs",
  24.             AutoCreateSqlTable = true // 自动建表(首次运行时)
  25.         },
  26.        columnOptions: new ColumnOptions
  27.        {
  28.            TimeStamp = { ConvertToUtc = true },
  29.            AdditionalColumns = new List<SqlColumn> // 自定义列,存储当前机器名
  30.             {
  31.                 new SqlColumn
  32.                 {
  33.                     DataType = SqlDbType.NVarChar,
  34.                     DataLength = 255, // 对于字符串类型,建议指定长度
  35.                     ColumnName = "MachineName"
  36.                 }
  37.             },
  38.        })
  39.     .CreateLogger();
  40. builder.Services.AddLogging(loggingBuilder =>
  41. {
  42.     loggingBuilder.ClearProviders();
  43.     loggingBuilder.AddSerilog(dispose: true);
  44. });
  45. var host = builder.Build();
  46. // 示例日志
  47. var logger = host.Services.GetRequiredService<ILogger<Program>>();
  48. logger.LogInformation("Test {@User}", new { Name = "Alice" });
  49. logger.LogInformation("应用程序启动");
  50. using (LogContext.PushProperty("MachineName", Environment.MachineName))
  51. {
  52.     logger.LogWarning("这是一条带自定义属性的日志");
  53. }
  54. try
  55. {
  56.     throw new InvalidOperationException("测试异常日志");
  57. }
  58. catch (Exception ex)
  59. {
  60.     logger.LogError(ex, "发生了一个异常");
  61. }
  62. logger.LogInformation("应用程序结束");
  63. host.RunAsync().Wait();
复制代码
3)在启动测试项目之前,需要先手动创建数据库
CREATE DATABASE SerilogDemo;
5.png

最后启动项目,日志信息就可以自动写入到数据库,并且数据库的表 Log,在程序首次启动时自动创建的。
6.png

注意:Log 表中的 Properties 字段默认输出为 XML,博主尝试了更换 Serilog.Sinks.MSSqlServer 的版本和写入到比较新的 MSSQL2022 版本,依然没打输出 json 格式,有大佬知道原因的烦请留言。
2.3 其他用法

使用 Serilog.Sinks.MSSqlServer 将日志写入 Microsoft SQL Server 数据库,除了基础的自动建表和默认字段写入外,还支持多种高级用法,可满足企业级日志管理、结构化分析、性能优化等需求。
2.3.1 自定义数据表列

排除标准列(Excluding Standard Columns):默认情况下,Sink 会创建一系列标准列。如果你不需要某些列(例如 MessageTemplate 或 Properties 的 XML/JSON 列),可以将其从 Store 集合中移除,以节省存储空间和提高写入性能。
  1. var columnOptions = new ColumnOptions();
  2. columnOptions.Store.Remove(StandardColumn.MessageTemplate);
  3. columnOptions.Store.Remove(StandardColumn.Properties);
  4. // 甚至可以去掉 Id,如果不需要自增主键
  5. // columnOptions.Store.Remove(StandardColumn.Id);
复制代码
添加自定义列(Adding Custom Columns):你可以将日志事件中的特定属性(Properties)提升到独立的数据库列中。这对于需要频繁查询、索引或聚合的字段(如 UserId, OrderId, TenantId, RequestId)非常有用,避免了每次都去解析 JSON/XML 属性列。如下代码是基于上一章节的示例代码进行的优化,新增了 UserName、RequestId 两列:
  1. using Microsoft.Extensions.DependencyInjection;
  2. using Microsoft.Extensions.Hosting;
  3. using Microsoft.Extensions.Logging;
  4. using Serilog;
  5. using Serilog.Context;
  6. using Serilog.Events;
  7. using Serilog.Sinks.MSSqlServer;
  8. using System.Data;
  9. var builder = Host.CreateApplicationBuilder(args);
  10. // 配置 Serilog
  11. Log.Logger = new LoggerConfiguration()
  12.     .MinimumLevel.Debug()
  13.     .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
  14.     .Enrich.FromLogContext()
  15.     .Enrich.WithProperty("MachineName", Environment.MachineName) // 全部日志记录都带上参数:MachineName
  16.     // 注意:.Enrich.WithProperty(...) 是一个一次性 enricher,适用于静态值
  17.     // 如果需要动态或更复杂的 enricher,可以实现 ILogEventEnricher
  18.     .WriteTo.Console() // 可选:同时输出到控制台
  19.     .WriteTo.MSSqlServer(
  20.         connectionString: "Server=localhost;Database=SerilogDemo;Trusted_Connection=True;TrustServerCertificate=True;",
  21.         sinkOptions: new Serilog.Sinks.MSSqlServer.MSSqlServerSinkOptions
  22.         {
  23.             TableName = "Logs",
  24.             AutoCreateSqlTable = true // 自动建表(首次运行时)
  25.         },
  26.        columnOptions: new ColumnOptions
  27.        {
  28.            TimeStamp = { ConvertToUtc = true },
  29.            AdditionalColumns = new List<SqlColumn> // 自定义列,存储当前机器名
  30.             {
  31.                 new SqlColumn
  32.                 {
  33.                     DataType = SqlDbType.NVarChar,
  34.                     DataLength = 255, // 对于字符串类型,建议指定长度
  35.                     ColumnName = "MachineName"
  36.                 },
  37.                 new SqlColumn
  38.                 {
  39.                     DataType = SqlDbType.NVarChar,
  40.                     DataLength = 100,
  41.                     ColumnName = "UserName", // 新增一个动态列:用户名
  42.                     AllowNull = true
  43.                 },
  44.                 new SqlColumn
  45.                 {
  46.                     DataType = SqlDbType.NVarChar,
  47.                     DataLength = 50,
  48.                     ColumnName = "RequestId", // 新增一个动态列:请求 ID
  49.                     AllowNull = true
  50.                 }
  51.             },
  52.        })
  53.     .CreateLogger();
  54. builder.Services.AddLogging(loggingBuilder =>
  55. {
  56.     loggingBuilder.ClearProviders();
  57.     loggingBuilder.AddSerilog(dispose: true);
  58. });
  59. var host = builder.Build();
  60. // 示例日志
  61. var logger = host.Services.GetRequiredService<ILogger<Program>>();
  62. logger.LogInformation("Test {@User}", new { Name = "Alice" });
  63. logger.LogInformation("应用程序启动");
  64. using (LogContext.PushProperty("MachineName", Environment.MachineName))
  65. {
  66.     logger.LogWarning("这是一条带自定义属性的日志");
  67. }
  68. // 测试写入自定义列的内容
  69. string currentUserId = "User_Alice_88";
  70. string currentRequestId = Guid.NewGuid().ToString().Substring(0, 8);
  71. using (LogContext.PushProperty("UserName", currentUserId))
  72. using (LogContext.PushProperty("RequestId", currentRequestId))
  73. {
  74.     logger.LogInformation("2. 嵌套作用域:当前用户 {UserName} 的操作,请求ID {RequestId}", currentUserId, currentRequestId);
  75.     // 即使不显式在消息模板中写 {UserName},只要 LogContext 里有,且列名匹配,就会存入数据库
  76.     logger.LogWarning("3. 警告日志:自动捕获上下文中的 UserName 和 RequestId");
  77. }
  78. try
  79. {
  80.     throw new InvalidOperationException("测试异常日志");
  81. }
  82. catch (Exception ex)
  83. {
  84.     logger.LogError(ex, "发生了一个异常");
  85. }
  86. logger.LogInformation("应用程序结束");
  87. host.RunAsync().Wait();
复制代码
运行代码,用户名和请求 ID 就会写入数据库,如下图:
7.png

更改属性列的存储格式(Properties Column Format):默认的 Properties 列通常存储为 XML 或 NVARCHAR(MAX) 格式的 JSON。可以配置它存储为 SQL Server 的原生 JSON 类型(如果数据库版本支持),或者调整其长度和名称。
  1. columnOptions.Properties.ColumnName = "LogContext";
  2. columnOptions.Properties.DataType = SqlDbType.NVarChar;
  3. columnOptions.Properties.DataLength = 2048; // 限制长度,防止过大
  4. // 注意:若要利用 SQL Server 的 JSON 函数,通常只需确保存储的是有效 JSON 字符串即可
复制代码
配置主键和索引(Primary Keys and Indexes):虽然 Sink 主要负责写入,但你可以通过 ColumnOptions 指定哪一列作为主键(默认是 Id),并在建表时自动创建。对于高性能查询,通常建议在数据库层面手动为常用的自定义列(如 UserId 或 TimeStamp)添加索引。
2.3.2 性能优化(Performance Optimization)

批量写入(Batch Posting):为了减少数据库连接开销和事务日志压力,可以配置批量写入。设置 BatchPostingLimit(每次批处理的日志数量)和 Period(触发批处理的时间间隔)。
  1. var sinkOptions = new MSSqlServerSinkOptions()
  2. {
  3.     TableName = "Logs",
  4.     BatchPostingLimit = 100, // 每 100 条提交一次
  5.     Period = TimeSpan.FromSeconds(5) // 或每 5 秒提交一次
  6. };
  7. Log.Logger = new LoggerConfiguration()
  8.     .WriteTo.MSSqlServer(
  9.         connectionString: "...",
  10.         sinkOptions: sinkOptions,
  11.         columnOptions: columnOptions)
  12.     .CreateLogger();
复制代码
异步写入(Asynchronous Logging):虽然 MSSqlServer Sink 本身有一定的缓冲机制,但在高并发场景下,推荐结合 Serilog.Sinks.Async 包使用。它将日志写入操作封装在一个后台队列中,由独立线程异步执行,彻底避免日志 I/O 阻塞主业务线程。
  1. // 需要安装 Serilog.Sinks.Async
  2. Log.Logger = new LoggerConfiguration()
  3.     .WriteTo.Async(a => a.MSSqlServer(
  4.         connectionString: "...",
  5.         sinkOptions: sinkOptions,
  6.         columnOptions: columnOptions
  7.     ))
  8.     .CreateLogger();
复制代码
禁用自动建表(Disable Auto Table Creation):在生产环境中,通常建议手动创建表并精确控制索引、分区和文件组,而不是依赖 Sink 的自动建表功能。可以通过设置 AutoCreateSqlTable = false 来禁用此功能。
  1. var sinkOptions = new MSSqlServerSinkOptions()
  2. {
  3.     AutoCreateSqlTable = false
  4. };
复制代码
2.3.3 结构化与上下文增强 (Structuring & Context)

丰富日志上下文(Enrichers):结合 Serilog.Enrichers.Environment 或其他自定义 Enricher,自动向每条日志添加机器名、进程ID、用户信息、请求ID等上下文数据。这些数据可以被映射到上述的“自定义列”中。
  1. // 自动添加 MachineName, UserName 等
  2. .Enrich.FromLogContext()
  3. .Enrich.WithMachineName()
  4. .Enrich.WithThreadId()
复制代码
结构化对象日志:Serilog 的核心优势是结构化日志。在记录对象时(如 Log.Information("User {@User} logged in", userObj)),对象会被序列化为 JSON 存入 Properties 列或拆分到自定义列。这使得在 SQL Server 中使用 OPENJSON 或 value() 方法进行复杂查询成为可能。
2.3.4 安全与连接管理(Security & Connection)

使用托管标识或集成认证:在 Azure 环境或域环境中,可以使用 Windows 身份验证或 Managed Identity,避免在连接字符串中硬编码密码。
  1. // 连接字符串示例 (Integrated Security)
  2. "Server=...;Database=...;Integrated Security=true;"
复制代码
自定义 SQL 客户端配置:可以通过 SqlConnection 的高级设置(如 Connect Timeout, Encrypt 等)来增强连接的安全性和稳定性。
2.3.5 故障转移与可靠性(Reliability)

备用 Sink(Fallback Sink):如果数据库不可用,日志不应导致应用程序崩溃。可以配置一个备用 Sink(如文件或控制台),当 MSSQL Server 写入失败时自动切换。
  1. // 伪代码概念,实际需使用 Fallback 包装器或自定义逻辑
  2. .WriteTo.MSSqlServer(...)
  3. .WriteTo.File("logs/fallback-.txt") // 作为备份
复制代码
注意:Serilog 核心库不直接提供自动故障转移包装器给特定 Sink,通常通过 Serilog.Sinks.Map 或自定义 ILogEventSink 实现,或者简单地同时写入文件和数据库。
通过这些高级用法,Serilog.Sinks.MSSqlServer 可以从一个简单的日志记录工具转变为一个强大的企业级数据收集组件,支持高效的审计、监控和数据分析。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册