OpenTelemetry 可观测性
大约 12 分钟约 3615 字
OpenTelemetry 可观测性
简介
可观测性(Observability)是现代云原生应用的三大支柱:日志(Logs)、指标(Metrics)、追踪(Traces)。OpenTelemetry 是 CNCF 的开放标准,提供统一的 API 和 SDK 采集这三类遥测数据。ASP.NET Core 通过 OpenTelemetry .NET SDK 可以自动采集 HTTP 请求、数据库调用、外部服务调用的遥测数据,发送到 Jaeger、Prometheus、Grafana 等后端。
三大支柱的关系
问题排查流程:
1. 告警触发(Metrics)
"订单服务 P99 延迟超过 2 秒"
2. 定位问题(Traces)
"TraceId: abc123 -> 发现数据库查询耗时 1.8 秒"
3. 分析根因(Logs)
"查询慢日志: SELECT * FROM orders WHERE ... 执行时间 1800ms"
三大支柱协作:
- Metrics -> 发现异常(什么时候出了问题)
- Traces -> 定位路径(哪个请求、哪个服务出了问题)
- Logs -> 分析原因(具体什么错误导致了问题)OpenTelemetry 架构
应用代码
|
v
[OpenTelemetry API] -- 标准 API(不绑定具体实现)
|
v
[OpenTelemetry SDK] -- 采集、处理、导出
|
+-- [Instrumentation] -- 自动仪表化(ASP.NET Core、EF Core、HttpClient)
+-- [Processor] -- 数据处理(采样、过滤、聚合、批量)
+-- [Exporter] -- 数据导出(OTLP、Prometheus、Console)
|
v
[Collector / 后端] -- Jaeger、Prometheus、Grafana、Elasticsearch特点
项目配置
NuGet 包
<!-- OpenTelemetry 核心包 -->
<PackageReference Include="OpenTelemetry" Version="1.8.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.8.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.0" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.8.0-rc.1" />
<!-- 自动仪表化 -->
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.8.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.8.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.GrpcNetClient" Version="1.8.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Redis" Version="1.0.0-rc9" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" Version="1.8.0-beta.1" />完整服务注册
// ============================================
// OpenTelemetry 完整配置
// ============================================
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = WebApplication.CreateBuilder(args);
// 服务名称和版本
var serviceName = builder.Configuration["OpenTelemetry:ServiceName"] ?? "MyApi";
var serviceVersion = builder.Configuration["OpenTelemetry:ServiceVersion"] ?? "1.0.0";
var otlpEndpoint = builder.Configuration["OpenTelemetry:OtlpEndpoint"] ?? "http://localhost:4317";
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
{
resource
.AddService(
serviceName: serviceName,
serviceVersion: serviceVersion)
.AddAttributes(new Dictionary<string, object>
{
["deployment.environment"] = builder.Environment.EnvironmentName,
["host.name"] = Environment.MachineName
});
})
.WithTracing(tracing =>
{
tracing
// 注册自定义 ActivitySource
.AddSource("MyApp.Orders")
.AddSource("MyApp.Payments")
.AddSource("MyApp.Inventory")
// ASP.NET Core 自动仪表化
.AddAspNetCoreInstrumentation(options =>
{
// 过滤不需要追踪的请求
options.Filter = ctx =>
!ctx.Request.Path.StartsWithSegments("/health") &&
!ctx.Request.Path.StartsWithSegments("/metrics") &&
!ctx.Request.Path.StartsWithSegments("/favicon.ico");
// 记录请求和响应头
options.FilterRequestHeaders = (ctx, header) =>
header.StartsWith("X-", StringComparison.OrdinalIgnoreCase);
options.FilterResponseHeaders = (ctx, header) =>
header.Equals("Content-Length", StringComparison.OrdinalIgnoreCase);
// 记录异常
options.RecordException = true;
})
// HttpClient 自动仪表化(外部服务调用)
.AddHttpClientInstrumentation(options =>
{
options.FilterHttpRequestMessage = (req) =>
!req.RequestUri?.Host.Contains("localhost") ?? true;
options.RecordException = true;
})
// SQL Client 自动仪表化(数据库调用)
.AddSqlClientInstrumentation(options =>
{
options.SetDbStatementForText = true; // 记录 SQL 语句
options.SetDbStatementForStoredProcedure = true;
options.RecordException = true;
options.EnableConnectionLevelAttributes = true;
})
// EF Core 自动仪表化
.AddEntityFrameworkCoreInstrumentation(options =>
{
options.Filter = (cmd) => !cmd.Command.CommandText.Contains("__EFMigrations");
})
// 采样策略
.SetSampler(new TraceIdRatioBasedSampler(0.1)) // 10% 采样率
// 导出器
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(otlpEndpoint);
})
.AddConsoleExporter(); // 开发环境控制台输出
})
.WithMetrics(metrics =>
{
metrics
// ASP.NET Core 指标(请求计数、延迟、错误率)
.AddAspNetCoreInstrumentation()
// HttpClient 指标
.AddHttpClientInstrumentation()
// .NET 运行时指标(GC、线程池、内存)
.AddRuntimeInstrumentation()
// 进程指标(CPU、内存)
.AddProcessInstrumentation()
// HTTP Listener 指标
.AddMeter("Microsoft.AspNetCore.Hosting")
.AddMeter("Microsoft.AspNetCore.Routing")
// 注册自定义 Meter
.AddMeter("MyApp.Orders")
.AddMeter("MyApp.Payments")
// 导出器
.AddPrometheusExporter() // Prometheus 抓取
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(otlpEndpoint);
})
.AddConsoleExporter(); // 开发环境控制台输出
});
var app = builder.Build();
// Prometheus 指标抓取端点
app.MapPrometheusScrapingEndpoint();
app.Run();采样策略
// ============================================
// 采样策略配置
// ============================================
// 1. 固定比例采样 — 适合高流量场景
tracing.SetSampler(new TraceIdRatioBasedSampler(0.1)); // 10% 采样
// 2. 自定义采样 — 根据条件决定是否采样
tracing.SetSampler(new CustomSampler());
// 3. 父子关联采样 — 如果父 Span 被采样,子 Span 也被采样
tracing.SetParentBasedSampler(new TraceIdRatioBasedSampler(0.1));
// ============================================
// 自定义采样器示例
// ============================================
public class CustomSampler : Sampler
{
public override SamplingResult ShouldSample(
in SamplingParameters samplingParameters)
{
var path = samplingParameters.Tags
.FirstOrDefault(t => t.Key == "http.route")
.Value?.ToString() ?? "";
// 关键路径 100% 采样
if (path.Contains("/api/orders") ||
path.Contains("/api/payments") ||
path.Contains("/api/auth/login"))
{
return new SamplingResult(SamplingDecision.RecordAndSample);
}
// 健康检查等不采样
if (path.StartsWith("/health") || path.StartsWith("/metrics"))
{
return new SamplingResult(SamplingDecision.Drop);
}
// 其他路径 10% 采样
return Random.Shared.NextDouble() < 0.1
? new SamplingResult(SamplingDecision.RecordAndSample)
: new SamplingResult(SamplingDecision.Drop);
}
}从配置文件读取
// appsettings.json
{
"OpenTelemetry": {
"ServiceName": "MyApi",
"ServiceVersion": "1.0.0",
"OtlpEndpoint": "http://otel-collector:4317",
"Tracing": {
"SamplingRate": 0.1,
"EnabledInstrumentations": ["AspNetCore", "HttpClient", "SqlClient", "EFCore"]
},
"Metrics": {
"EnabledInstrumentations": ["AspNetCore", "HttpClient", "Runtime", "Process"],
"PrometheusEndpoint": "/metrics"
}
}
}自定义指标
计数器和直方图
// ============================================
// 自定义业务指标
// ============================================
public class OrderMetrics
{
private readonly Counter<long> _ordersCreated;
private readonly Counter<long> _ordersFailed;
private readonly Histogram<double> _orderValue;
private readonly Histogram<double> _orderProcessingDuration;
private readonly UpDownCounter<int> _activeOrders;
private readonly ObservableGauge<int> _pendingOrders;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MyApp.Orders", "1.0.0");
// Counter: 只增不减的计数器(订单总数)
_ordersCreated = meter.CreateCounter<long>(
name: "orders.created.total",
unit: "{order}",
description: "创建订单总数");
// Counter: 失败计数(按错误类型分标签)
_ordersFailed = meter.CreateCounter<long>(
name: "orders.failed.total",
unit: "{error}",
description: "订单失败总数");
// Histogram: 值分布(订单金额分布)
_orderValue = meter.CreateHistogram<double>(
name: "orders.value",
unit: "CNY",
description: "订单金额分布");
// Histogram: 延迟分布(订单处理耗时)
_orderProcessingDuration = meter.CreateHistogram<double>(
name: "orders.processing.duration",
unit: "ms",
description: "订单处理耗时");
// UpDownCounter: 可增可减(当前活跃订单数)
_activeOrders = meter.CreateUpDownCounter<int>(
name: "orders.active",
unit: "{order}",
description: "当前活跃订单数");
}
public void RecordOrderCreated(decimal amount)
{
_ordersCreated.Add(1,
new("order.type", "standard"),
new("payment.method", "wechat"));
_orderValue.Record((double)amount);
_activeOrders.Add(1);
}
public void RecordOrderFailed(string errorType)
{
_ordersFailed.Add(1, new("error.type", errorType));
_activeOrders.Add(-1);
}
public void RecordOrderCompleted(decimal amount, double durationMs)
{
_orderValue.Record((double)amount);
_orderProcessingDuration.Record(durationMs);
_activeOrders.Add(-1);
}
}
// ============================================
// 注册和使用
// ============================================
builder.Services.AddSingleton<OrderMetrics>();
public class OrderService
{
private readonly OrderMetrics _metrics;
private readonly ILogger<OrderService> _logger;
public OrderService(OrderMetrics metrics, ILogger<OrderService> logger)
{
_metrics = metrics;
_logger = logger;
}
public async Task<Order> CreateAsync(CreateOrderRequest request)
{
var sw = Stopwatch.StartNew();
try
{
var order = await _repository.CreateAsync(request);
sw.Stop();
_metrics.RecordOrderCompleted(order.TotalAmount, sw.ElapsedMilliseconds);
_logger.LogInformation(
"订单创建成功: OrderId={OrderId}, Amount={Amount}, DurationMs={DurationMs}",
order.Id, order.TotalAmount, sw.ElapsedMilliseconds);
return order;
}
catch (Exception ex)
{
_metrics.RecordOrderFailed(ex.GetType().Name);
_logger.LogError(ex, "订单创建失败: UserId={UserId}", request.UserId);
throw;
}
}
}指标类型说明
| 指标类型 | 说明 | 适用场景 | 示例 |
|---|---|---|---|
| Counter | 只增不减 | 累计计数 | 订单总数、错误次数 |
| UpDownCounter | 可增可减 | 当前值 | 活跃连接数、队列长度 |
| Histogram | 值分布 | 延迟分析 | 请求延迟、订单金额 |
| Gauge | 瞬时值 | 快照 | CPU 使用率、内存占用 |
| ObservableCounter | 回调式累计 | 需要计算的累计值 | 数据库查询总数 |
| ObservableGauge | 回调式瞬时 | 需要计算的瞬时值 | 线程池利用率 |
自定义追踪
Span 和 Activity
// ============================================
// 自定义追踪 Span
// ============================================
using System.Diagnostics;
public class OrderService
{
// ActivitySource 名称必须与注册时一致
private static readonly ActivitySource ActivitySource = new("MyApp.Orders");
public async Task<OrderResult> ProcessOrderAsync(Order order)
{
// 创建自定义 Span
using var activity = ActivitySource.StartActivity("ProcessOrder");
activity?.SetTag("order.id", order.Id);
activity?.SetTag("order.amount", order.TotalAmount);
activity?.SetTag("order.user_id", order.UserId);
// 添加业务属性
activity?.SetTag("order.currency", "CNY");
activity?.SetTag("order.item_count", order.Items.Count);
// 添加事件(标记处理阶段的里程碑)
activity?.AddEvent(new ActivityEvent("开始处理订单",
tags: new ActivityTagsCollection
{
{ "order.id", order.Id }
}));
// 子操作 1:验证库存
using (ActivitySource.StartActivity("ValidateInventory"))
{
activity?.AddEvent(new ActivityEvent("验证库存"));
await ValidateInventoryAsync(order);
}
// 子操作 2:扣减库存
using (ActivitySource.StartActivity("DeductInventory"))
{
activity?.AddEvent(new ActivityEvent("扣减库存"));
await DeductInventoryAsync(order);
}
// 子操作 3:创建支付
using (ActivitySource.StartActivity("CreatePayment"))
{
activity?.AddEvent(new ActivityEvent("创建支付"));
await CreatePaymentAsync(order);
}
// 记录异常
try
{
await NotifyCustomerAsync(order);
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
activity?.SetStatus(ActivityStatusCode.Ok);
return new OrderResult { Success = true, OrderId = order.Id };
}
// 带标签的 Span
public async Task<bool> ValidateInventoryAsync(Order order)
{
using var activity = ActivitySource.StartActivity("ValidateInventory");
activity?.SetTag("inventory.product_ids",
string.Join(",", order.Items.Select(i => i.ProductId)));
// 业务逻辑...
return true;
}
}
// 注册 ActivitySource(必须!)
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.AddSource("MyApp.Orders");
tracing.AddSource("MyApp.Payments");
tracing.AddSource("MyApp.Inventory");
});跨服务追踪传播
// ============================================
// HttpClient 追踪传播 — 自动注入 TraceId 到下游请求
// ============================================
// 自动仪表化已经处理了追踪传播
// 当使用 IHttpClientFactory 时,追踪上下文会自动传播
builder.Services.AddHttpClient("PaymentService", client =>
{
client.BaseAddress = new Uri("https://payment-service/api/");
})
.AddHttpMessageHandler(() => new HttpClientTraceHandler()); // 自动注入
// ============================================
// 手动传播(如果需要自定义 HTTP 调用)
// ============================================
public class OrderService
{
private readonly HttpClient _httpClient;
private static readonly ActivitySource ActivitySource = new("MyApp.Orders");
public async Task<PaymentResult> CreatePaymentAsync(Order order)
{
using var activity = ActivitySource.StartActivity("CreatePayment");
var request = new HttpRequestMessage(HttpMethod.Post, "https://payment-service/api/payments")
{
Content = JsonContent.Create(new { OrderId = order.Id, Amount = order.TotalAmount })
};
// 注入追踪头
var currentActivity = Activity.Current;
if (currentActivity != null)
{
request.Headers.Add("traceparent",
$"00-{currentActivity.TraceId}-{currentActivity.SpanId}-01");
request.Headers.Add("tracestate", currentActivity.TraceStateString ?? "");
}
var response = await _httpClient.SendAsync(request);
return await response.Content.ReadFromJsonAsync<PaymentResult>() ?? new();
}
}结构化日志
日志与追踪关联
// ============================================
// 日志配置 — 自动注入 TraceId 和 SpanId
// ============================================
builder.Logging.ClearProviders();
builder.Logging.AddJsonConsole(options =>
{
options.JsonWriterOptions = new System.Text.Json.JsonWriterOptions
{
Indented = true
};
});
// 或者使用 Serilog
builder.Host.UseSerilog((context, services, configuration) =>
{
configuration
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.Enrich.WithProperty("Application", "MyApi")
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
.WriteTo.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] " +
"[TraceId:{TraceId}] [SpanId:{SpanId}] " +
"{Message:lj}{NewLine}{Exception}")
.WriteTo.Seq("http://localhost:5341");
});
// ============================================
// 在日志中使用
// ============================================
public class OrderController : ControllerBase
{
private readonly ILogger<OrderController> _logger;
public OrderController(ILogger<OrderController> logger)
{
_logger = logger;
}
[HttpPost("orders")]
public async Task<ActionResult> CreateOrder(CreateOrderRequest request)
{
// 结构化日志 — 使用占位符而非字符串插值
_logger.LogInformation(
"创建订单开始: UserId={UserId}, ItemCount={ItemCount}, Amount={Amount}",
request.UserId, request.Items?.Count ?? 0, request.Amount);
try
{
var order = await _orderService.CreateAsync(request);
_logger.LogInformation(
"订单创建成功: OrderId={OrderId}, DurationMs={DurationMs}",
order.Id, order.ProcessingTimeMs);
return Ok(order);
}
catch (InvalidOperationException ex)
{
// 业务异常 — 警告级别
_logger.LogWarning(ex,
"订单创建业务异常: UserId={UserId}, Reason={Reason}",
request.UserId, ex.Message);
return BadRequest(new { error = ex.Message });
}
catch (Exception ex)
{
// 系统异常 — 错误级别
_logger.LogError(ex,
"订单创建系统异常: UserId={UserId}, ExceptionType={ExceptionType}",
request.UserId, ex.GetType().Name);
throw;
}
}
}日志级别规范
LogLevel 使用规范:
Critical (500) — 系统级故障,需要立即处理
示例:数据库连接池耗尽、磁盘空间不足、应用无法启动
Error (400) — 影响单个请求的严重错误
示例:外部服务调用失败、数据库写入失败、消息队列发布失败
Warning (300) — 不影响主流程的异常情况
示例:限流触发、缓存未命中、降级处理、业务校验失败
Information (200) — 正常业务流程的关键节点
示例:订单创建成功、用户登录、支付完成、数据导入
Debug (100) — 开发调试信息(生产环境通常关闭)
示例:SQL 语句、中间件执行顺序、缓存键生成
Trace (0) — 最详细的追踪信息
示例:方法进入/退出、循环迭代健康检查集成
// ============================================
// 健康检查 + 指标
// ============================================
builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("PostgreSQL")!)
.AddRedis(builder.Configuration.GetConnectionString("Redis")!)
.AddUrlGroup(new Uri("https://payment-service/health"), "payment-service")
.AddCheck<KafkaHealthCheck>("kafka");
// 健康检查端点(含响应详情)
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
// 就绪检查(含依赖检查)
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
// 存活检查(仅检查进程是否存活)
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = _ => false // 不检查任何依赖
});生产环境最佳实践
配置分离
// appsettings.Production.json
{
"OpenTelemetry": {
"OtlpEndpoint": "http://otel-collector:4317",
"Tracing": {
"SamplingRate": 0.05
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Microsoft": "Warning",
"System": "Warning"
}
}
}性能优化
// ============================================
// 批量导出配置 — 减少网络开销
// ============================================
tracing.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(otlpEndpoint);
options.BatchExportProcessorOptions = new BatchExportProcessorOptions<Activity>
{
MaxExportBatchSize = 512, // 每批最大条目
MaxQueueSize = 2048, // 队列大小
ScheduledDelayMilliseconds = 5000, // 导出间隔
ExporterTimeoutMilliseconds = 30000 // 导出超时
};
});
metrics.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(otlpEndpoint);
options.PeriodicExportingMetricReaderOptions = new PeriodicExportingMetricReaderOptions
{
ExportIntervalMilliseconds = 30000, // 30 秒导出一次
ExportTimeoutMilliseconds = 10000
};
});优点
缺点
总结
OpenTelemetry 是云原生可观测性的标配。核心配置:AspNetCore + HttpClient + SqlClient 自动仪表化,OTLP 导出到 Jaeger/Grafana。自定义指标用 Meter + Counter/Histogram,自定义追踪用 ActivitySource + Activity。三大支柱统一采集,为故障排查和性能优化提供数据支撑。生产环境务必配置采样策略控制数据量,并使用 OTLP Collector 做集中处理和路由。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 采样率是生产环境控制成本的关键参数。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 生产环境 100% 采样导致数据量爆炸、成本失控。
- 忘记注册自定义 ActivitySource/Meter,导致自定义遥测数据丢失。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 研究基于 Metrics 的自动告警和 SLO/SLI 定义。
适用场景
- 当你准备把《OpenTelemetry 可观测性》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
- 从 RED 指标(Rate、Errors、Duration)开始建立监控基线。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
- 检查 ActivitySource/Meter 是否已注册到 OpenTelemetry。
- 检查 OTLP 端点是否可达:
curl http://otel-collector:4317。
复盘问题
- 如果把《OpenTelemetry 可观测性》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《OpenTelemetry 可观测性》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《OpenTelemetry 可观测性》最大的收益和代价分别是什么?
