数据库 Sinks(.net8)

一、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)首先要创建数据库和用于存储日志信息的表

复制代码

|---|--------------------------------------------|
| | CREATE DATABASE IF NOT EXISTS loggingdb; |
| | USE loggingdb; |
| | |
| | CREATE TABLE IF NOT EXISTS Logs ( |
| | Id INT AUTO_INCREMENT PRIMARY KEY, |
| | Message TEXT NULL, |
| | MessageTemplate TEXT NULL, |
| | LogLevel VARCHAR(32) NULL, |
| | TimeStamp DATETIME NOT NULL, |
| | Exception TEXT NULL, |
| | Properties TEXT NULL |
| | ); |

2)创建一个 .NET 8 控制台应用,并安装必要的 NuGet 包

复制代码

|---|--------------------------------------------|
| | # 安装 Serilog 核心包 |
| | dotnet add package Serilog |
| | # 安装 MariaDB Sink (它兼容 MySQL) |
| | dotnet add package Serilog.Sinks.MariaDB |
| | # 安装 MySQL 驱动 (Sink 依赖此驱动连接 MySQL) |
| | dotnet add package MySql.Data |

3)代码实现(Program.cs)

复制代码

|---|---------------------------------------------------------------------------------------------------|
| | using Serilog; |
| | using Serilog.Sinks.MariaDB; |
| | using Serilog.Sinks.MariaDB.Extensions; |
| | using System; |
| | using System.Threading.Tasks; |
| | |
| | // 1. 配置 MySQL 连接字符串 |
| | // 请替换为您的实际数据库信息 |
| | var connectionString = "Server=localhost;Port=3306;Database=loggingdb;Uid=root;Pwd=1234Zxcv;"; |
| | // 关键排查步骤:开启 SelfLog,将内部错误输出到控制台 |
| | Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine($"[Serilog Internal Error]: {msg}")); |
| | // 2. 配置 Serilog |
| | Log.Logger = new LoggerConfiguration() |
| | .MinimumLevel.Information() // 设置最低日志级别 |
| | .Enrich.FromLogContext() // enrich 日志上下文 |
| | .WriteTo.Console() // 同时输出到控制台,方便调试 |
| | .WriteTo.MariaDB( |
| | connectionString: connectionString, |
| | tableName: "logs", |
| | autoCreateTable: true, // 如果表已创建,设为 false;若想让代码自动建表,设为 true |
| | formatProvider: null, |
| | restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information |
| | ) |
| | .CreateLogger(); |
| | try |
| | { |
| | Log.Information("应用程序启动,开始测试 MySQL 日志写入..."); |
| | // 模拟一些业务逻辑和日志记录 |
| | int userId = 1001; |
| | string userName = "Alice"; |
| | Log.Information("用户 {UserId} ({UserName}) 已登录", userId, userName); |
| | // 模拟一个警告 |
| | Log.Warning("用户 {UserId} 的操作响应时间超过阈值 ({Threshold}ms)", userId, 200); |
| | // 模拟一个错误(带异常) |
| | try |
| | { |
| | throw new InvalidOperationException("模拟的数据库连接超时错误"); |
| | } |
| | catch (Exception ex) |
| | { |
| | Log.Error(ex, "处理用户 {UserId} 请求时发生严重错误", userId); |
| | } |
| | Log.Information("测试完成,等待日志异步写入数据库..."); |
| | // 稍微等待一下,确保异步日志写入完成(在生产环境中通常不需要手动等待,程序退出时会刷新) |
| | await Task.Delay(2000); |
| | } |
| | catch (Exception ex) |
| | { |
| | Log.Fatal(ex, "应用程序意外终止"); |
| | } |
| | finally |
| | { |
| | // 3. 关闭并刷新日志 |
| | await Log.CloseAndFlushAsync(); |
| | Console.WriteLine("日志已刷新到数据库,按任意键退出..."); |
| | Console.ReadKey(); |
| | } |

注意:

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

4)最后运行程序,查看运行结果

如下图为成功的结果:

数据库中的数据:

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 之前)加入以下代码,将内部错误输出到控制台:

复制代码

|---|---------------------------------------------------------------------------------------------------|
| | // 在 new LoggerConfiguration() 之前添加 |
| | 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...,则是网络或账号密码问题。

如下图,表中字段名不匹配的错误提示:

回到顶部

二、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)添加必要的包:

复制代码

|---|---------------------------------------------------|
| | dotnet add package Serilog |
| | dotnet add package Serilog.Sinks.Console |
| | dotnet add package Microsoft.Extensions.Hosting |
| | dotnet add package Serilog.Extensions.Hosting |
| | dotnet add package Serilog.Sinks.MSSqlServer |

