ASP.NET Core 日志介绍
适用对象:ASP.NET Core 开发人员、运维人员与技术负责人 示例采用 Minimal Hosting 模型,适用于现代 ASP.NET Core 项目。
1. 日志是什么
日志是应用在运行期间产生的事件记录。它可以帮助团队:
- 了解应用启动、停止和配置加载情况;
- 排查异常、接口失败与性能问题;
- 追踪一次请求经过的服务和业务步骤;
- 监控关键业务事件,例如订单创建、支付失败;
- 为告警、审计和容量分析提供数据。
ASP.NET Core 提供统一的日志抽象,应用代码通常只依赖 ILogger<T>,再由一个或多个日志提供程序把日志输出到控制台、调试窗口、Windows Event Log、Application Insights 或第三方日志平台。
这种设计把"如何记录日志"与"日志最终写到哪里"分离开来。业务代码不需要因为更换日志平台而大范围修改。
2. 核心概念
2.1 ILogger<T>
ILogger<T> 是最常用的日志接口,通常通过依赖注入获得:
public sealed class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void CreateOrder(long orderId)
{
_logger.LogInformation("Creating order {OrderId}", orderId);
}
}
泛型参数 OrderService 会成为日志类别(Category)。类别通常是类的完整名称,可用于按命名空间或模块过滤日志。
2.2 日志级别
ASP.NET Core 定义了以下日志级别,严重程度从低到高排列:
| 级别 | 典型用途 | 生产环境建议 |
|---|---|---|
Trace | 极细粒度执行过程、逐步诊断信息 | 默认关闭,仅短时排障启用 |
Debug | 开发调试信息、内部状态 | 通常关闭或按模 块开启 |
Information | 正常且有价值的业务或系统事件 | 有选择地记录 |
Warning | 可恢复问题、异常趋势、降级行为 | 建议记录并监控 |
Error | 当前操作失败,但应用仍可继续运行 | 必须记录,通常需要告警 |
Critical | 应用或系统级严重故障 | 必须记录并立即告警 |
None | 禁用指定类别的日志 | 仅用于配置过滤 |
配置的级别表示"允许输出的最低严重程度"。例如配置为 Warning 时,只会输出 Warning、Error 和 Critical。
2.3 日志类别
日志类别(Category)是附在日志上的来源名称,用来回答"这条日志是哪个类或组件写出来的"。它不同于日志级别:类别说明来源,级别说明事件的严重程度。
类别名称从哪里来
使用 ILogger<T> 时,类别通常就是类型 T 的完整名称,包括命名空间和类名。例如:
namespace MyCompany.OrderApi.Services;
public sealed class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void CreateOrder(long orderId)
{
_logger.LogInformation("Creating order {OrderId}", orderId);
_logger.LogWarning("Order {OrderId} requires review", orderId);
}
}
这两条日志的类别都是 MyCompany.OrderApi.Services.OrderService,但级别分别为 Information 和 Warning。同一个日志记录器写出的日志,不会因为消息内容或级别不同而改变类别。
类别只是用于分类和过滤的字符串,不是文件名,也不会自动创建一个同名日志文件。
常见类别
| 类别名称或前缀 | 表示的日志来源 |
|---|---|
MyCompany.OrderApi.Services.OrderService | 订单业务服务 |
MyCompany.OrderApi.Controllers.OrderController | 订单控制器 |
Microsoft.AspNetCore | ASP.NET Core 的路由、中间件等框架组件 |
Microsoft.EntityFrameworkCore.Database.Command | EF Core 数据库命令 |
System.Net.Http.HttpClient | 通过 IHttpClientFactory 创建的客户端所产生的 HTTP 请求日志 |
其中 MyCompany.OrderApi 是示例命名空间,使用时应替换为自己项目中的实际名称。
如何按类别控制输出
假设只想查看订单服务的调试日志,而不想让整个应用都输出调试信息,可以这样配置:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"MyCompany.OrderApi": "Information",
"MyCompany.OrderApi.Services.OrderService": "Debug"
}
}
}
在没有提供程序专用规则覆盖的情况下,上面的配置意味着:
- 订单服务输出
Debug及以上级别; - 当前应用的其他类别输出
Information及以上级别; - 以
Microsoft开头的框架类别输出Warning及以上级别; - 其他未匹配到类别规则的日志,使用
Default设置。
类别按名称前缀匹配;在同一组适用规则中,匹配前缀越长,规则越具体。例如,针对 MyCompany.OrderApi.Services.OrderService 的设置,比针对 MyCompany.OrderApi 的设置更具体。
实际用途是"定点排查":只提高某个故障模块的日志详细程度,其他模块继续使用原来的级别。规则的完整说明见微软日志配置文档。
2.4 日志提供程序
日志提供程序(Logging Provider)负责把日志送到具体的输出目标,例如终端、调试窗口或日志服务。
可以用三个问题区分核心概念:
| 概念 | 回答的问题 | 示例 |
|---|---|---|
| 日志类别 | 日志来自哪里? | MyCompany.OrderApi.Services.OrderService |
| 日志级别 | 这件事有多严重? | Information、Error |
| 日志提供程序 | 日志输出到哪里? | Console、Debug、Application Insights |
业务代码调用 ILogger<T> 写日志,日志基础设施再根据各个提供程序的过滤规则,交给对应提供程序输出。同一条日志可以被送到多个目标,不需要在业务代码里分别调用多个日志框架。
常见提供程序和接入方案
| 提供程序或方案 | 输出目标 | 主要用途 |
|---|---|---|
| Console | 标准输出,默认显示在终端 | 本地运行、容器环境日志采集 |
| Debug | 调试输出;需要附加调试器 | 在 Visual Studio 等开发工具中调试 |
| EventSource | .NET 事件源 | 配合诊断工具采集运行信息 |
| EventLog | Windows 事件日志 | Windows 服务和服务器运维 |
| Application Insights | Azure Application Insights | 将日志发送到 Azure 统一查询 |
| Serilog、NLog 的日志提供程序 | 由相应框架及其扩展配置的文件、控制台或外部服务 | 滚动文件、格式定制、多目标输出 |
其中 Console、Debug、EventSource 和 Windows 上的 EventLog 是常见的内置提供程序;其他接入方案通常需要额外安装包并完成配置。具体支持范围见微软日志提供程序文档。
如何注册提供程序
通过 WebApplication.CreateBuilder(args) 创建应用时,框架已经配置了默认日志基础设施和提供程序,通常不需要手动注册 ILogger<T>。
如果希望明确指定只使用 Console 和 Debug,可以这样写:
var builder = WebApplication.CreateBuilder(args);
// 清除默认提供程序,再添加本项目需要的输出目标。
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
var app = builder.Build();
app.MapGet("/", (ILogger<Program> logger) =>
{
logger.LogInformation("Home endpoint was requested");
return "OK";
});
app.Run();
访问接口后,符合过滤条件的日志会输出到控制台;附加调试器时,也可以看到 Debug 提供程序的输出。
ClearProviders() 只移除已注册的日志提供程序,不会删除历史日志文件。如果清除后没有添加任何提供程序,调用日志接口也不会产生对应的日志输出。
每个输出目标可以使用不同级别
例如,控制台只保留常规信息,调试窗口则额外显示调试日志:
{
"Logging": {
"LogLevel": {
"Default": "Information"
},
"Console": {
"LogLevel": {
"Default": "Information"
}
},
"Debug": {
"LogLevel": {
"Default": "Debug"
}
}
}
}
在此配置下,一条 Debug 级别日志不会出现在控制台,但附加调试器时可以由 Debug 提供程序输出。提供程序专用的级别设置可以覆盖通用设置。
这里容易混淆:"Debug": { ... } 表示 Debug 提供程序,而 "Default": "Debug" 中的 Debug 表示日志级别。
配置文件负责调整已注册提供程序的行为;仅在 JSON 中增加某个提供程序的名字,并不会自动安装或注册它。
输出格式与输出目标不是一回事
AddConsole() 和 AddJsonConsole() 都使用 Console 提供程序,区别在于文本格式和 JSON 格式,而不是日志输出到了不同位置。
Console 本身不会自动保存日志文件;需要宿主或采集系统保存其输出。ASP.NET Core 没有内置的通用滚动文件日志提供程序,如果需要按日期或大小滚动写文件,可以接入 Serilog、NLog 等方案。
3. 结构化日志
结构化日志把关键数据作为独立字段传递给日志系统,而不只是拼成一段文本。
推荐写法:
_logger.LogInformation(
"Order {OrderId} created for customer {CustomerId}",
orderId,
customerId);
日志平台可将 OrderId 和 CustomerId 保存为字段,从而支持精确查询和聚合。
不推荐写法:
_logger.LogInformation(
$"Order {orderId} created for customer {customerId}");
字符串插值会提前生成完整文本,通常会丢失字段语义;即使该级别被过滤,也可能产生不必要的字符串分配。
建议遵循以下规则:
- 消息模板保持稳定,变量放在
{Placeholder}中; - 占位符使用含义清晰且统一的名称,如
{OrderId}; - 不要把用户输入动态拼到消息模板本身;
- 注意参数按位置与占位符对应,而不是按占位符名称匹配;
- 对同一概念使用固定字段名,避免同时出现
UserId、UserID、Uid。
4. 在业务服务中记录日志
下面的示例展示如何在业务服务中记录操作开始、成功、取消和失败,并通过结构化字段保留订单标识:
public sealed class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public async Task SubmitAsync(
long orderId,
long customerId,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"Submitting order {OrderId} for customer {CustomerId}",
orderId,
customerId);
try
{
await SaveOrderAsync(orderId, cancellationToken);
_logger.LogInformation("Order {OrderId} submitted successfully", orderId);
}
catch (OperationCanceledException)
when (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("Order {OrderId} submission was cancelled", orderId);
throw;
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to submit order {OrderId}", orderId);
throw;
}
}
private static Task SaveOrderAsync(
long orderId,
CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
}
上面的 SaveOrderAsync 仅为占位方法,实际项目应替换为真实的订单保存逻辑。
要点:
- 将异常对象作为
LogError的第一个参数传入,日志系统才能保留异常类型、消息和堆栈; - 捕获异常后如果无法真正处理,应使用
throw;保留原始堆栈; - 取消操作通常不是系统错误,应根据业务语义记录为
Information、Debug,或不记录; - 同一个异常尽量只在负责处理或跨越系统边界的位置记录一次,避免每层捕获后重复写入。
5. HTTP 请求与响应日志
ASP.NET Core 提供 HTTP Logging 中间件,可记录请求方法、路径、状态码、请求头、响应头、耗时,以及按配置选择的请求体或响应体。
using Microsoft.AspNetCore.HttpLogging;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields =
HttpLoggingFields.RequestMethod |
HttpLoggingFields.RequestPath |
HttpLoggingFields.ResponseStatusCode |
HttpLoggingFields.Duration;
});
var app = builder.Build();
app.UseHttpLogging();
app.MapGet("/orders/{id:long}", (long id) =>
Results.Ok(new { id }));
app.Run();
中间件的位置会影响它能观察到的请求处理范围,通常应放在管道较前方。
HTTP Logging 使用 Information 级别。如果已经把 Microsoft.AspNetCore 设置为 Warning,需要为该中间件单独放开级别,将以下配置合并到现有配置中:
{
"Logging": {
"LogLevel": {
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}
}
}
生产环境不建议默认记录完整请求体、响应体或全部请求头,因为它们可能包含:
- 密码、访问令牌、Cookie 和 API Key;
- 身份证号、手机号、地址等个人信息;
- 支付信息与商业敏感数据;
- 大文件或大 JSON,导致明显的 I/O、内存和存储开销。
启用正文日志前,应明确允许的 Content-Type、大小上限、脱敏策略和保留周期,并完成性能测试与安全评审。
6. 总结
ASP.NET Core 日志体系的关键不是"多写日志",而是记录可检索、可关联、级别准确且安全的信息:
- 在业务代码中依赖
ILogger<T>; - 使用稳定的结构化消息模板;
- 通过配置按类别和提供程序过滤;
- 选择合适的日志提供程序,明确日志输出和保存位置;
- 使用订单 ID 等必要业务字段,方便查询和定位问题;
- 根据需要启用 HTTP 请求与响应日志,避免记录敏感数据。
做好这些基础工作后,日志才能真正成为开发、运维和业务团队共同使用的可观测性资产。