一.引言
日志是构建健壮 Web API 的重要组成部分,能够帮助我们追踪请求、诊断问题、记录关键事件。在 .Net 中,日志系统由内置的 Microsoft.Extensions.Logging 抽象提供统一接口,并支持多种第三方日志框架(如 Serilog、NLog 等)。
环境为.net8,配合第三方框架Serilog(因为微软只提供了统一的接口,和一些实现包,如将日志打印到控制台,但是不提供写日志到文件的内建实现,所以通常会引入第三方的流行库,例如Serilog,Serilog是基于微软的统一接口实现的,可以无缝接入)
二.环境配置
1环境搭建
创建.Net8 Asp.Net WebApi
我们先试试将日志打印到控制台的功能,主要是使用这两个包,前者是日志系统的基础包,后者是关于控制台的基础实现,在开发过程中我们会常用这个功能.框架已经内置这两个包了,我们无需引入.
2基本使用
我们在使用的时候直接使用注入的日志服务,然后调用打印日志,这里面有些没有打印,是引入默认配置的打印级别最低到Information,更低的级别不打印了,这个是可配置的.
3日志级别
级别 | LogLevel 值 | 说明 |
---|---|---|
Trace |
0 | 最详细的日志,通常只在开发调试中启用。记录非常底层的信息(例如方法进入/退出) |
Debug |
1 | 调试信息,开发时常用,记录变量值、分支判断等辅助信息 |
Information |
2 | 应用的正常流程事件,比如用户登录成功、请求开始/结束等 |
Warning |
3 | 表示可能的问题,例如“找不到缓存,使用默认值”,但程序还能继续运行 |
Error |
4 | 发生了错误,功能失败,例如数据库异常、请求失败等 |
Critical |
5 | 致命错误,通常会导致应用崩溃或服务不可用,需要立刻处理 |
4框架的默认配置
创建了 WebApi的模版项目,我什么服务也没注册,直接就可以使用日志服务,这是引入框架默认已经将这个服务注入进去了.只是这段没有显示的出现.
builder.Services.AddLogging(loggingBuilder =>
{
loggingBuilder.AddConsole();
});
5日志接口和各个提供者的关系
.NET 日志系统(Microsoft.Extensions.Logging)的一大亮点:通过抽象接口与具体实现(提供者)解耦。
优雅的解耦设计可以让我们的控制器不关心具体的实现,想用哪个只需要在Program里面配置一下就行,我们的控制器代码只需要注入日志服务,打印日志就够了(甚至这些日志被输出到哪里都不关心,可以是控制台,文件,云等等).
前面的例子就是使用了内置的控制器提供者,但是因为我们要使用第三方的Serilog,所以我们要介绍一下Serilog.
三.Serilog
我们的重点是Serilog,所以前面的部分有一个概念上的理解就可以.
我们要使用Serilog接管原生的日志系统.在此之前我们需要对其有个基本的了解.
1.控制台牛刀小试
$ dotnet add package Serilog $ dotnet add package Serilog.Sinks.Console
创建一个控制台,在控制台中安装这两个包.
using Serilog;
var log = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
使用LoggerConfiguration可以创建一个日志记录者,使用WriteTo.Console()意味着我的日志将会被记录到控制台上,显然这里可以继续配置记录到文件,云等等
log.Information("Hello, Serilog!");
使用这个对象就可以记录日志了.
Log.Logger = log;
Log.Information("The global logger has been configured");
可以将这个对象交给全局的静态引用,以后就可以很方便的使用这个静态引用.这不是必须的,必要的时候你可以随时使用LoggerConfiguration来构建一个新的日志对象,使用这个新对象来记录日志!
2.Sinks
如何加一个日志接收者(可以称呼为Sinks)呢?
$ dotnet add package Serilog
$ dotnet add package Serilog.Sinks.Console
$ dotnet add package Serilog.Sinks.File
在引入这个Sinks的实现包,比如想记录到文件,就再引入第三个包.一个接受者对应一个Nuget包,
https://github.com/serilog/serilog/wiki/Provided-Sinks,所有实现都在,应该能满足多数项目了!
using System;
using Serilog;
class Program
{
static async Task Main()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.WriteTo.File("logs/myapp.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
Log.Information("Hello, world!");
int a = 10, b = 0;
try
{
Log.Debug("Dividing {A} by {B}", a, b);
Console.WriteLine(a / b);
}
catch (Exception ex)
{
Log.Error(ex, "Something went wrong");
}
finally
{
await Log.CloseAndFlushAsync();
}
}
}
WriteTo.File("logs/myapp.txt", rollingInterval: RollingInterval.Day)
只需要配置要记录到的文件路径和日志滚动(日志文件自动按时间拆分)策略.
RollingInterval.Day
:表示 每天生成一个新的日志文件
枚举值 | 含义 |
---|---|
RollingInterval.Infinite |
永远写一个文件(不滚动) |
RollingInterval.Year |
每年一个文件 |
RollingInterval.Month |
每月一个文件 |
RollingInterval.Day |
✅ 每天一个文件 |
RollingInterval.Hour |
每小时一个文件 |
RollingInterval.Minute |
每分钟一个文件(较少用) |
Log.CloseAndFlushAsync();在程序的最后需要调用一下释放资源,比如可能占用着文件句柄.
最后你可能注意到,记录日志的时候只需要按级别记录日志就行了,根本不关心日志被记录到了哪里!
3.日志级别
和刚才提到的日志级别类似,但是Serilog和原生的还是有区别的.
Serilog 的日志级别(从低到高):
日志等级 | 说明 | 场景 | 日志量 |
---|---|---|---|
Verbose | 最详细,几乎所有日志信息都会输出 | 极限调试、诊断底层问题 | 🌊 最大 |
Debug | 仅内部调试信息,用于了解系统内部状态变化 | 调试开发时使用 | 📘 很多 |
Information | 系统的正常行为,业务操作的主要流程日志 | 用户登录、订单完成等 | 📗 常用 |
Warning | 警告,有潜在问题或轻微异常,但系统还能正常运行 | 配置缺失、请求重试、服务降级等 | ⚠️ 警觉性 |
Error | 真正发生错误,功能失败但系统仍运行 | 异常捕获、数据库连接失败等 | ❌ 严重 |
Fatal | 致命错误,系统必须停止/立即处理的重大问题 | 启动失败、内存泄露导致崩溃等 | ☠️ 最高 |
.MinimumLevel.Debug(),配置 Serilog 的日志“最低输出等级”, Debug 及以上的所有日志 都会被输出,而比它低的等级
Verbose
不会被输出。
如果没有配置MinimumLevel
,那么会使用默认级别Information
不仅如此,还可以进行更细致的控制!
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() // Logger整体是Debug及以上才会“生成日志事件”
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
.WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: LogEventLevel.Debug)
.CreateLogger();
关注restrictedToMinimumLevel参数,使用该参数可以让Sinks进一步的筛选日志等级.先配置最小等级为Debug,但是我可以单独配置控制台只接收到更高的Information,要注意的是只能配置高于最小等级的级别,就比如我最小级别是Debug,但是不存在配置Verbose级别而让控制台能接收Verbose.
我们在依赖注入使用ILogger来记录日志,所以记录方面使用原生的日志级别,但是Serilog配置代码要使用Serilog级别,Serilog会自动映射到原生日志级别.
4.WebApi中使用
dotnet add package Serilog.AspNetCore
在WebApi项目中一般只需要引入这个包就够了
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.File(
"Logs/log-.txt",
rollingInterval: RollingInterval.Day)
.CreateLogger();
builder.Host.UseSerilog(dispose: true);
先配置记录对象交给静态引用,然后使用
builder.Host.UseSerilog(dispose: true);
public static IHostBuilder UseSerilog(
this IHostBuilder builder,
ILogger? logger = null,
bool dispose = false,
LoggerProviderCollection? providers = null)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
builder.ConfigureServices((_, collection) =>
{
collection.AddSerilog(logger, dispose, providers);
});
return builder;
}
参数名 | 类型 | 说明 |
---|---|---|
builder |
IHostBuilder |
主机构建器,ASP.NET Core 启动的入口点。 |
logger |
Serilog.ILogger? |
可选的 Serilog 日志实例,如果不传,将使用静态类 Serilog.Log 。 |
dispose |
bool |
如果为 true ,当应用关闭时,自动调用 Dispose() (或 Log.CloseAndFlush() )释放资源。 |
providers |
LoggerProviderCollection? |
用于桥接其他 ILoggerProvider (比如 NLog、Console 等),使得其他提供者也能接收到日志(通过 WriteTo.Providers() 使用)。 |
如果你看过官方文档会发现文档的示例一般都是使用 AddSerilog(...)
方法完成真正的注册工作。
但我推荐使用UseSerilog方法,但其实内部也是使用了AddSerilog方法.
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact;
namespace Serilog_Study
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.File(
"Logs/log-.txt",
rollingInterval: RollingInterval.Day)
.CreateLogger();
builder.Host.UseSerilog(dispose: true);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseSerilogRequestLogging();
app.MapControllers();
app.Run();
}
}
}
现在已经可以和刚才一样,通过依赖注入的形式,通过构造器注入ILogger服务.
你可以得到这样的日志,同时还会保留到文本文件.
5.请求记录
现代日志系统通常会记录每一次HTTP请求.
原因 | 说明 |
---|---|
🔍 排查错误 | 出现异常或 Bug 时,需要知道调用了哪个接口、用了什么方法、返回了什么状态码 |
📈 性能分析 | 可以看到每个请求的耗时(比如哪个接口慢) |
📦 审计记录 | 某些系统(如金融、医疗)需要记录访问历史 |
🧩 统一结构化 | 每条请求日志都含有标准字段(方法、路径、耗时、状态码),方便日志平台分析 |
Serilog提供了中间件来帮助实现该功能.
只需要加上下面一行就能使用该中间件,但是在此之前
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning)
我们要 配置这三行,因为ASP.NET Core 框架本身(如 MVC、Routing、Kestrel)会产生日志,且十分啰嗦,一次请求会将很多细节被展示出来(比如中间件执行、路由匹配、控制器执行、模型绑定、请求开始/结束……),日志碎片非常多,一个请求可能产生 5~10 条日志,而且你无法集中看到这次请求的整体表现。
但是如果你配置了上述三个,则只会产生一行,非常的简洁.
[16:30:55 INF] HTTP GET /WeatherForecast responded 200 in 1.5273 ms
要注意是:该中间件只能记录它后面发生的事。所以要想记录 Web API 请求,你要把它放在 app.MapControllers() 之前。
6.Output templates
.WriteTo.File("log.txt",
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
文本类的日志接收器(sinks)使用输出模板来控制日志的格式, 你或许注意到outputTemplate参数,输出格式就是这个参数来配置的
默认就是这个样子的,当然你可以按需配置.
占位符 | 含义 | 示例输出 |
---|---|---|
{Timestamp} |
日志时间戳 | 2025-06-23 16:50:30.123 +08:00 |
{Level} |
日志等级(Information / Warning 等) | Information |
{Level:u3} |
日志等级,3个字母大写 | INF , WRN , ERR |
{Level:w3} |
日志等级,3个字母小写 | inf , wrn , err |
{Message} |
最终渲染出的日志消息 | 用户登录成功 |
{Message:lj} |
日志消息,同时保留结构化数据 | (见下) |
{Properties} |
所有结构化字段(除了上面显示的) | {UserId=123, IP="127.0.0.1"} |
{Properties:j} |
上面这些字段以 JSON 格式输出 | {"UserId":123,"IP":"127.0.0.1"} |
{NewLine} |
换行 | \n |
{Exception} |
如果有异常,会打印异常堆栈 | System.NullReferenceException: xxx... |
7.结构化日志
上面我们输出的日志只是一个简单的字符串,人读起来比较方便,但是日志信息是海量的,人读效率显然不高,但是如果将日志结构化输出
{
"MessageTemplate": "User {User} updated student {StudentId}. Changes: {Changes}",
"Properties": {
"User": "admin",
"StudentId": 1001,
"Changes": "{\"Name\":\"Alice\",\"Age\":20}"
}
}
比如输出成一个json,那么计算机也能读了,哪怕是海量的日志计算机都能帮我们快速分析.
Serilog提供了这样强大的功能,并且推荐我们这么做.
_logger.LogInformation("User {User} updated student {StudentId}. Changes: {Changes}",
currentUser, studentId, JsonConvert.SerializeObject(changes));
只需要这样输出日志,日志的结构就被保留了下来.
不仅如此,还为我们提供了Seq,Seq 不仅有一个强大的 可视化界面(Web UI),还提供了 专用 NuGet 包,用于从你的 .NET 应用中发送结构化日志到它的日志收集服务中。
Seq安装
Seq官网:Seq — centralized structured logs
Nuget包安装
dotnet add package Serilog.Sinks.Seq
https://github.com/datalust/serilog-sinks-seq
Seq是一个日志接受者(是一个独立的服务),而引入这个包可以将日志发送过去.使用下面的方法.
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Seq("http://localhost:5341") // Seq 默认监听端口
.CreateLogger();
builder.Host.UseSerilog();
┌────────────┐
│ ASP.NET App│
│ (Serilog) │
└────┬───────┘
│
▼
Serilog.Sinks.Seq (NuGet 包)
│
▼
┌─────────────┐
│ Seq Server│ ← 启动后访问 http://localhost:5341
│ (实时日志 + 搜索 + 图表)
└─────────────┘
再写就太长了,Seq的使用可以在官网学习.
END