作者 : jiangbo_dev 技术栈 : .NET 10 + OpenTelemetry + Grafana 生态 内容标签: 微服务、可观测性、.NET、OpenTelemetry、架构设计
工作 11 年,我维护过的 .NET 项目从 .NET Framework 4.0 一路演进到了 .NET 10。 日志 我们用过 log4net、NLog、Serilog;监控 写过直接存 SQL Server 然后用 Grafana 反查的"土法监控";至于链路追踪 ?很多时候还停留在 Stopwatch + _logger.LogInformation("耗时:{ms}ms") 的石器时代。
一旦业务上了微服务或迁移到 Docker/k8s,这类土办法的问题立刻现形:
- 某个接口变慢,日志里一堆
RequestId: xxx,但跨服务根本追不下去; - Redis、PG、HTTP Client 各报各的,无法在一张图上看清「这次请求到底卡在哪」;
- 容器一旦重启,本地日志直接丢失,定位问题全靠运气。
直到全面接入 OpenTelemetry,系统才真正拥有了"上帝视角"。本文将为你复盘如何在 .NET 架构下从 0 到 1 落地 OpenTelemetry,内含避坑指南、代码配置以及可直接复用的 docker-compose 编排。
1. 架构速览:一图看懂可观测性目标
先明确要做的事,后续所有配置都围绕这张图展开:
核心设计思路:
- 单一出口:应用服务只关心一个出口(OTLP → Collector),不直接耦合任何后端(解耦强依赖);
- 旁路处理:Collector 负责分流、过滤和采样,后端随时可换(今天 Jaeger,明天 Tempo);
- 视图统一:Grafana 是唯一的"看图入口",一个面板联动看 Trace / Metrics / Logs。
2. .NET 侧接入:5 个 NuGet 包搞定
首先,在你的 ASP.NET Core 项目中引入以下包:
xml
<ItemGroup>
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.StackExchangeRedis" Version="1.9.0-beta.1" />
</ItemGroup>
⚠️ 坑警报 :
Instrumentation.SqlClient虽在 beta,但在生产环境强烈建议加上,否则数据库耗时无法体现在链路中。如果是 SqlSugar/EF Core,底层走 ADO.NET 都会自动生效。
核心 Program.cs 配置 (可直接复用)
csharp
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
const string ServiceName = "app_service_core";
var serviceVersion = "1.0.0";
var resource = ResourceBuilder.CreateDefault()
.AddService(serviceName: ServiceName, serviceVersion: serviceVersion)
.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] = builder.Environment.EnvironmentName,
["host.name"] = Environment.MachineName // 注意 Docker 下会变成容器 ID
});
builder.Services.AddOpenTelemetry()
.ConfigureResource(r => r.AddService(ServiceName, serviceVersion: serviceVersion))
.WithTracing(t => t
.AddAspNetCoreInstrumentation(o => {
o.RecordException = true;
o.Filter = ctx => !ctx.Request.Path.StartsWithSegments("/health"); // 忽略健康检查
})
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation(o => {
o.SetDbStatementForText = true; // 记录完整 SQL 语句
o.RecordException = true;
})
.AddRedisInstrumentation()
.AddSource($"{ServiceName}.*") // 收集自定义业务 Span
.AddOtlpExporter(o => {
o.Endpoint = new Uri(builder.Configuration["Otel:Endpoint"]!);
o.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
}))
.WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation() // 必须引入单独的包,收集 GC/线程池指标
.AddProcessInstrumentation()
.AddOtlpExporter(o => {
o.Endpoint = new Uri(builder.Configuration["Otel:Endpoint"]!);
}));
// 日志接管
builder.Logging.AddOpenTelemetry(o =>
{
o.SetResourceBuilder(resource);
o.IncludeFormattedMessage = true;
o.IncludeScopes = true;
o.AddOtlpExporter(opt => opt.Endpoint = new Uri(builder.Configuration["Otel:Endpoint"]!));
});
3. Collector 与存储后端:docker-compose 开箱即用
如果你没上 k8s,只需要用 docker-compose 就能拉起一整套本地/单机生产可用的观测平台。
docker-compose.observability.yml
yaml
version: "3.8"
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.103.0
command: ["--config=/etc/otel-collector.yaml"]
volumes:
- ./otel-collector.yaml:/etc/otel-collector.yaml:ro
ports:
- "4317:4317" # gRPC
- "4318:4318" # HTTP
jaeger:
image: jaegertracing/all-in-one:1.57
environment:
- COLLECTOR_OTLP_ENABLED=true
ports:
- "16686:16686"
prometheus:
image: prom/prometheus:v2.52.0
ports:
- "9090:9090"
loki:
image: grafana/loki:3.0.0
ports:
- "3100:3100"
grafana:
image: grafana/grafana:11.0.0
ports:
- "3000:3000"
OTel Collector 核心配置 (otel-collector.yaml)
yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
# 强烈建议配置尾部采样!降低 80% 无用日志存储!
tail_sampling:
decision_wait: 10s
policies:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 500 }
- name: rest
type: probabilistic
probabilistic: { sampling_percentage: 10 } # 正常请求只采 10%
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls: { insecure: true }
prometheus:
endpoint: 0.0.0.0:8889
loki:
endpoint: http://loki:3100/loki/api/v1/push
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling, batch]
exporters: [otlp/jaeger]
# Metrics 和 Logs 配置同理省略...
4. 实战排坑精华
以下是我在生产环境踩过的真实血泪坑:
- Serilog 与 OTel 双写的陷阱: 如果项目本来有 Serilog,会导致同一条日志被记了两次。建议:Serilog 仅做本地控制台或致命错误文件输出,链路里的全交由 OTel 接管。
- 自己监控自己 : 引入 HTTP 自动埋点后,Otel-Collector 的健康检查和数据上报会被抓进 Trace,产生大量"自己调自己"的无用 Span。解法:在
AddHttpClientInstrumentation的 Filter 中排除4317端口的请求。 - SQL 参数脱敏问题 : 一旦开启了
SetDbStatementForText = true,SQL 的敏感参数会泄漏到日志中。必须在 Collector 增加attributes处理器把敏感信息正则替换,或在生产环境关闭它。 - 磁盘打满刺客:不做采样 : 没有配置
tail_sampling时,一周的测试环境日志竟然达到 38G。加上了尾部采样策略(即:只 100% 保留报错和慢请求,其他概率抽样)后,存储量骤降 85%。 - 找不到 GC 数据? : .NET 的 Runtime 状态需要单独引入
OpenTelemetry.Instrumentation.Runtime包并显式注册AddRuntimeInstrumentation(),这是排查线上 CPU 飙高和内存泄漏的命脉。
5. 结语
从过去的"查日志全凭运气"到现在的"一图看全链路并带 SQL 耗时",这就是云原生时代规范化的力量。如果你还在被 .NET 庞大的单体应用折磨,引入 OTel 将是你破局的最好的第一步。
(深耕后端与云原生的探索者,期待你的点赞与交流!)