.NET 项目集成可观测性:从架构思考到落地实践需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。
这篇内容以一个真实的.NET 9项目集成可观测性栈的经历,分享从"为什么做"到"怎么落地"的完整过程。
项目中有一个业务接口,涉及多表关联、缓存查询、外部服务调用,业务联链条很长。上线后用户反馈"慢",但具体慢在哪一步?是数据库查询?是外部调用?还是序列化?靠日志很难定位,本地复现也困难。
我的诉求很简单:想上好监控,做到心里有数。
这就是我们集成可观测性的起点——不是为了"高大上",而是为了解决一个具体的问题。
此次集成,用到的核心组件有4个,OpenTelemetry,Prometheus,jaeger,grafana,在这之前,日志的部分我们已经集成到了ELK,所以整个可观测性的架构设计图如下
| 组件 | 作用 | 说明 |
|---|---|---|
| OpenTelemetry Collector | 数据中枢 | 接收应用推来的 OTLP 数据,分发给后端 |
| Prometheus | 指标存储 | 存 QPS、延迟、内存等时序数据 |
| Jaeger | 链路追踪 | 存每次请求的完整调用链 |
| Grafana | 可视化 | 把Prometheus和Jaeger的数据做成图表 |
| ElasticSearch | 日志存储 | 通过Logstash将系统产生的各类日志传输存储 |
注意:本篇后续不过多涉及ELK相关内容,这里点出来是因为和可观测性组件产生了交集。此外,本文演示的相关中间件的部署环境都是基于Windows,大家部署时可以根据实际情况选择部署环境。
我这里的数据可观测组件部署情况是,只在1台中心服务器上部署了Jaeger,Prometheus和Grafana,而所有应用节点服务器都通过Otel将数据推送到中心服务器,也就是webapi的trace/metric全部汇聚到中心服务器,只提供一个面板地址,同一排查。
这套架构在我这里的场景验证是有效的,适合中小型项目。
Collector是整个栈的核心枢纽,应用只跟它通信,它负责把数据分发给Prometheus和Jaeger。
核心配置:
receivers:otlp:protocols:grpc:endpoint:0.0.0.0:4317# 应用通过这个端口推数据过来processors:batch:timeout:5s# 攒批超时,减少网络开销memory_limiter:limit_mib:512# 防止数据堆积导致OOMexporters:prometheus:endpoint:"0.0.0.0:8889"# 暴露给Prometheus抓取otlp/jaeger:endpoint:127.0.0.1:14250# traces推给Jaegertls:insecure:true# 内网不需要TLSservice:pipelines:metrics:receivers: [otlp] exporters: [prometheus] traces:receivers: [otlp] exporters: [otlp/jaeger]关键点:
receivers 定义数据怎么进来(OTLP gRPC)exporters 定义数据往哪去(Prometheus收指标,Jaeger收traces)processors 做批量和内存保护namespace 前缀,否则Grafana查询metric名会不匹配Prometheus 定时去 OTel Collector 拉数据(pull 模式)。
核心配置:
global:scrape_interval:15s# 每 15 秒抓一次scrape_configs:-job_name:"otel-collector"static_configs:-targets: ["localhost:8889"] # Collector的Prometheus exporter端口启动参数:
prometheus.exe --config.file=prometheus.yml --storage.tsdb.retention.time=30dretention.time=30d:数据保留 30 天,自动清理Jaeger负责存储和查询traces,可以看到每次请求的完整调用链。
核心配置(Jaeger v2格式):
receivers:otlp:protocols:grpc:endpoint:0.0.0.0:14250# 接收 Collector 推送的 tracesexporters:jaeger_storage_exporter:# 写入存储(必须!否则UI为空)trace_storage:jaeger_storageextensions:jaeger_query:# Web UIstorage:traces:jaeger_storagejaeger_storage:# 内存存储后端backends:jaeger_storage:memory:max_traces:100000service:pipelines:traces:receivers: [otlp] exporters: [jaeger_storage_exporter] # 必须包含!踩坑提醒:traces pipeline必须包含 jaeger_storage_exporter,否则Jaeger UI里Service下拉为空,这是最常见的坑。
Grafana负责把数据做成图表,配置两个数据源即可:
datasources:-name:Prometheustype:prometheusurl:http://localhost:9090isDefault:true-name:Jaegertype:jaegerurl:http://localhost:16686部署顺序(很重要):
1. Jaeger ← 必须先启动(Collector 要连它的 14250)2. OTel Collector ← 接收应用数据3. Prometheus ← 抓取指标4. Grafana ← 可视化上述服务部署后控制台输出效果分别如下
.NET项目里只需要做一件事:通过OTel SDK把数据推给OTel Collector。后面的Prometheus、Jaeger、Grafana都是接收和展示,不需要应用关心。
NuGet 包:
<PackageReferenceInclude="OpenTelemetry.Extensions.Hosting"Version="1.15.3" /><PackageReferenceInclude="OpenTelemetry.Instrumentation.AspNetCore"Version="1.15.2" /><PackageReferenceInclude="OpenTelemetry.Instrumentation.Runtime"Version="1.15.1" /><PackageReferenceInclude="OpenTelemetry.Exporter.OpenTelemetryProtocol"Version="1.15.3" />服务注册(约 30 行代码):
privatestaticvoidConfigureOpenTelemetry(this WebApplicationBuilder builder){ string otlpEndpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"] ?? "http://localhost:4317"; builder.Services.AddOpenTelemetry() .WithTracing(tracing => { tracing .AddAspNetCoreInstrumentation() // 自动采集HTTP请求traces .AddSource("Declaration.*") // 通配自定义Span .ConfigureResource(r => r.AddService("Magic.Declaration.WebAPI")) .AddOtlpExporter(options => { options.Endpoint = new Uri(otlpEndpoint); options.ExportProcessorType = ExportProcessorType.Batch; }); }) .WithMetrics(metrics => { metrics .AddAspNetCoreInstrumentation() // 自动采集HTTP请求指标 .AddRuntimeInstrumentation() // .NET运行时指标(GC、CPU、内存) .ConfigureResource(r => r.AddService("Magic.Declaration.WebAPI")) .AddOtlpExporter(options => { options.Endpoint = new Uri(otlpEndpoint); }); });}采集信息类别:
| 类别 | 指标 | 说明 |
|---|---|---|
| HTTP 请求 | QPS、延迟分布、状态码 | Instrumentation.AspNetCore 自动采集 |
| .NET 运行时 | GC、CPU、内存、线程池 | Instrumentation.Runtime 自动采集 |
为了保持开发环境的高效,且能看到采集到的基本数据,我们可以在开发环境配置将采集的数据输出到控制台。
if (builder.Environment.IsDevelopment()){ tracing.AddConsoleExporter(); // traces 输出到控制台}启动应用后,控制台会直接输出每次请求的 trace 信息:
Activity.TraceId: 8d35e...Activity.SpanId: a7f2e...Activity.DisplayName: GET /api/order/detailActivity.Kind: ServerActivity.Duration: 00:00:00.1234Resource associated with Activity: service.name: MyApp.WebAPI效果如下
开发阶段用控制台就够了,不用装任何额外组件。
这套可观测性栈是锦上添花型接入,不是项目运行的必要条件:
http://localhost:4317对业务代码零侵入:所有配置都收敛在服务注册阶段,业务代码不需要任何改动。
部署完成后,先验证数据是否流通:
# 1. 检查 Collector 是否在接收数据curl http://localhost:8888/metrics | findstr "accepted_spans"# 2. 检查 Prometheus 是否有指标curl "http://localhost:9090/api/v1/query?query=http_server_request_duration_seconds_count"# 3. 检查 Jaeger 是否有 tracescurl http://localhost:16686/api/servicesJaeger UI地址:http://localhost:16686
定位链条:
打开Jaeger面板后,点击某条Trace,展开看完整的调用链:
[HTTP GET /api/order/detail] 120ms ├── [MyApp.CreateOrder] 95ms ← 你的自定义 Span │ ├── [FreeSql Query] 80ms ← 数据库查询 │ └── [Redis Get] 5ms ← 缓存读取 └── [HTTP Response] 10msTips
链路追踪数据除了在Jaeger面板查看,也可以集成到Grafana,统一管理,本篇受篇幅限制不在赘述,下面分别给出Jaeger和Grafana的面板效果。
常见场景:
| 场景 | 操作 |
|---|---|
| 接口慢但不知道慢在哪 | 搜该接口的Trace → 看哪个Span耗时最长 |
| 间歇性超时 | 搜Duration > 1s的Trace → 对比正常和异常的差异 |
| 500 错误 | 搜Status = ERROR的Trace → 看Logs中的异常信息 |
Grafana UI地址:http://localhost:3000
第一步:添加数据源(如果用 provisioning 文件部署则自动配置)
Configuration → Data Sources → Add data source → 选 Prometheus 和 Jaeger
第二步:创建 Dashboard
推荐先建一个包含以下面板的Dashboard:
| 面板 | 数据源 | PromQL |
|---|---|---|
| 实时QPS | Prometheus | sum(rate(http_server_request_duration_seconds_count[1m])) |
| P95延迟 | Prometheus | histogram_quantile(0.95, sum(rate(http_server_request_duration_seconds_bucket[5m])) by (le)) |
| 错误率 | Prometheus | sum(rate(http_server_request_duration_seconds_count{http_response_status_code=~"5.."}[5m])) / sum(rate(http_server_request_duration_seconds_count[5m])) |
| 内存占用 | Prometheus | dotnet_gc_last_collection_heap_size_bytes |
| CPU使用率 | Prometheus | rate(dotnet_process_cpu_time_seconds_total[1m]) |
第三步:按路由分组
想看每个接口的独立指标,用 by (http_route) 分组:
sum(rate(http_server_request_duration_seconds_count[5m])) by (http_route, http_request_method)Dashboard 设计原则:
Grafana效果
注意:以上统计的方法,均可在Grafana社区官网查询到,也可以在这里(grafana.com/grafana/das…
集成可观测性的本质是让系统运行状态可见,而不是靠猜。
四个组件各司其职:
.NET 项目要做的很少:
可拔插、零侵入:
可观测性不是一次性任务,而是持续的过程——先跑起来,遇到问题再逐步深入。
OpenClaw到底能做什么?有什么用?推荐你先装这几个实用的Skills实践整理
Windows安装Claude Code的实践教程
Claude Code连接MySQL的保姆级教程实践整理
Ollama无法通过本地IP访问11434端口的解决方案实践整理
在Claude Code中配置DeepSeek从报错到成功调用的全流程实践整理
OpenClaw网关启动失败:配置文件权限错误的排查与修复指南实践整理