1. ILogger 与日志级别
一句话总结: ILogger 是 ASP.NET Core 内置的日志抽象,通过日志级别与类别过滤控制输出,配合 DI 注入到任何类。
ILogger<T> 是结构化日志的入口,T(通常是当前类)作为日志类别,用于过滤与聚合。日志级别从 Trace 到 Critical 六级,生产环境通常只记录 Information 及以上。级别不只是「严重程度」,更是成本控制——Debug 日志在高并发下可能写爆磁盘。
public class OrderService(ILogger<OrderService> logger)
{
public async Task<Order> CreateAsync(CreateOrderRequest req)
{
logger.LogInformation("开始创建订单 {Customer}", req.CustomerName);
try
{
var order = await _repo.SaveAsync(req);
logger.LogInformation("订单创建成功 {OrderId},耗时 {Ms}ms", order.Id, _sw.ElapsedMilliseconds);
return order;
}
catch (Exception ex)
{
logger.LogError(ex, "订单创建失败 {Customer}", req.CustomerName);
throw;
}
}
}
| 级别 | 用途 | 生产默认 |
|---|---|---|
| Trace/Debug | 调试细节 | 通常关闭 |
| Information | 业务事件、状态变化 | 开启 |
| Warning | 可恢复异常、降级 | 开启 |
| Error | 异常、失败 | 开启 |
| Critical | 进程级故障 | 开启 |
避坑: 别用字符串拼接组装日志消息——
LogInformation($"创建 {x}")会在不记录时也执行拼接,产生分配。用占位符形式LogInformation("创建 {X}", x),只有真正记录时才格式化,且字段名会进结构化日志。
2. 结构化日志
一句话总结: 结构化日志把「一行文本」变成「键值字段」,让日志可被机器查询、聚合与告警,而非只能人肉 grep。
传统日志是给人看的文本;结构化日志把 {Customer}、{OrderId} 变成索引字段,让日志系统可以按字段过滤、统计、关联。ILogger 的占位符天然支持结构化,配合 JSON 格式的 provider 即可输出键值对。
// 占位符 → 结构化字段
logger.LogInformation("创建订单 Customer={Customer} Total={Total}",
req.CustomerName, req.Total);
// 输出到控制台(JSON 格式)大致为:
// {"@t":"2026-10-01T05:00:00Z","@mt":"创建订单 Customer={Customer} Total={Total}",
// "Customer":"张三","Total":199.0,"SourceContext":"OrderService","Level":"Information"}
| 字段 | 含义 |
|---|---|
@t | 时间戳 |
@mt | 消息模板 |
Level | 级别 |
SourceContext | 记录者类别 |
| 自定义字段 | Customer/OrderId 等业务字段 |
避坑: 结构化日志的价值在于字段稳定。字段名要固定拼写(别一会
OrderId一会order_id),否则聚合查询被割裂。敏感数据(手机号、身份证)不进日志字段,需要脱敏再记。
3. Serilog 与日志管道
一句话总结: Serilog 通过「sink」把结构化日志写到控制台、文件、Elasticsearch 等目标,是 .NET 生态最流行的日志框架。
Serilog 的 LoggerConfiguration 定义日志管道:Enrich 加公共字段,WriteTo 指定输出目标,Filter 控制级别。它常与 ILogger 无缝集成——用 UseSerilog() 替换默认 provider,业务代码继续用 ILogger<T>。
// Program.cs —— Serilog 配置
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog((ctx, cfg) =>
cfg.ReadFrom.Configuration(ctx.Configuration)
.Enrich.WithMachineName()
.Enrich.WithProperty("App", "orders-api")
.WriteTo.Console(new RenderedCompactJsonFormatter())
.WriteTo.File("logs/orders-.log",
rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:HH:mm:ss} [{Level}] {SourceContext} {Message}{NewLine}{Exception}"));
// appsettings.json 里的 Serilog 级别覆盖
// "Serilog": { "MinimumLevel": "Information",
// "Override": { "Microsoft.AspNetCore": "Warning" } }
| 组件 | 作用 |
|---|---|
| Enrich | 附加机器名、App 名等公共字段 |
| WriteTo | 控制台/文件/ES 等 sink |
| Filter | 按级别/字段过滤 |
| RollingInterval | 文件按天滚动 |
| RenderedCompactJsonFormatter | 紧凑 JSON,适合摄取 |
避坑: 日志文件要设滚动与保留策略(
RetainedFileCountLimit),否则磁盘被日志写满。结构化格式优先 JSON(CompactJsonFormatter),文本模板只适合本地调试。Serilog 的ReadFrom.Configuration能热改级别,但生产一般走环境变量。
4. OpenTelemetry 链路追踪
一句话总结: 分布式追踪用 traceId 贯穿跨服务调用,OpenTelemetry 是采集与导出的事实标准,配合 ASP.NET Core 自动埋点即可打通链路。
单机日志只能看到「一个服务里发生了什么」,跨服务调用需要链路追踪:请求生成全局 traceId,每个服务记 spanId 与父 span,汇聚成完整调用链。OpenTelemetry .NET 通过 AddOpenTelemetry + AddAspNetCoreInstrumentation 自动为 HTTP 请求创建 span。
// Program.cs —— 启用 OpenTelemetry
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.AddAspNetCoreInstrumentation();
tracing.AddHttpClientInstrumentation();
tracing.AddSource("MyApp.*");
tracing.AddOtlpExporter(opt =>
opt.Endpoint = new Uri("http://otel-collector:4317"));
})
.WithMetrics(metrics =>
{
metrics.AddAspNetCoreInstrumentation();
metrics.AddMeter("MyApp.Meters");
metrics.AddOtlpExporter();
});
// 业务代码创建自定义 span
using var activity = ActivitySource.StartActivity("order.create");
activity?.SetTag("order.id", order.Id);
| 概念 | 说明 |
|---|---|
| traceId | 整条链路唯一 ID |
| span | 一个服务的某段工作 |
| parentId | 父 span,串起调用链 |
| Instrumentation | 自动埋点库 |
| Exporter | OTLP 导出到 Collector |
避坑: 链路追踪要打通日志关联:日志里带上
traceId,出问题时从日志反查链路。ASP.NET Core 的Activity与日志的关联通常由ILogger的 scopes 或 Serilog 的TraceIdenrichment 完成,别让日志与追踪「两张皮」。
5. 指标采集
一句话总结: 指标是「数字化的健康状态」,通过计数器、仪表与直方图反映吞吐、错误率与延迟,供监控系统聚合告警。
日志回答「发生了什么」,指标回答「有多严重」。OpenTelemetry 指标模型:Counter 累加计数(请求数)、UpDownCounter 可增可减(在线连接数)、Histogram 分布(耗时)。ASP.NET Core 自动提供 HTTP 指标(http.server.request.duration),业务指标用 Meter 自定义。
// 业务指标
public static class OrderMetrics
{
public static readonly Meter Meter = new("MyApp.Meters", "1.0.0");
public static readonly Counter<long> OrdersCreated =
Meter.CreateCounter<long>("orders.created", "orders");
public static readonly Histogram<double> OrderDuration =
Meter.CreateHistogram<double>("order.duration", "ms");
}
public class OrderService(ILogger<OrderService> logger)
{
public async Task<Order> CreateAsync(CreateOrderRequest req)
{
var sw = Stopwatch.StartNew();
var order = await _repo.SaveAsync(req);
sw.Stop();
OrderMetrics.OrdersCreated.Add(1);
OrderMetrics.OrderDuration.Record(sw.ElapsedMilliseconds);
return order;
}
}
| 指标类型 | 用途 | 示例 |
|---|---|---|
| Counter | 单调递增 | 请求总数 |
| UpDownCounter | 可增可减 | 在线连接数 |
| Histogram | 分布 | 延迟、包大小 |
| Gauge | 当前值 | 队列深度 |
// Prometheus 侧聚合查询示例
// 请求速率(1 分钟窗口)
rate(http_server_request_duration_seconds_count[1m])
// 99 分位延迟
histogram_quantile(0.99,
sum(rate(http_server_request_duration_seconds_bucket[5m])) by (le))
// 按路由聚合错误率
sum(rate(http_server_requests_total{status=~"5.."}[5m])) by (route)
/ sum(rate(http_server_requests_total[5m])) by (route)
避坑: 指标命名要遵循约定(点分结构
orders.created),带维度(如method、route)才有聚合价值,但维度爆炸(cardinality 过高)会拖垮存储。别把用户 ID 之类高基数值当指标维度。
6. 健康检查与探活
一句话总结: 健康检查让编排平台知道实例是否可服务,区分「进程活着」与「依赖可用」,是故障摘除的前提。
ASP.NET Core 内置健康检查:AddHealthChecks 注册检查项,MapHealthChecks 暴露端点。可以分三类:Liveness(进程活)、Readiness(依赖可用、可接流量)、Startup(初始化完成)。Kubernetes 的 startupProbe/livenessProbe/readinessProbe 分别对应。
builder.Services.AddHealthChecks()
.AddDbContextCheck<OrderDbContext>() // 数据库
.AddRedis("redis:6379") // 缓存
.AddUrlGroup(new Uri("https://payments.internal"), "payments"); // 下游
var app = builder.Build();
// 存活探针:进程活着
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
Predicate = _ => false // 不执行具体检查,只回 200
});
// 就绪探针:依赖可用才接流量
app.MapHealthChecks("/readyz", new HealthCheckOptions
{
Predicate = _ => true
});
| 探针 | 端点 | 失败后果 |
|---|---|---|
| Liveness | /healthz | 重启容器 |
| Readiness | /readyz | 从负载均衡摘除 |
| Startup | /startupz | 启动阶段探活 |
避坑: 健康检查别去检查「不关键」的依赖——比如检查一个不重要的报表库,挂了就把整个实例摘了,放大故障。健康端点本身要轻量(低超时),别做重查询。
7. 日志治理与容量管控
一句话总结: 日志治理是「质量 + 成本」工程:控制级别、过滤噪音、脱敏敏感数据、设定保留周期,让日志系统可持续运转。
日志量失控会从「可观测性」变成「成本黑洞」。治理手段:按环境设默认级别、过滤健康检查与静态资源噪音、敏感字段脱敏、分级保留(热数据短、冷数据长)。日志是「写给未来的自己排查用的」,垃圾日志与缺失日志一样有害。
// 过滤噪音:健康检查与静态资源不进日志
builder.Services.AddLogging(logging =>
{
logging.AddFilter("Microsoft.AspNetCore.HealthChecks", LogLevel.Warning);
logging.AddFilter("Microsoft.AspNetCore.StaticFiles", LogLevel.Warning);
});
// 脱敏:敏感字段在写入前打码
public static string MaskPhone(string phone)
=> phone.Length >= 7 ? $"{phone[..3]}****{phone[^4..]}" : "****";
logger.LogInformation("用户注册 Phone={Phone}", MaskPhone(user.Phone));
| 治理项 | 手段 |
|---|---|
| 级别基线 | 生产 Information,Debug 只对特定类 |
| 噪音过滤 | AddFilter 按类别压制 |
| 脱敏 | 写入前 Mask 或结构化忽略 |
| 保留策略 | 按日滚动 + 容量限制 |
| 告警 | 只对 Error/Critical 与关键指标告警 |
避坑: 别把所有 Exception 都打 Error——可预期、已优雅降级的异常应该打 Warning,否则告警风暴让团队麻木。日志字段要克制,每行日志的价值密度要高,配合
traceId关联,出问题能 3 分钟定位。
8. 总结
| 主题 | 要点 |
|---|---|
| ILogger | 占位符结构化、按级别与类别过滤 |
| 结构化日志 | 字段稳定、可查询、可聚合 |
| Serilog | sink 多样化,Enrich + JSON 输出 |
| 链路追踪 | OTel 自动埋点,traceId 贯穿调用链 |
| 指标 | Counter/Histogram 反映吞吐与延迟 |
| 健康检查 | Liveness/Readiness 分层探活 |
| 日志治理 | 级别基线、噪音过滤、脱敏、保留策略 |
可观测性的三角是日志、指标、追踪:日志回答「发生了什么」,指标回答「有多严重」,追踪回答「发生在哪条链路上」。ASP.NET Core + OpenTelemetry 让三者的采集成本大幅降低——自动埋点、标准导出、traceId 关联。真正考验工程能力的是治理:控制日志量、稳定字段约定、脱敏敏感数据、让告警精准。当系统抖动时,靠这三角能在几分钟内定位到具体服务与代码路径,这就是可观测性的回报。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。