opentelemetry

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

OpenTelemetry

OpenTelemetry

Core Principles

核心原则

  1. Three pillars, one setup — Configure traces, metrics, and logs through a single
    AddOpenTelemetry()
    call. Use
    UseOtlpExporter()
    for cross-cutting export to any OTLP-compatible backend.
  2. Use
    IMeterFactory
    for metrics
    — Never create
    Meter
    instances with
    new
    . The factory manages lifetime through DI and prevents leaks.
  3. Null-safe activities
    StartActivity()
    returns
    null
    when no listener is attached. Always use
    ?.
    when setting tags or events.
  4. Environment variables over code — Use
    OTEL_EXPORTER_OTLP_ENDPOINT
    and
    OTEL_SERVICE_NAME
    so deployments control telemetry routing without code changes.
  5. Low-cardinality metric tags — Keep metric tag combinations under ~1000 per instrument. Use span attributes or logs for high-cardinality data like user IDs or request IDs.
  1. 三大支柱,一套配置 — 通过单次
    AddOpenTelemetry()
    调用配置链路追踪、指标与日志。使用
    UseOtlpExporter()
    实现跨信号导出至任意兼容OTLP的后端。
  2. 使用
    IMeterFactory
    管理指标
    — 切勿通过
    new
    创建
    Meter
    实例。工厂通过依赖注入(DI)管理生命周期,避免内存泄漏。
  3. 空安全的Activity — 当没有监听器附着时,
    StartActivity()
    会返回
    null
    。设置标签或事件时务必使用
    ?.
    操作符。
  4. 环境变量优先于代码配置 — 使用
    OTEL_EXPORTER_OTLP_ENDPOINT
    OTEL_SERVICE_NAME
    ,让部署环节无需修改代码即可控制遥测路由。
  5. 低基数指标标签 — 每个指标工具的标签组合数需保持在约1000以内。用户ID、请求ID等高基数数据应使用Span属性或日志记录。

Patterns

实践模式

Full Setup with All Three Signals

全信号完整配置

csharp
// 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).
The OTLP endpoint defaults to
http://localhost:4317
(gRPC). Override via:
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
OTEL_SERVICE_NAME=MyApp.Api
csharp
// 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()             // 此处无需为单个信号配置导出器 —
    .UseOtlpExporter();        // UseOtlpExporter可覆盖所有三类信号

// UseOtlpExporter替代了为单个信号调用AddOtlpExporter的方式。切勿将两者混用
// 混用会抛出NotSupportedException(详见反模式章节)。
OTLP端点默认值为
http://localhost:4317
(gRPC)。可通过以下方式覆盖:
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4317
OTEL_SERVICE_NAME=MyApp.Api

Custom Metrics with IMeterFactory

基于IMeterFactory的自定义指标

Register a metrics class as a singleton.
IMeterFactory
handles
Meter
disposal through DI.
csharp
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>();
将指标类注册为单例。
IMeterFactory
通过DI处理
Meter
的销毁。
csharp
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);
}

// 注册
builder.Services.AddSingleton<OrderMetrics>();

Multi-Dimensional Metric Tags

多维指标标签

Three or fewer tags are allocation-free. For more, use
TagList
.
csharp
// 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);
三个及以下标签无需分配内存。超过三个时,请使用
TagList
csharp
// 无内存分配(3个及以下标签)
_ordersCreated.Add(1,
    new KeyValuePair<string, object?>("order.type", "standard"),
    new KeyValuePair<string, object?>("payment.method", "credit_card"));

// 4个及以上标签 — 使用TagList避免内存分配
var tags = new TagList
{
    { "order.type", "standard" },
    { "payment.method", "credit_card" },
    { "region", "us-east" },
    { "priority", "high" }
};
_ordersCreated.Add(1, tags);

Custom ActivitySource for Distributed Tracing

用于分布式追踪的自定义ActivitySource

csharp
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;
        }
    }
}
Register the source:
.AddSource("MyApp.Orders")
in the tracing builder.
csharp
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 for Local Development

用于本地开发的Aspire Dashboard

Run the standalone Aspire Dashboard without Aspire orchestration:
bash
docker run --rm -it -p 18888:18888 -p 4317:18889 \
    mcr.microsoft.com/dotnet/aspire-dashboard:latest
Then point your app at it:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
Dashboard UI is at
http://localhost:18888
.
无需Aspire编排即可运行独立的Aspire Dashboard:
bash
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

Source-Generated Logging with OTel

结合OTel的源代码生成式日志

