mcpField
O
opentelemetry

opentelemetry

github.com/codewithmukesh云与 DevOps7913.0k 次安装12 周前更新
代码仓库

关于 opentelemetry

opentelemetry 是面向 .NET 开发者的 OpenTelemetry 实践指南,涵盖追踪、指标和日志的配置与使用。它还说明指标生命周期、活动空值处理和低基数标签等实现注意事项。

主要功能

  1. 01一次配置追踪、指标和日志
  2. 02通过 OTLP 导出遥测数据
  3. 03使用 IMeterFactory 创建指标
  4. 04创建自定义 ActivitySource 追踪
  5. 05提供 Aspire Dashboard 本地配置

适用场景

  1. 01为 .NET 应用配置三类遥测
  2. 02为业务流程添加自定义指标
  3. 03为分布式操作创建追踪跨度
  4. 04在本地开发中查看遥测数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill opentelemetry

安装后在 Claude Code 中输入/opentelemetry确认已加载。

详细介绍

OpenTelemetry

核心原则

  1. 三大支柱,一次设置 — 通过一次 AddOpenTelemetry() 调用配置跟踪、指标和日志。使用 UseOtlpExporter() 将数据统一导出到任何兼容 OTLP 的后端。
  2. 使用 IMeterFactory 处理指标 — 切勿使用 new 创建 Meter 实例。工厂通过 DI 管理生命周期,并防止资源泄漏。
  3. Activity 可为 null — 没有附加监听器时,StartActivity() 会返回 null。设置标签或事件时始终使用 ?.。
  4. 优先使用环境变量,而不是代码 — 使用 OTEL_EXPORTER_OTLP_ENDPOINT 和 OTEL_SERVICE_NAME,让部署环境无需修改代码即可控制遥测数据的路由。
  5. 使用低基数指标标签 — 每个指标的标签组合应保持在约 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 + 单例指标类
自定义跟踪 spanActivitySource + StartActivity()
本地开发后端Aspire Dashboard 独立容器
生产后端使用 OTel Collector 作为中间层,连接到 Grafana/Datadog 等
生产环境中的采样OTEL_TRACES_SAMPLER=parentbased_traceidratio,采样比例为 10%
高性能日志记录[LoggerMessage] 源生成器
指标标签基数每个指标最多约 1000 种组合
环境配置OTEL_* 环境变量(也可通过 appsettings.json 使用)

评论

支持 Markdown