关于 opentelemetry
opentelemetry 是面向 .NET 开发者的 OpenTelemetry 实践指南,涵盖追踪、指标和日志的配置与使用。它还说明指标生命周期、活动空值处理和低基数标签等实现注意事项。
主要功能
- 01一次配置追踪、指标和日志
- 02通过 OTLP 导出遥测数据
- 03使用 IMeterFactory 创建指标
- 04创建自定义 ActivitySource 追踪
- 05提供 Aspire Dashboard 本地配置
适用场景
- 01为 .NET 应用配置三类遥测
- 02为业务流程添加自定义指标
- 03为分布式操作创建追踪跨度
- 04在本地开发中查看遥测数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill opentelemetry安装后在 Claude Code 中输入/opentelemetry确认已加载。
详细介绍
OpenTelemetry
核心原则
- 三大支柱,一次设置 — 通过一次
AddOpenTelemetry()调用配置跟踪、指标和日志。使用UseOtlpExporter()将数据统一导出到任何兼容 OTLP 的后端。 - 使用
IMeterFactory处理指标 — 切勿使用new创建Meter实例。工厂通过 DI 管理生命周期,并防止资源泄漏。 - Activity 可为 null — 没有附加监听器时,
StartActivity()会返回null。设置标签或事件时始终使用?.。 - 优先使用环境变量,而不是代码 — 使用
OTEL_EXPORTER_OTLP_ENDPOINT和OTEL_SERVICE_NAME,让部署环境无需修改代码即可控制遥测数据的路由。 - 使用低基数指标标签 — 每个指标的标签组合应保持在约 1000 个以内。对于用户 ID 或请求 ID 等高基数数据,请使用 span 属性或日志。
模式
包含全部三种信号的完整设置
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(
serviceName: builder.Environment.ApplicationName,
serviceVersion: "1.0.0"))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddSource("MyApp.Orders"))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddMeter("MyApp.Orders"))
.WithLogging() // no per-signal exporter here —
.UseOtlpExporter(); // UseOtlpExporter covers all three signals
// UseOtlpExporter replaces per-signal AddOtlpExporter calls. Never combine
// the two — mixing them throws NotSupportedException (see Anti-patterns).
OTLP 端点默认为 http://localhost:4317(gRPC)。可通过以下方式覆盖:
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
OTEL_SERVICE_NAME=MyApp.Api
使用 IMeterFactory 创建自定义指标
将指标类注册为单例。IMeterFactory 会通过 DI 处理 Meter 的释放。
public sealed class OrderMetrics
{
private readonly Counter<int> _ordersCreated;
private readonly Histogram<double> _orderDuration;
private readonly UpDownCounter<int> _activeOrders;
private readonly Gauge<double> _queueDepth;
public OrderMetrics(IMeterFactory meterFactory)
{
var meter = meterFactory.Create("MyApp.Orders");
_ordersCreated = meter.CreateCounter<int>(
"myapp.orders.created", "{orders}", "Number of orders created");
_orderDuration = meter.CreateHistogram<double>(
"myapp.orders.duration", "s", "Order processing duration",
advice: new InstrumentAdvice<double>
{
HistogramBucketBoundaries = [0.01, 0.05, 0.1, 0.5, 1, 5, 10]
});
_activeOrders = meter.CreateUpDownCounter<int>(
"myapp.orders.active", "{orders}", "Currently active orders");
_queueDepth = meter.CreateGauge<double>(
"myapp.orders.queue_depth", "{items}", "Current queue depth");
}
public void OrderCreated() => _ordersCreated.Add(1);
public void RecordDuration(double seconds) => _orderDuration.Record(seconds);
public void OrderStarted() => _activeOrders.Add(1);
public void OrderCompleted() => _activeOrders.Add(-1);
public void SetQueueDepth(double depth) => _queueDepth.Record(depth);
}
// Registration
builder.Services.AddSingleton<OrderMetrics>();
多维指标标签
三个或更少的标签不会产生分配。若标签更多,请使用 TagList。
// Allocation-free (3 or fewer tags)
_ordersCreated.Add(1,
new KeyValuePair<string, object?>("order.type", "standard"),
new KeyValuePair<string, object?>("payment.method", "credit_card"));
// 4+ tags — use TagList to avoid allocations
var tags = new TagList
{
{ "order.type", "standard" },
{ "payment.method", "credit_card" },
{ "region", "us-east" },
{ "priority", "high" }
};
_ordersCreated.Add(1, tags);
用于分布式跟踪的自定义 ActivitySource
public sealed class OrderService(ILogger<OrderService> logger)
{
private static readonly ActivitySource Source = new("MyApp.Orders");
public async Task<Order> ProcessOrderAsync(CreateOrderRequest request, CancellationToken ct)
{
using var activity = Source.StartActivity("ProcessOrder", ActivityKind.Internal);
activity?.SetTag("order.customer_id", request.CustomerId);
try
{
await ValidateOrder(request, ct);
activity?.AddEvent(new ActivityEvent("OrderValidated"));
var order = await SaveOrder(request, ct);
activity?.SetTag("order.id", order.Id.ToString());
activity?.SetStatus(ActivityStatusCode.Ok);
return order;
}
catch (Exception ex)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
activity?.RecordException(ex);
throw;
}
}
}
注册来源:在跟踪构建器中使用 .AddSource("MyApp.Orders")。
用于本地开发的 Aspire Dashboard
在不使用 Aspire 编排的情况下运行独立的 Aspire Dashboard:
docker run --rm -it -p 18888:18888 -p 4317:18889 \
mcr.microsoft.com/dotnet/aspire-dashboard:latest
然后将应用指向它:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
Dashboard UI 位于 http://localhost:18888。
使用 OTel 的源生成日志
要获得最高性能,请使用 [LoggerMessage]——它可消除装箱和内存分配。
public partial class OrderService(ILogger<OrderService> logger)
{
[LoggerMessage(Level = LogLevel.Information,
Message = "Processing order {OrderId} for customer {CustomerId}")]
partial void LogOrderProcessing(Guid orderId, Guid customerId);
}
当前存在 Activity 时,OpenTelemetry 日志记录会自动包含 TraceId 和 SpanId。
反模式
不要为每个请求创建 Meter
// BAD — new Meter per request causes memory leaks
public void HandleRequest()
{
var meter = new Meter("MyApp");
meter.CreateCounter<int>("requests").Add(1);
}
// GOOD — singleton via IMeterFactory
public class MyMetrics(IMeterFactory meterFactory)
{
private readonly Counter<int> _requests =
meterFactory.Create("MyApp").CreateCounter<int>("myapp.requests");
public void RequestHandled() => _requests.Add(1);
}
不要跳过 Activity 的 null 检查
// BAD — NullReferenceException when no listener is attached
using var activity = source.StartActivity("Work");
activity.SetTag("key", "value");
// GOOD — null-safe
activity?.SetTag("key", "value");
不要使用高基数指标标签
// BAD — unbounded cardinality causes memory explosion in collectors
_counter.Add(1, new("request.id", Guid.NewGuid().ToString()));
_counter.Add(1, new("user.id", userId));
// GOOD — low-cardinality dimensions only
_counter.Add(1, new("http.method", "GET"), new("http.status_code", 200));
不要混用 UseOtlpExporter 和 AddOtlpExporter
// BAD — throws NotSupportedException at runtime
builder.Services.AddOpenTelemetry()
.UseOtlpExporter()
.WithTracing(t => t.AddOtlpExporter());
// GOOD — use one approach
builder.Services.AddOpenTelemetry().UseOtlpExporter();
不要忘记注册自定义来源
// BAD — activities silently dropped (no listener registered)
var source = new ActivitySource("MyApp.Custom");
using var activity = source.StartActivity("Work"); // null!
// GOOD — register in the tracing builder
otel.WithTracing(t => t.AddSource("MyApp.Custom"));
otel.WithMetrics(m => m.AddMeter("MyApp.Custom"));
决策指南
| 场景 | 建议 |
|---|---|
| 完整的可观测性设置 | 使用 AddOpenTelemetry() 配置全部三种信号,并添加 UseOtlpExporter() |
| 自定义业务指标 | IMeterFactory + 单例指标类 |
| 自定义跟踪 span | ActivitySource + StartActivity() |
| 本地开发后端 | Aspire Dashboard 独立容器 |
| 生产后端 | 使用 OTel Collector 作为中间层,连接到 Grafana/Datadog 等 |
| 生产环境中的采样 | OTEL_TRACES_SAMPLER=parentbased_traceidratio,采样比例为 10% |
| 高性能日志记录 | [LoggerMessage] 源生成器 |
| 指标标签基数 | 每个指标最多约 1000 种组合 |
| 环境配置 | OTEL_* 环境变量(也可通过 appsettings.json 使用) |
评论
支持 Markdown