For maximum performance, use
[LoggerMessage]
— eliminates boxing and allocations.
csharp
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);
}
OpenTelemetry logging automatically includes
TraceId
and
SpanId
when an
Activity
is current.
为追求极致性能,使用
[LoggerMessage]
— 消除装箱操作与内存分配。
csharp
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

Anti-patterns

反模式

Don't Create Meters Per Request

切勿为每个请求创建Meter

csharp
// 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);
}
csharp
// 错误 — 每个请求创建新Meter会导致内存泄漏
public void HandleRequest()
{
    var meter = new Meter("MyApp");
    meter.CreateCounter<int>("requests").Add(1);
}

// 正确 — 通过IMeterFactory实现单例
public class MyMetrics(IMeterFactory meterFactory)
{
    private readonly Counter<int> _requests =
        meterFactory.Create("MyApp").CreateCounter<int>("myapp.requests");
    public void RequestHandled() => _requests.Add(1);
}

Don't Skip Null Checks on Activity

切勿跳过Activity的空值检查

csharp
// BAD — NullReferenceException when no listener is attached
using var activity = source.StartActivity("Work");
activity.SetTag("key", "value");

// GOOD — null-safe
activity?.SetTag("key", "value");
csharp
// 错误 — 无监听器附着时会抛出NullReferenceException
using var activity = source.StartActivity("Work");
activity.SetTag("key", "value");

// 正确 — 空安全写法
activity?.SetTag("key", "value");

Don't Use High-Cardinality Metric Tags

切勿使用高基数指标标签

csharp
// 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));
csharp
// 错误 — 无限基数会导致收集器内存激增
_counter.Add(1, new("request.id", Guid.NewGuid().ToString()));
_counter.Add(1, new("user.id", userId));

// 正确 — 仅使用低基数维度
_counter.Add(1, new("http.method", "GET"), new("http.status_code", 200));

Don't Mix UseOtlpExporter with AddOtlpExporter

切勿混用UseOtlpExporter与AddOtlpExporter

csharp
// BAD — throws NotSupportedException at runtime
builder.Services.AddOpenTelemetry()
    .UseOtlpExporter()
    .WithTracing(t => t.AddOtlpExporter());

// GOOD — use one approach
builder.Services.AddOpenTelemetry().UseOtlpExporter();
csharp
// 错误 — 运行时会抛出NotSupportedException
builder.Services.AddOpenTelemetry()
    .UseOtlpExporter()
    .WithTracing(t => t.AddOtlpExporter());

// 正确 — 使用其中一种方式
builder.Services.AddOpenTelemetry().UseOtlpExporter();

Don't Forget to Register Custom Sources

切勿忘记注册自定义源

csharp
// 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"));
csharp
// 错误 — Activity会被静默丢弃(未注册监听器)
var source = new ActivitySource("MyApp.Custom");
using var activity = source.StartActivity("Work"); // 返回null!

// 正确 — 在追踪构建器中注册
otel.WithTracing(t => t.AddSource("MyApp.Custom"));
otel.WithMetrics(m => m.AddMeter("MyApp.Custom"));

Decision Guide

决策指南

ScenarioRecommendation
Full observability setup
AddOpenTelemetry()
with all three signals +
UseOtlpExporter()
Custom business metrics
IMeterFactory
+ singleton metrics class
Custom trace spans
ActivitySource
+
StartActivity()
Local development backendAspire Dashboard standalone container
Production backendOTel Collector as intermediary to Grafana/Datadog/etc.
Sampling in production
OTEL_TRACES_SAMPLER=parentbased_traceidratio
with 10% ratio
High-performance logging
[LoggerMessage]
source generator
Metric tag cardinalityMax ~1000 combinations per instrument
Environment configuration
OTEL_*
env vars (also work via
appsettings.json
)
场景推荐方案
完整可观测性配置
AddOpenTelemetry()
+ 三类信号 +
UseOtlpExporter()
自定义业务指标
IMeterFactory
+ 单例指标类
自定义追踪Span
ActivitySource
+
StartActivity()
本地开发后端独立容器运行Aspire Dashboard
生产环境后端使用OTel Collector作为中间件对接Grafana/Datadog等平台
生产环境采样设置
OTEL_TRACES_SAMPLER=parentbased_traceidratio
,采样比例设为10%
高性能日志使用
[LoggerMessage]
源代码生成器
指标标签基数每个指标工具最多保留约1000种标签组合
环境配置使用
OTEL_*
环境变量(也可通过
appsettings.json
配置)