2)修改 Program.cs

复制代码

|---|-------------------------------------------------------------------------------------------------------------------|
| | using Microsoft.Extensions.DependencyInjection; |
| | using Microsoft.Extensions.Hosting; |
| | using Microsoft.Extensions.Logging; |
| | using Serilog; |
| | using Serilog.Context; |
| | using Serilog.Events; |
| | using Serilog.Sinks.MSSqlServer; |
| | using System.Data; |
| | |
| | var builder = Host.CreateApplicationBuilder(args); |
| | |
| | // 配置 Serilog |
| | Log.Logger = new LoggerConfiguration() |
| | .MinimumLevel.Debug() |
| | .MinimumLevel.Override("Microsoft", LogEventLevel.Information) |
| | .Enrich.FromLogContext() |
| | .Enrich.WithProperty("MachineName", Environment.MachineName) // 全部日志记录都带上参数:MachineName |
| | // 注意:.Enrich.WithProperty(...) 是一个一次性 enricher,适用于静态值 |
| | // 如果需要动态或更复杂的 enricher,可以实现 ILogEventEnricher |
| | .WriteTo.Console() // 可选:同时输出到控制台 |
| | .WriteTo.MSSqlServer( |
| | connectionString: "Server=localhost;Database=SerilogDemo;Trusted_Connection=True;TrustServerCertificate=True;", |
| | sinkOptions: new Serilog.Sinks.MSSqlServer.MSSqlServerSinkOptions |
| | { |
| | TableName = "Logs", |
| | AutoCreateSqlTable = true // 自动建表(首次运行时) |
| | }, |
| | columnOptions: new ColumnOptions |
| | { |
| | TimeStamp = { ConvertToUtc = true }, |
| | AdditionalColumns = new List<SqlColumn> // 自定义列,存储当前机器名 |
| | { |
| | new SqlColumn |
| | { |
| | DataType = SqlDbType.NVarChar, |
| | DataLength = 255, // 对于字符串类型,建议指定长度 |
| | ColumnName = "MachineName" |
| | } |
| | }, |
| | }) |
| | .CreateLogger(); |
| | |
| | builder.Services.AddLogging(loggingBuilder => |
| | { |
| | loggingBuilder.ClearProviders(); |
| | loggingBuilder.AddSerilog(dispose: true); |
| | }); |
| | var host = builder.Build(); |
| | |
| | // 示例日志 |
| | var logger = host.Services.GetRequiredService<ILogger<Program>>(); |
| | logger.LogInformation("Test {@User}", new { Name = "Alice" }); |
| | logger.LogInformation("应用程序启动"); |
| | using (LogContext.PushProperty("MachineName", Environment.MachineName)) |
| | { |
| | logger.LogWarning("这是一条带自定义属性的日志"); |
| | } |
| | try |
| | { |
| | throw new InvalidOperationException("测试异常日志"); |
| | } |
| | catch (Exception ex) |
| | { |
| | logger.LogError(ex, "发生了一个异常"); |
| | } |
| | logger.LogInformation("应用程序结束"); |
| | host.RunAsync().Wait(); |

3)在启动测试项目之前,需要先手动创建数据库

CREATE DATABASE SerilogDemo;

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

注意: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 集合中移除,以节省存储空间和提高写入性能。

复制代码

|---|---------------------------------------------------------------|
| | var columnOptions = new ColumnOptions(); |
| | columnOptions.Store.Remove(StandardColumn.MessageTemplate); |
| | columnOptions.Store.Remove(StandardColumn.Properties); |
| | // 甚至可以去掉 Id,如果不需要自增主键 |
| | // columnOptions.Store.Remove(StandardColumn.Id); |

**添加自定义列(Adding Custom Columns):**你可以将日志事件中的特定属性(Properties)提升到独立的数据库列中。这对于需要频繁查询、索引或聚合的字段(如 UserId, OrderId, TenantId, RequestId)非常有用,避免了每次都去解析 JSON/XML 属性列。如下代码是基于上一章节的示例代码进行的优化,新增了 UserName、RequestId 两列:

复制代码

