本文基于一个真实的.NET 9项目集成可观测性栈的经历,分享从"为什么做"到"怎么落地"的完整过程。
一、为什么需要集成可观测性?
1.1 触发点:一个"慢"接口
项目中有一个业务接口,涉及多表关联、缓存查询、外部服务调用,业务联链条很长。上线后用户反馈"慢",但具体慢在哪一步?是数据库查询?是外部调用?还是序列化?靠日志很难定位,本地复现也困难。
我的诉求很简单:想上好监控,做到心里有数。
- 接口整体耗时多少?哪些步骤最耗时?
- 系统运行时的QPS、错误率、内存、CPU是什么水平?
- 出问题时能快速定位,而不是靠猜。
这就是我们集成可观测性的起点------不是为了"高大上",而是为了解决一个具体的问题。
二、可观测性架构设计
此次集成,用到的核心组件有4个,OpenTelemetry,Prometheus,jaeger,grafana,在这之前,日志的部分我们已经集成到了ELK,所以整个可观测性的架构设计图如下

| 组件 | 作用 | 说明 |
|---|---|---|
| OpenTelemetry Collector | 数据中枢 | 接收应用推来的 OTLP 数据,分发给后端 |
| Prometheus | 指标存储 | 存 QPS、延迟、内存等时序数据 |
| Jaeger | 链路追踪 | 存每次请求的完整调用链 |
| Grafana | 可视化 | 把Prometheus和Jaeger的数据做成图表 |
| ElasticSearch | 日志存储 | 通过Logstash将系统产生的各类日志传输存储 |
注意:本篇后续不过多涉及ELK相关内容,这里点出来是因为和可观测性组件产生了交集。
*2.1 部署架构
我这里的数据可观测组件部署情况是,只在1台中心服务器上部署了Jaeger,Prometheus和Grafana,而所有应用节点服务器都通过Otel将数据推送到中心服务器,也就是webapi的trace/metric全部汇聚到中心服务器,只提供一个面板地址,同一排查。
这套架构在我这里的场景验证是有效的,适合中小型项目。

2.2 OpenTelemetry Collector:数据中枢
Collector是整个栈的核心枢纽,应用只跟它通信,它负责把数据分发给Prometheus和Jaeger。
核心配置:
yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317 # 应用通过这个端口推数据过来
processors:
batch:
timeout: 5s # 攒批超时,减少网络开销
memory_limiter:
limit_mib: 512 # 防止数据堆积导致OOM
exporters:
prometheus:
endpoint: "0.0.0.0:8889" # 暴露给Prometheus抓取
otlp/jaeger:
endpoint: 127.0.0.1:14250 # traces推给Jaeger
tls:
insecure: true # 内网不需要TLS
service:
pipelines:
metrics:
receivers: [otlp]
exporters: [prometheus]
traces:
receivers: [otlp]
exporters: [otlp/jaeger]
关键点:
receivers定义数据怎么进来(OTLP gRPC)exporters定义数据往哪去(Prometheus收指标,Jaeger收traces)processors做批量和内存保护- 注意不要加
namespace前缀,否则Grafana查询metric名会不匹配
2.3 Prometheus:指标存储
Prometheus 定时去 OTel Collector 拉数据(pull 模式)。
核心配置:
yaml
global:
scrape_interval: 15s # 每 15 秒抓一次
scrape_configs:
- job_name: "otel-collector"
static_configs:
- targets: ["localhost:8889"] # Collector的Prometheus exporter端口
启动参数:
bash
prometheus.exe --config.file=prometheus.yml --storage.tsdb.retention.time=30d
retention.time=30d:数据保留 30 天,自动清理
2.4 Jaeger:链路追踪
Jaeger负责存储和查询traces,可以看到每次请求的完整调用链。
核心配置(Jaeger v2格式):
yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:14250 # 接收 Collector 推送的 traces
exporters:
jaeger_storage_exporter: # 写入存储(必须!否则UI为空)
trace_storage: jaeger_storage
extensions:
jaeger_query: # Web UI
storage:
traces: jaeger_storage
jaeger_storage: # 内存存储后端
backends:
jaeger_storage:
memory:
max_traces: 100000
service:
pipelines:
traces:
receivers: [otlp]
exporters: [jaeger_storage_exporter] # 必须包含!
踩坑提醒 :traces pipeline必须包含 jaeger_storage_exporter,否则Jaeger UI里Service下拉为空,这是最常见的坑。
2.5 Grafana:可视化
Grafana负责把数据做成图表,配置两个数据源即可:
yaml
datasources:
- name: Prometheus
type: prometheus
url: http://localhost:9090
isDefault: true
- name: Jaeger
type: jaeger
url: http://localhost:16686
部署顺序(很重要):
plain
1. Jaeger ← 必须先启动(Collector 要连它的 14250)
2. OTel Collector ← 接收应用数据
3. Prometheus ← 抓取指标
4. Grafana ← 可视化
上述服务部署后控制台输出效果分别如下
- jaeger服务启动后控制台输出

