跳到主要内容

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 时,只会输出 WarningErrorCritical

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,但级别分别为 InformationWarning。同一个日志记录器写出的日志,不会因为消息内容或级别不同而改变类别。

类别只是用于分类和过滤的字符串,不是文件名,也不会自动创建一个同名日志文件。

常见类别

类别名称或前缀表示的日志来源
MyCompany.OrderApi.Services.OrderService订单业务服务
MyCompany.OrderApi.Controllers.OrderController订单控制器
Microsoft.AspNetCoreASP.NET Core 的路由、中间件等框架组件
Microsoft.EntityFrameworkCore.Database.CommandEF 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
日志级别这件事有多严重?InformationError
日志提供程序日志输出到哪里?Console、Debug、Application Insights

业务代码调用 ILogger<T> 写日志,日志基础设施再根据各个提供程序的过滤规则,交给对应提供程序输出。同一条日志可以被送到多个目标,不需要在业务代码里分别调用多个日志框架。

常见提供程序和接入方案

提供程序或方案输出目标主要用途
Console标准输出,默认显示在终端本地运行、容器环境日志采集
Debug调试输出;需要附加调试器在 Visual Studio 等开发工具中调试
EventSource.NET 事件源配合诊断工具采集运行信息
EventLogWindows 事件日志Windows 服务和服务器运维
Application InsightsAzure 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);

日志平台可将 OrderIdCustomerId 保存为字段,从而支持精确查询和聚合。

不推荐写法:

_logger.LogInformation(
$"Order {orderId} created for customer {customerId}");

字符串插值会提前生成完整文本,通常会丢失字段语义;即使该级别被过滤,也可能产生不必要的字符串分配。

建议遵循以下规则:

  • 消息模板保持稳定,变量放在 {Placeholder} 中;
  • 占位符使用含义清晰且统一的名称,如 {OrderId}
  • 不要把用户输入动态拼到消息模板本身;
  • 注意参数按位置与占位符对应,而不是按占位符名称匹配;
  • 对同一概念使用固定字段名,避免同时出现 UserIdUserIDUid

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; 保留原始堆栈;
  • 取消操作通常不是系统错误,应根据业务语义记录为 InformationDebug,或不记录;
  • 同一个异常尽量只在负责处理或跨越系统边界的位置记录一次,避免每层捕获后重复写入。

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 日志体系的关键不是"多写日志",而是记录可检索、可关联、级别准确且安全的信息:

  1. 在业务代码中依赖 ILogger<T>
  2. 使用稳定的结构化消息模板;
  3. 通过配置按类别和提供程序过滤;
  4. 选择合适的日志提供程序,明确日志输出和保存位置;
  5. 使用订单 ID 等必要业务字段,方便查询和定位问题;
  6. 根据需要启用 HTTP 请求与响应日志,避免记录敏感数据。

做好这些基础工作后,日志才能真正成为开发、运维和业务团队共同使用的可观测性资产。

参考资料