.NET 微服务监控避坑指南:告别盲翻日志,10 分钟搞定 OpenTelemetry 全链路追踪

作者 : 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. 架构速览:一图看懂可观测性目标

先明确要做的事,后续所有配置都围绕这张图展开:

flowchart LR A[.NET 10 核心服务] -- OTLP/gRPC --> C[OTel Collector] B[Nginx 前置网关] -- access log --> C C -- traces --> J[Jaeger] C -- metrics --> P[Prometheus] C -- logs --> L[Loki] J --> G[Grafana] P --> G L --> G G -. 告警 .-> AM[AlertManager]

核心设计思路

  • 单一出口:应用服务只关心一个出口(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. 实战排坑精华

以下是我在生产环境踩过的真实血泪坑:

  1. Serilog 与 OTel 双写的陷阱: 如果项目本来有 Serilog,会导致同一条日志被记了两次。建议:Serilog 仅做本地控制台或致命错误文件输出,链路里的全交由 OTel 接管。
  2. 自己监控自己 : 引入 HTTP 自动埋点后,Otel-Collector 的健康检查和数据上报会被抓进 Trace,产生大量"自己调自己"的无用 Span。解法:在 AddHttpClientInstrumentation 的 Filter 中排除 4317 端口的请求。
  3. SQL 参数脱敏问题 : 一旦开启了 SetDbStatementForText = true,SQL 的敏感参数会泄漏到日志中。必须在 Collector 增加 attributes 处理器把敏感信息正则替换,或在生产环境关闭它。
  4. 磁盘打满刺客:不做采样 : 没有配置 tail_sampling 时,一周的测试环境日志竟然达到 38G。加上了尾部采样策略(即:只 100% 保留报错和慢请求,其他概率抽样)后,存储量骤降 85%。
  5. 找不到 GC 数据? : .NET 的 Runtime 状态需要单独引入 OpenTelemetry.Instrumentation.Runtime 包并显式注册 AddRuntimeInstrumentation(),这是排查线上 CPU 飙高和内存泄漏的命脉。

5. 结语

从过去的"查日志全凭运气"到现在的"一图看全链路并带 SQL 耗时",这就是云原生时代规范化的力量。如果你还在被 .NET 庞大的单体应用折磨,引入 OTel 将是你破局的最好的第一步。

(深耕后端与云原生的探索者,期待你的点赞与交流!)

相关推荐
米高梅狮子1 小时前
07.基于LNMP架构部署blog应用和DaemonSet、Job
架构
毛骗导演2 小时前
Cladue Code 源码解析-键盘事件与 Vim 模式:parse-keypress 解析状态机
前端·架构
不甘先生2 小时前
Go 包引用架构指南:从 internal 隔离到破解循环依赖的实战手册
架构·golang
胡利光2 小时前
Context Engineering 实战 02|System Prompt 是架构决策,不是写说明书
java·架构·prompt
薛定猫AI2 小时前
【深度解析】Memo 2.5 Pro:面向长程 Agent 工作流的 MoE 大模型架构与实战接入
架构
SamDeepThinking2 小时前
秒杀系统的幂等,只做一层Redis判重远远不够
java·后端·架构
Ribou2 小时前
Kubernetes v1.35.2 基于 Cilium Gateway API 的服务访问架构
架构·kubernetes·gateway
米高梅狮子2 小时前
09.kube-proxy、Ingress和Network Policy
云原生·容器·架构·kubernetes·自动化
剑飞的编程思维2 小时前
传统制造业数字化转型|架构评估核心全维度清单
架构