|---|-------------------------------------------------------------------------------------------------------------------|
| | using Microsoft.Extensions.DependencyInjection; |
| | using Microsoft.Extensions.Hosting; |
| | using Microsoft.Extensions.Logging; |
| | using Serilog; |
| | using Serilog.Context; |
| | using Serilog.Events; |
| | using Serilog.Sinks.MSSqlServer; |
| | using System.Data; |
| | |
| | var builder = Host.CreateApplicationBuilder(args); |
| | |
| | // 配置 Serilog |
| | Log.Logger = new LoggerConfiguration() |
| | .MinimumLevel.Debug() |
| | .MinimumLevel.Override("Microsoft", LogEventLevel.Information) |
| | .Enrich.FromLogContext() |
| | .Enrich.WithProperty("MachineName", Environment.MachineName) // 全部日志记录都带上参数:MachineName |
| | // 注意:.Enrich.WithProperty(...) 是一个一次性 enricher,适用于静态值 |
| | // 如果需要动态或更复杂的 enricher,可以实现 ILogEventEnricher |
| | .WriteTo.Console() // 可选:同时输出到控制台 |
| | .WriteTo.MSSqlServer( |
| | connectionString: "Server=localhost;Database=SerilogDemo;Trusted_Connection=True;TrustServerCertificate=True;", |
| | sinkOptions: new Serilog.Sinks.MSSqlServer.MSSqlServerSinkOptions |
| | { |
| | TableName = "Logs", |
| | AutoCreateSqlTable = true // 自动建表(首次运行时) |
| | }, |
| | columnOptions: new ColumnOptions |
| | { |
| | TimeStamp = { ConvertToUtc = true }, |
| | AdditionalColumns = new List<SqlColumn> // 自定义列,存储当前机器名 |
| | { |
| | new SqlColumn |
| | { |
| | DataType = SqlDbType.NVarChar, |
| | DataLength = 255, // 对于字符串类型,建议指定长度 |
| | ColumnName = "MachineName" |
| | }, |
| | new SqlColumn |
| | { |
| | DataType = SqlDbType.NVarChar, |
| | DataLength = 100, |
| | ColumnName = "UserName", // 新增一个动态列:用户名 |
| | AllowNull = true |
| | }, |
| | new SqlColumn |
| | { |
| | DataType = SqlDbType.NVarChar, |
| | DataLength = 50, |
| | ColumnName = "RequestId", // 新增一个动态列:请求 ID |
| | AllowNull = true |
| | } |
| | }, |
| | }) |
| | .CreateLogger(); |
| | |
| | builder.Services.AddLogging(loggingBuilder => |
| | { |
| | loggingBuilder.ClearProviders(); |
| | loggingBuilder.AddSerilog(dispose: true); |
| | }); |
| | var host = builder.Build(); |
| | |
| | // 示例日志 |
| | var logger = host.Services.GetRequiredService<ILogger<Program>>(); |
| | logger.LogInformation("Test {@User}", new { Name = "Alice" }); |
| | logger.LogInformation("应用程序启动"); |
| | using (LogContext.PushProperty("MachineName", Environment.MachineName)) |
| | { |
| | logger.LogWarning("这是一条带自定义属性的日志"); |
| | } |
| | |
| | // 测试写入自定义列的内容 |
| | string currentUserId = "User_Alice_88"; |
| | string currentRequestId = Guid.NewGuid().ToString().Substring(0, 8); |
| | using (LogContext.PushProperty("UserName", currentUserId)) |
| | using (LogContext.PushProperty("RequestId", currentRequestId)) |
| | { |
| | logger.LogInformation("2. 嵌套作用域:当前用户 {UserName} 的操作,请求ID {RequestId}", currentUserId, currentRequestId); |
| | // 即使不显式在消息模板中写 {UserName},只要 LogContext 里有,且列名匹配,就会存入数据库 |
| | logger.LogWarning("3. 警告日志:自动捕获上下文中的 UserName 和 RequestId"); |
| | } |
| | try |
| | { |
| | throw new InvalidOperationException("测试异常日志"); |
| | } |
| | catch (Exception ex) |
| | { |
| | logger.LogError(ex, "发生了一个异常"); |
| | } |
| | logger.LogInformation("应用程序结束"); |
| | host.RunAsync().Wait(); |

运行代码,用户名和请求 ID 就会写入数据库,如下图:

**更改属性列的存储格式(Properties Column Format):**默认的 Properties 列通常存储为 XML 或 NVARCHAR(MAX) 格式的 JSON。可以配置它存储为 SQL Server 的原生 JSON 类型(如果数据库版本支持),或者调整其长度和名称。

复制代码

|---|------------------------------------------------------------|
| | columnOptions.Properties.ColumnName = "LogContext"; |
| | columnOptions.Properties.DataType = SqlDbType.NVarChar; |
| | columnOptions.Properties.DataLength = 2048; // 限制长度,防止过大 |
| | // 注意:若要利用 SQL Server 的 JSON 函数,通常只需确保存储的是有效 JSON 字符串即可 |

