Как перестать гадать и начать видеть свой код насквозь?
Вы когда-нибудь сидели ночью перед монитором, смотря в логи, которые кричат об ошибке, но молчат о причине? Или пытались понять, почему ваш микросервис на C# отвечает 10 секунд, хотя вчера всё летало? Знакомо? Добро пожаловать в мир, где без Observability (наблюдаемости) ваш код — это чёрный ящик.
В этой статье мы не будем читать скучные лекции. Мы разберём три кита наблюдаемости: трейсинг, метрики и логи — и покажем, как подружить их с C#. Вы узнаете, как перестать тушить пожары и начать их предотвращать. Поехали!
Что такое Observability и почему это не просто "логирование"?
Многие путают Observability с обычным сбором логов. Но разница колоссальна. Observability — это свойство системы, которое позволяет вам задавать любые вопросы о её состоянии, не добавляя нового кода. Три столпа, на которых всё держится:
- Логи (Logs) — дискретные события. "Пользователь нажал кнопку", "Ошибка соединения". Это "что произошло?".
- Метрики (Metrics) — агрегированные данные. "Запросов в секунду", "Потребление памяти". Это "сколько и как часто?".
- Трейсинг (Traces) — путь запроса через систему. "Запрос пришёл, пошёл в БД, потом в кэш, вернулся". Это "почему так долго?".
В C# экосистеме .NET эти три кита объединяются с помощью библиотек вроде OpenTelemetry. И сейчас мы разберём, как это выглядит в коде.
Ставим эксперимент: пишем приложение с Observability
Представьте, что у нас есть простой сервис на ASP.NET Core, который обрабатывает заказы. Мы хотим видеть:
- Каждый шаг обработки заказа (трейсинг).
- Сколько заказов обработано и с какой ошибкой (метрики).
- Подробности исключений (логи).
Шаг 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(