- 启动Otel Collector后传输数据效果如下

- 启动Prometheus后控制台输出效果如下

- grafana启动后控制台输出

三、项目代码
3.1 和OpenTelemetry通信
.NET项目里只需要做一件事:通过OTel SDK把数据推给OTel Collector。后面的Prometheus、Jaeger、Grafana都是接收和展示,不需要应用关心。
NuGet 包:
xml
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
服务注册(约 30 行代码):
csharp
private static void ConfigureOpenTelemetry(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 自动采集 |
3.2 开发环境
为了保持开发环境的高效,且能看到采集到的基本数据,我们可以在开发环境配置将采集的数据输出到控制台。
csharp
if (builder.Environment.IsDevelopment())
{
tracing.AddConsoleExporter(); // traces 输出到控制台
}
启动应用后,控制台会直接输出每次请求的 trace 信息:
plain
Activity.TraceId: 8d35e...
Activity.SpanId: a7f2e...
Activity.DisplayName: GET /api/order/detail
Activity.Kind: Server
Activity.Duration: 00:00:00.1234
Resource associated with Activity:
service.name: MyApp.WebAPI
效果如下

开发阶段用控制台就够了,不用装任何额外组件。
3.3 可拔插设计
这套可观测性栈是锦上添花型接入,不是项目运行的必要条件:
- 不装 OTel Collector:应用正常跑,只是不采集数据
- 不装 Prometheus/Jaeger:Collector 可以只做控制台输出
- 环境变量不配 :代码里有默认值
http://localhost:4317
对业务代码零侵入:所有配置都收敛在服务注册阶段,业务代码不需要任何改动。
四、怎么验证和分析采集到的数据
4.1 验证数据流
部署完成后,先验证数据是否流通:
bash
# 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 是否有 traces
curl http://localhost:16686/api/services
4.2 Jaeger:链路追踪分析
Jaeger UI地址:http://localhost:16686
定位链条:
打开Jaeger面板后,点击某条Trace,展开看完整的调用链:
plain
[HTTP GET /api/order/detail] 120ms
├── [MyApp.CreateOrder] 95ms ← 你的自定义 Span
│ ├── [FreeSql Query] 80ms ← 数据库查询
│ └── [Redis Get] 5ms ← 缓存读取
└── [HTTP Response] 10ms
Tips
链路追踪数据除了在Jaeger面板查看,也可以集成到Grafana,统一管理,本篇受篇幅限制不在赘述,下面分别给出Jaeger和Grafana的面板效果。
- jaeger面板

- grafana面板

常见场景:
| 场景 | 操作 |
|---|---|
| 接口慢但不知道慢在哪 | 搜该接口的Trace → 看哪个Span耗时最长 |
| 间歇性超时 | 搜Duration > 1s的Trace → 对比正常和异常的差异 |
| 500 错误 | 搜Status = ERROR的Trace → 看Logs中的异常信息 |
4.3 Grafana:配置Dashboard
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) 分组:
plain
sum(rate(http_server_request_duration_seconds_count[5m])) by (http_route, http_request_method)
Dashboard 设计原则:
- 顶部放概览数字(QPS、延迟、错误率、CPU)
- 中间放趋势图(按时间变化)
- 底部放明细表(按接口分组)
Grafana效果

注意:以上统计的方法,均可在Grafana社区官网查询到,也可以在这里(grafana.com/grafana/das...
五、总结
集成可观测性的本质是让系统运行状态可见,而不是靠猜。
四个组件各司其职:
- OpenTelemetry Collector 做数据中枢
- Prometheus 存指标
- Jaeger 存链路
- Grafana 做展示
.NET 项目要做的很少:
- 接入 OTel SDK,配置 OTLP 导出地址
- 自动采集 HTTP 请求和运行时指标
- 需要时加自定义 Span 定位业务逻辑
- 开发阶段控制台就能看,生产环境按需部署
可拔插、零侵入:
- 不装组件不影响业务运行
- 配置都在服务注册阶段,业务代码无感
- 环境变量控制接入程度
可观测性不是一次性任务,而是持续的过程------先跑起来,遇到问题再逐步深入。