**配置主键和索引(Primary Keys and Indexes):**虽然 Sink 主要负责写入,但你可以通过 ColumnOptions 指定哪一列作为主键(默认是 Id),并在建表时自动创建。对于高性能查询,通常建议在数据库层面手动为常用的自定义列(如 UserId 或 TimeStamp)添加索引。

2.3.2 性能优化(Performance Optimization)

**批量写入(Batch Posting):**为了减少数据库连接开销和事务日志压力,可以配置批量写入。设置 BatchPostingLimit(每次批处理的日志数量)和 Period(触发批处理的时间间隔)。

复制代码

|---|--------------------------------------------------|
| | var sinkOptions = new MSSqlServerSinkOptions() |
| | { |
| | TableName = "Logs", |
| | BatchPostingLimit = 100, // 每 100 条提交一次 |
| | Period = TimeSpan.FromSeconds(5) // 或每 5 秒提交一次 |
| | }; |
| | |
| | Log.Logger = new LoggerConfiguration() |
| | .WriteTo.MSSqlServer( |
| | connectionString: "...", |
| | sinkOptions: sinkOptions, |
| | columnOptions: columnOptions) |
| | .CreateLogger(); |

**异步写入(Asynchronous Logging):**虽然 MSSqlServer Sink 本身有一定的缓冲机制,但在高并发场景下,推荐结合 Serilog.Sinks.Async 包使用。它将日志写入操作封装在一个后台队列中,由独立线程异步执行,彻底避免日志 I/O 阻塞主业务线程。

复制代码

|---|------------------------------------------|
| | // 需要安装 Serilog.Sinks.Async |
| | Log.Logger = new LoggerConfiguration() |
| | .WriteTo.Async(a => a.MSSqlServer( |
| | connectionString: "...", |
| | sinkOptions: sinkOptions, |
| | columnOptions: columnOptions |
| | )) |
| | .CreateLogger(); |

**禁用自动建表(Disable Auto Table Creation):**在生产环境中,通常建议手动创建表并精确控制索引、分区和文件组,而不是依赖 Sink 的自动建表功能。可以通过设置 AutoCreateSqlTable = false 来禁用此功能。

复制代码

|---|--------------------------------------------------|
| | var sinkOptions = new MSSqlServerSinkOptions() |
| | { |
| | AutoCreateSqlTable = false |
| | }; |

2.3.3 结构化与上下文增强 (Structuring & Context)

**丰富日志上下文(Enrichers):**结合 Serilog.Enrichers.Environment 或其他自定义 Enricher,自动向每条日志添加机器名、进程ID、用户信息、请求ID等上下文数据。这些数据可以被映射到上述的"自定义列"中。

复制代码

|---|-----------------------------------|
| | // 自动添加 MachineName, UserName 等 |
| | .Enrich.FromLogContext() |
| | .Enrich.WithMachineName() |
| | .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,避免在连接字符串中硬编码密码。

复制代码

|---|-------------------------------------------------------|
| | // 连接字符串示例 (Integrated Security) |
| | "Server=...;Database=...;Integrated Security=true;" |

**自定义 SQL 客户端配置:**可以通过 SqlConnection 的高级设置(如 Connect Timeout, Encrypt 等)来增强连接的安全性和稳定性。

2.3.5 故障转移与可靠性(Reliability)

**备用 Sink(Fallback Sink):**如果数据库不可用,日志不应导致应用程序崩溃。可以配置一个备用 Sink(如文件或控制台),当 MSSQL Server 写入失败时自动切换。

复制代码

|---|-----------------------------------------------|
| | // 伪代码概念,实际需使用 Fallback 包装器或自定义逻辑 |
| | .WriteTo.MSSqlServer(...) |
| | .WriteTo.File("logs/fallback-.txt") // 作为备份 |

相关推荐
Dreamboat¿3 小时前
SQL 注入漏洞
数据库·sql
曹牧4 小时前
Oracle数据库中,将JSON字符串转换为多行数据
数据库·oracle·json
被摘下的星星4 小时前
MySQL count()函数的用法
数据库·mysql
末央&4 小时前
【天机论坛】项目环境搭建和数据库设计
java·数据库
徒 花4 小时前
数据库知识复习07
数据库·作业
素玥4 小时前
实训5 python连接mysql数据库
数据库·python·mysql
jnrjian4 小时前
text index 查看index column index定义 index 刷新频率 index视图
数据库·oracle
瀚高PG实验室5 小时前
审计策略修改
网络·数据库·瀚高数据库
言慢行善5 小时前
sqlserver模糊查询问题
java·数据库·sqlserver