OpenTelemetry в .NET: трейсы, метрики и логи с минимумом шаблонного кода

Published: 2026-03-13

OpenTelemetry — стандарт наблюдаемости, заменяющий SDK отдельных вендоров. Инструментирование настраивается один раз, экспортёры меняются в конфигурации. Для .NET-приложений в Kubernetes это трейсы в Jaeger, метрики в Prometheus и структурированные логи в любой нужный приёмник — всё из одной инициализации SDK.


NuGet-пакеты

xml<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.*" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.*" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.*-beta*" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.*" />

Пакеты разделены: инструментирование (что собирать) и экспортёры (куда отправлять). Для публикации метрик через /metrics используйте Prometheus-экспортёр AspNetCore.


Настройка SDK в Program.cs

csharpvar builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService(
                serviceName: "my-service",
                serviceVersion: Environment.GetEnvironmentVariable("APP_VERSION") ?? "dev"))
        .AddAspNetCoreInstrumentation(opts =>
        {
            opts.RecordException = true;
            opts.Filter = ctx => ctx.Request.Path != "/healthz";
        })
        .AddHttpClientInstrumentation()
        .AddOtlpExporter(opts =>
        {
            opts.Endpoint = new Uri(
                builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]
                ?? "http://otel-collector.observability.svc.cluster.local:4317");
        }))
    .WithMetrics(metrics => metrics
        .SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService("my-service"))
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddRuntimeInstrumentation()
        .AddPrometheusExporter());

var app = builder.Build();
app.MapPrometheusScrapingEndpoint();

Переменные окружения для конфигурации

yamlenv:
  - name: OTEL_EXPORTER_OTLP_ENDPOINT
    value: "http://otel-collector.observability.svc.cluster.local:4317"
  - name: OTEL_SERVICE_NAME
    value: "my-service"
  - name: OTEL_RESOURCE_ATTRIBUTES
    value: "deployment.environment=prod,k8s.namespace=app"
  - name: APP_VERSION
    value: "1.2.3"

SDK читает OTEL_SERVICE_NAME автоматически. Никаких изменений кода между окружениями.


Кастомные трейсы: спаны для бизнес-операций

csharpprivate static readonly ActivitySource _tracer =
    new ActivitySource("my-service.business");

public async Task<Order> ProcessOrderAsync(Guid orderId)
{
    using var span = _tracer.StartActivity("ProcessOrder");
    span?.SetTag("order.id", orderId.ToString());

    try
    {
        var order = await _repo.GetAsync(orderId);
        span?.SetTag("order.status", order.Status.ToString());
        span?.SetTag("order.amount", order.Total);

        await _paymentService.ChargeAsync(order);

        span?.SetStatus(ActivityStatusCode.Ok);
        return order;
    }
    catch (Exception ex)
    {
        span?.SetStatus(ActivityStatusCode.Error, ex.Message);
        span?.RecordException(ex);
        throw;
    }
}

Регистрация источника:

csharp.WithTracing(tracing => tracing
    .AddSource("my-service.business")
    ...

Кастомные метрики

csharpprivate static readonly Meter _meter = new Meter("my-service.business");
private static readonly Counter<long> _ordersProcessed =
    _meter.CreateCounter<long>("orders_processed_total");
private static readonly Histogram<double> _orderAmount =
    _meter.CreateHistogram<double>("order_amount_eur");

_ordersProcessed.Add(1, new TagList { { "status", "success" } });
_orderAmount.Record(order.Total, new TagList { { "currency", "EUR" } });

Регистрация:

csharp.WithMetrics(metrics => metrics
    .AddMeter("my-service.business")
    ...

Связь логов с трейсами

csharpbuilder.Logging.AddOpenTelemetry(logging =>
{
    logging.IncludeFormattedMessage = true;
    logging.IncludeScopes = true;
    logging.AddOtlpExporter(opts =>
    {
        opts.Endpoint = new Uri(
            builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]
            ?? "http://otel-collector.observability.svc.cluster.local:4317");
    });
});

Каждая запись лога получает атрибуты trace_id и span_id. В Grafana можно переходить от спана трейса к связанным логам.


Развёртывание OTel Collector

yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: otel-collector
  namespace: observability
spec:
  chart:
    spec:
      chart: opentelemetry-collector
      version: "0.x"
      sourceRef:
        kind: HelmRepository
        name: open-telemetry
  values:
    mode: deployment
    config:
      receivers:
        otlp:
          protocols:
            grpc:
              endpoint: 0.0.0.0:4317
            http:
              endpoint: 0.0.0.0:4318
      exporters:
        otlp/jaeger:
          endpoint: jaeger-collector.observability.svc.cluster.local:4317
          tls:
            insecure: true
      service:
        pipelines:
          traces:
            receivers: [otlp]
            exporters: [otlp/jaeger]

Приложения отправляют в Collector; Collector распределяет на несколько бэкендов. Смена бэкенда не требует изменений в приложении.


ServiceMonitor для скрейпинга Prometheus

yamlapiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-service
  namespace: observability
  labels:
    release: kube-prom-stack
spec:
  selector:
    matchLabels:
      app: my-service
  endpoints:
    - port: http
      path: /metrics
      interval: 30s
  namespaceSelector:
    matchNames:
      - app

Диагностика: трейсы не появляются

bash# Проверить доступность Collector из пода
kubectl exec -n app deploy/my-service -- \
  wget -qO- http://otel-collector.observability.svc.cluster.local:4317

# Включить debug-логирование OTel
- name: OTEL_LOG_LEVEL
  value: "debug"

# Логи коллектора
kubectl logs -n observability deploy/otel-collector --tail=50

Частые причины: неправильный namespace в URL OTLP-эндпоинта, отсутствующий insecure: true в экспортере коллектора, AddSource не вызван для кастомных ActivitySource.