Мониторинг C# приложений: метрики, алерты и продвинутые практики

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

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

Начать курс

Введение: Почему мониторинг C# приложений — это искусство

В современной разработке C# ( .NET) мониторинг перестал быть просто опцией. Это обязательный элемент production-среды, который позволяет не только фиксировать сбои, но и предсказывать их. На продвинутом уровне мониторинг превращается в систему раннего предупреждения: вы не просто смотрите на дашборды, а настраиваете алерты на основе сложных метрик.

В этой статье мы разберем, как выйти за рамки стандартных счетчиков CPU и памяти. Вы узнаете, как собирать кастомные бизнес-метрики, настраивать алерты с помощью Prometheus и Alertmanager, а также использовать OpenTelemetry для трассировки. Все примеры будут на C#.

1. Продвинутая инструментация: App Metrics и Prometheus

1.1. Подключение Prometheus к ASP.NET Core

Базовый мониторинг в .NET часто ограничивается встроенным PerformanceCounter. Однако для микросервисной архитектуры стандартом стал Prometheus. Установите пакет:

dotnet add package prometheus-net.AspNetCore

Затем добавьте middleware в Program.cs:

using Prometheus;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Включаем эндпоинт метрик
app.UseHttpMetrics();
app.UseRouting();
app.MapMetrics(); // /metrics

app.Run();

Это даст вам базовые метрики: количество запросов, длительность, ошибки. Но для продвинутого уровня этого мало.

1.2. Кастомные бизнес-метрики

Допустим, вы хотите отслеживать количество созданных заказов или время выполнения критической бизнес-операции. Используйте Counter и Histogram:

using Prometheus;

public class OrderMetrics
{
    private static readonly Counter OrdersCreated = Metrics
        .CreateCounter("orders_created_total", "Total number of created orders");

    private static readonly Histogram OrderProcessingTime = Metrics
        .CreateHistogram("order_processing_seconds", "Histogram of order processing time");

    public void RecordOrderCreation(double elapsedSeconds)
    {
        OrdersCreated.Inc();
        OrderProcessingTime.Observe(elapsedSeconds);
    }
}

Теперь в любом сервисе вы можете инжектировать OrderMetrics и записывать данные. Это позволит вам строить дашборды в Grafana с точностью до секунды.

2. Алерты: от простых триггеров к SLO-ориентированным правилам

2.1. Настройка Alertmanager для C# метрик

Собирать метрики — половина дела. Вторая половина — алерты. Создайте правило для Prometheus (alerts.yml):

groups:
  - name: csharp_app_alerts
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "High error rate in C# app"
          description: "Error rate is {{ $value }} for the last 5 minutes"

Это базовый пример. На продвинутом уровне стоит добавлять SLO-алерты. Например, если 99% запросов не укладываются в 200 мс — это повод для паники.

2.2. Алерты на основе бизнес-метрик

Используйте кастомные счетчики из предыдущего раздела. Если количество заказов резко упало — значит, что-то сломалось:

      - alert: OrdersStopped
        expr: rate(orders_created_total[10m]) == 0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "No orders created in last 10 minutes"

Такой алерт невозможно получить из стандартных метрик ОС — он требует глубокого понимания бизнес-логики.

3. OpenTelemetry: распределенная трассировка в C#

3.1. Интеграция OpenTelemetry SDK

Когда у вас 10 микросервисов на C#, метрики по отдельности не помогут. Нужна трассировка. Установите пакеты:

dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.Console
dotnet add package OpenTelemetry.Exporter.Jaeger

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

using OpenTelemetry.Trace;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracerProviderBuilder =>
        tracerProviderBuilder
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddConsoleExporter()
            .AddJaegerExporter(options =>
            {
                options.AgentHost = "localhost";
                options.AgentPort = 6831;
            }));

Теперь каждый HTTP-запрос будет иметь уникальный TraceId. Вы сможете увидеть, какой сервис тормозит.

3.2. Кастомные спаны и атрибуты

Для критических участков кода добавьте свои спаны. Например, при обработке заказа:

using System.Diagnostics;

public async Task ProcessOrder(Order order)
{
    using var activity = Activity.Current?.Source
        .StartActivity("ProcessOrder");
    
    activity?.SetTag("order.id", order.Id);
    activity?.SetTag("order.amount", order.Amount);
    
    // Ваша бизнес-логика
    await _paymentService.Charge(order);
    
    activity?.SetStatus(ActivityStatusCode.Ok);
}

В Jaeger или Zipkin вы увидите не просто "запрос обработан", а полную картину: сколько времени заняла оплата, проверка склада и т.д.

4. Health Checks и продвинутая диагностика

4.1. Кастомные Health Checks

Встроенные health checks в ASP.NET Core — это хорошо. Но на продвинутом уровне вы должны проверять не только "жив ли сервис", но и его зависимости:

public class DatabaseHealthCheck : IHealthCheck
{
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            using var connection = new SqlConnection(_connectionString);
            await connection.OpenAsync(cancellationToken);
            
            // Проверяем время ответа
            var stopwatch = Stopwatch.StartNew();
            // ... запрос
            stopwatch.Stop();
            
            if (stopwatch.ElapsedMilliseconds > 100)
                return HealthCheckResult.Degraded("DB slow response");
                
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("DB unreachable", ex);
        }                

Блоги

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