Observability в C#: Трейсинг, Метрики и Логи на Практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как перестать гадать и начать видеть свой код насквозь?

Вы когда-нибудь сидели ночью перед монитором, смотря в логи, которые кричат об ошибке, но молчат о причине? Или пытались понять, почему ваш микросервис на C# отвечает 10 секунд, хотя вчера всё летало? Знакомо? Добро пожаловать в мир, где без Observability (наблюдаемости) ваш код — это чёрный ящик.

В этой статье мы не будем читать скучные лекции. Мы разберём три кита наблюдаемости: трейсинг, метрики и логи — и покажем, как подружить их с C#. Вы узнаете, как перестать тушить пожары и начать их предотвращать. Поехали!

Что такое Observability и почему это не просто "логирование"?

Многие путают Observability с обычным сбором логов. Но разница колоссальна. Observability — это свойство системы, которое позволяет вам задавать любые вопросы о её состоянии, не добавляя нового кода. Три столпа, на которых всё держится:

  • Логи (Logs) — дискретные события. "Пользователь нажал кнопку", "Ошибка соединения". Это "что произошло?".
  • Метрики (Metrics) — агрегированные данные. "Запросов в секунду", "Потребление памяти". Это "сколько и как часто?".
  • Трейсинг (Traces) — путь запроса через систему. "Запрос пришёл, пошёл в БД, потом в кэш, вернулся". Это "почему так долго?".

В C# экосистеме .NET эти три кита объединяются с помощью библиотек вроде OpenTelemetry. И сейчас мы разберём, как это выглядит в коде.

Ставим эксперимент: пишем приложение с Observability

Представьте, что у нас есть простой сервис на ASP.NET Core, который обрабатывает заказы. Мы хотим видеть:

  1. Каждый шаг обработки заказа (трейсинг).
  2. Сколько заказов обработано и с какой ошибкой (метрики).
  3. Подробности исключений (логи).

Шаг 1: Настройка логирования с Serilog

Начнём с самого понятного — логов. Используем Serilog (но можно и NLog). Он умеет писать структурированные логи, которые потом удобно парсить.

// NuGet: Serilog, Serilog.AspNetCore, Serilog.Sinks.Console
using Serilog;

// Настройка в Program.cs
Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .WriteTo.File("logs/app.log", rollingInterval: RollingInterval.Day)
    .Enrich.WithProperty("Application", "OrderService")
    .CreateLogger();

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog(); // Заменяем стандартный логгер

// Пример использования в контроллере
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
    private readonly ILogger<OrderController> _logger;

    public OrderController(ILogger<OrderController> logger) => _logger = logger;

    [HttpPost]
    public IActionResult CreateOrder(Order order)
    {
        _logger.LogInformation("Creating order {OrderId} for user {User}", order.Id, order.UserId);
        // ... логика ...
        _logger.LogWarning("Order {OrderId} is delayed due to stock issues", order.Id);
        return Ok();
    }
}

Обратите внимание: мы используем именованные плейсхолдеры ({OrderId}). Это не просто текст — это структурированные данные. В Elasticsearch или Grafana Loki вы сможете искать все логи по конкретному OrderId.

Шаг 2: Метрики с Prometheus и .NET

Метрики отвечают за "циферки". Сколько запросов в секунду? Сколько ошибок 500? В C# мы используем Meters из System.Diagnostics.Metrics или библиотеку prometheus-net.

Давайте создадим простой счётчик обработанных заказов:

// NuGet: prometheus-net.AspNetCore
using Prometheus;

// В Program.cs добавляем метрики
var builder = WebApplication.CreateBuilder(args);
// ... остальная настройка ...
var app = builder.Build();

// Глобальный счётчик
var ordersProcessed = Metrics.CreateCounter("orders_processed_total", "Total number of processed orders");
var ordersFailed = Metrics.CreateCounter("orders_failed_total", "Total number of failed orders");

// Middleware для сбора HTTP метрик (встроенный)
app.UseHttpMetrics();

// В контроллере
[HttpPost]
public IActionResult CreateOrder(Order order)
{
    try
    {
        // ... логика ...
        ordersProcessed.Inc(); // Увеличиваем счётчик
        return Ok();
    }
    catch (Exception ex)
    {
        ordersFailed.Inc();
        _logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
        return StatusCode(500);
    }
}

// В конце Program.cs обязательно добавляем эндпоинт для Prometheus
app.MapMetrics(); // /metrics
app.Run();

Теперь по адресу /metrics ваш сервис отдаёт данные в формате, который понимает Prometheus. Можно строить дашборды в Grafana и видеть, например, резкий рост orders_failed_total — и сразу бежать смотреть логи.

Шаг 3: Трейсинг с OpenTelemetry

Трейсинг — самая мощная, но и самая недооценённая часть. Он показывает, как запрос путешествует между сервисами и компонентами. В C# стандарт — OpenTelemetry.

Представим, что наш сервис ходит в базу данных и во внешний API оплаты. С трейсингом мы увидим, где теряются миллисекунды.

// NuGet: OpenTelemetry, OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, OpenTelemetry.Exporter.Console (для теста)

using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

// Настройка OpenTelemetry
builder.Services.AddOpenTelemetry()
    .WithTracing(tracerProvider => tracerProvider
        .AddSource("OrderService") // Имя источника
        .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("OrderService"))
        .AddAspNetCoreInstrumentation() // Автоматический трейсинг HTTP запросов
        .AddHttpClientInstrumentation() // Трейсинг исходящих HTTP вызовов
        .AddEntityFrameworkCoreInstrumentation() // Трейсинг запросов к БД (если есть EF Core)
        .AddConsoleExporter()); // Для отладки — выводит трейсы в консоль
// В реальности вы будете экспортировать в Jaeger, Zipkin или Grafana Tempo

// Создаём активность (span) вручную
public class PaymentService
{
    private static readonly ActivitySource MyActivitySource = new("OrderService");

    public async Task<bool> ProcessPayment(Order order)
    {
        // Создаём дочерний span внутри текущего трейса
        using var activity = MyActivitySource.StartActivity("ProcessPayment");
        activity?.SetTag("order.id", order.Id);
        activity?.SetTag("payment.amount", order.Total);

        try
        {
            // Симуляция вызова API
            await Task.Delay(100);
            activity?.SetStatus(ActivityStatusCode.Ok);
            return true;
        }
        catch (Exception ex)
        {
            activity?.SetStatus(                

Блоги

Рекомендаций по книгам