Моніторинг

Спостережуваність: від console.log до production-систем

Фундаментальний вступ до observability: що це таке, чим відрізняється від моніторингу, три стовпи (метрики, логи, трейси) та четвертий — профілювання. Огляд LGTM-стеку як інструментарію курсу.

Спостережуваність: від console.log до production-систем

Уявіть: вівторок, 3:17 ранку. Ваш телефон вибухає від сповіщень — продакшн-система недоступна. Черговий інженер заходить на сервер і бачить... що саме? Якщо відповідь «нічого, крім journalctl -f» — ви читаєте правильний матеріал.

Сучасні production-системи живуть і вмирають не через якість коду, а через якість спостереження за кодом. Розробник, який вміє написати ефективний алгоритм, але не може відповісти на питання «чому мій сервіс уповільнився о 14:32 у п'ятницю?» — неповний інженер для production-середовища.

Цей розділ курсу присвячений саме цьому: як зробити так, щоб ваш ASP.NET додаток «розповідав» про себе все, що потрібно для розуміння його стану, поведінки та причин збоїв.


Моніторинг: реактивний підхід

Традиційний моніторинг — це спостереження за заздалегідь відомими показниками. Ви знаєте, що сервер може перегрітись — тому стежите за температурою процесора. Знаєте, що диск може заповнитись — тому стежите за вільним місцем. Якщо показник перевищує поріг — надходить алерт.

Це реактивний підхід: система фіксує конкретні відомі симптоми. Поки щось не вийшло за межі налаштованого порогу — ніхто нічого не знає.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Інженер" as Eng

rectangle "Традиційний Моніторинг\n(Reactive)" as MON #fef3c7 {
    rectangle "Заздалегідь відомі\nзапитання" as KQ #fde68a {
        note as N1
          CPU > 90%?
          Disk > 80%?
          HTTP 500 > 10/min?
        end note
    }
}

rectangle "Система" as SYS #dbeafe {
    rectangle ".NET API\nprocess" as API #bfdbfe
    rectangle "Database\nconnections" as DB #bfdbfe
    rectangle "Memory\nheap" as MEM #bfdbfe
}

rectangle "Алерт" as ALERT #fecaca

SYS --> MON : метрики потрапляють
MON --> ALERT : якщо поріг перевищено
ALERT --> Eng : о 3:17 ночі

note bottom of MON
  Питання треба сформулювати
  **заздалегідь**, до розгортання.
  Нові питання = нові правила.
end note

@enduml

Проблема стає очевидною, коли відбувається щось непередбачуване. Наприклад: деградація продуктивності конкретного ендпоінту тільки для запитів з певним набором параметрів, тільки у вівторок між 14:00 і 15:00, тільки якщо одночасно активних сесій більше 300. Жодного з цих порогів у вас не налаштовано — адже ви не могли передбачити саме цю комбінацію.

Традиційний моніторинг добре відповідає на питання «чи все ще працює?» — але погано відповідає на питання «чому воно так поводиться?».

Спостережуваність: проактивний підхід

Observability (спостережуваність) — це властивість системи, яка дозволяє зрозуміти її внутрішній стан на основі зовнішніх сигналів, що вона генерує. Термін прийшов з теорії управління (control theory) і означає: система «спостережувана», якщо будь-який її внутрішній стан можна визначити, вивчивши її зовнішні виходи.

Для програмних систем це звучить так: ви повинні мати можливість поставити будь-яке нове питання про поведінку системи і знайти відповідь — навіть якщо це питання ніколи не виникало раніше і ви не готувались до нього заздалегідь.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Інженер" as Eng

rectangle "Observability\n(Proactive)" as OBS #f0fdf4 {
    rectangle "Будь-яке нове\nзапитання" as AQ #bbf7d0 {
        note as N2
          Чому /checkout повільний
          лише для user_id > 100000?
          Чому GC паузи збільшились
          після деплою о 14:32?
          Звідки memory leak
          у конкретному middleware?
        end note
    }
}

rectangle "Система" as SYS #dbeafe {
    rectangle ".NET API" as API #bfdbfe
    rectangle "Metrics\n(числа у часі)" as M #d1fae5
    rectangle "Logs\n(події з контекстом)" as L #fce7f3
    rectangle "Traces\n(шлях запиту)" as T #fef3c7
}

Eng --> OBS : задає нове питання
SYS --> M : генерує
SYS --> L : генерує
SYS --> T : генерує
M --> OBS : відповідь
L --> OBS : відповідь
T --> OBS : відповідь

note bottom of SYS
  Система **постійно** генерує
  детальні дані про себе.
  Питання можна ставити пізніше.
end note

@enduml

Різниця фундаментальна: моніторинг каже «збери ці конкретні числа», observability каже «зроби систему настільки прозорою, щоб можна було відповісти на будь-яке питання».

Хороша метафора: моніторинг — це термометр у кімнаті. Observability — це повна система датчиків, камер та журналів подій, яка дозволить зрозуміти, чому температура змінилась саме так саме тоді.

Три стовпи спостережуваності

Галузь виробила три категорії телеметричних даних, які разом дають повну картину поведінки системи. Їх часто називають «три стовпи observability».

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Три стовпи Observability" as PILLARS #f8fafc {

    rectangle "📊 МЕТРИКИ\n(Metrics)" as M #dbeafe {
        note as NM
          Числові виміри у часі.
          Що відбувається зараз
          і як це змінилось?
          ——————————————
          HTTP requests/sec: 1240
          P95 latency: 87ms
          Error rate: 0.3%
          Memory heap: 412 MB
        end note
    }

    rectangle "📋 ЛОГИ\n(Logs)" as L #d1fae5 {
        note as NL
          Дискретні події з контекстом.
          Що конкретно сталося і коли?
          ——————————————
          [14:32:01] ERROR OrderService
          UserId=4521 OrderId=null
          SqlException: timeout after
          30s on query #47
        end note
    }

    rectangle "🔗 ТРЕЙСИ\n(Traces)" as T #fce7f3 {
        note as NT
          Шлях одного запиту
          через всі сервіси.
          Де саме втрачається час?
          ——————————————
          GET /checkout — 2340ms
           ├ Auth: 12ms
           ├ OrderService: 2280ms
           │  └ SQL query: 2251ms ⚠
           └ Response: 48ms
        end note
    }
}

@enduml

Кожен стовп відповідає на своє питання — і жоден не може повноцінно замінити інші.

Стовп перший: Метрики

Метрики — це числові значення, що вимірюються у часі. Ключова їхня властивість: вони агреговані. Ви не зберігаєте інформацію про кожен окремий HTTP-запит, натомість зберігаєте лічильник «кількість запитів за останню хвилину» або «розподіл часу відповіді за останні 5 хвилин».

Метрики надзвичайно ефективні з точки зору сховища і швидкості запитів, але ціна цієї ефективності — втрата деталей. Метрика «P95 latency = 350ms» повідомляє, що щось повільно, але не каже що саме, для якого користувача і чому.

Типові питання, на які відповідають метрики:

  • Скільки? — кількість запитів, помилок, активних з'єднань
  • Як швидко? — latency percentiles, throughput
  • Скільки ресурсів? — CPU, RAM, disk I/O, GC pauses
  • Чи зросло/впало? — порівняння з минулим

Стовп другий: Логи

Логи — це дискретні записи про події, що відбулись у системі. На відміну від метрик, лог — це завжди конкретна подія з повним контекстом: час, рівень важливості, повідомлення, структуровані поля.

Структуровані логи (на відміну від Console.WriteLine("Error!")) зберігають дані як об'єкти, що дозволяє ефективно фільтрувати та агрегувати: «покажи всі помилки для userId=4521 за останню годину».

Типові питання, на які відповідають логи:

  • Що конкретно сталося? — повне повідомлення про подію
  • Для кого? — userId, requestId, tenantId
  • З якою помилкою? — stack trace, exception details
  • У якому контексті? — які параметри запиту, стан системи

Стовп третій: Трейси

Трейси (distributed traces) — це записи про шлях одного конкретного запиту через систему. Один трейс складається з spans — відрізків часу, кожен з яких відповідає одній операції: виклик HTTP ендпоінту, SQL-запит, зверненню до Redis, виклику зовнішнього API.

Трейсинг — це інструмент для розуміння причинно-наслідкових зв'язків і розподілу часу. Коли запит займає 3 секунди, трейс точно показує: 2.8 секунди з них — це очікування відповіді від бази даних на запит у методі OrderRepository.GetByUserId().

Типові питання, на які відповідають трейси:

  • Де втрачається час? — waterfall-діаграма spans
  • Який шлях пройшов запит? — через які сервіси, в якому порядку
  • Де виникла помилка? — конкретний span із помилкою в ланцюжку
  • Як мікросервіси взаємодіють? — service dependency map

Коли що використовувати: навігаційна таблиця

У реальній роботі observability — це не вибір між трьома стовпами, а навігація між ними залежно від питання, яке потрібно вирішити.

| Питання | Стовп | Інструмент | | ------------------------------------- | ------------ | ------------------------------ | ---------- | | Чи є зараз деградація продуктивності? | Метрики | Prometheus + Grafana dashboard | | О котрій годині почалась проблема? | Метрики | Grafana time series | | Які ендпоінти постраждали? | Метрики | PromQL by (route) | | Що конкретно відбувається в коді? | Логи | Loki + LogQL | | Для яких користувачів проблема? | Логи | {userId="..."} | = "ERROR" | | Де в ланцюжку викликів затримка? | Трейси | Grafana Tempo | | Який конкретний SQL запит повільний? | Трейси | Span attributes | | Чи зафіксована проблема в базі? | Логи | Full exception + stack trace | | Який метод витрачає найбільше CPU? | Профілювання | Pyroscope flame graph |

Золоте правило розслідування інциденту: починайте з метрик (зрозуміти масштаб і час), переходьте до логів (знайти конкретні помилки), завершуйте трейсами (зрозуміти причину).

Четвертий стовп: Профілювання

Останніми роками спільнота дійшла консенсусу щодо четвертого стовпа — continuous profiling. Якщо три класичних стовпи відповідають на питання «що відбувається», профілювання відповідає на питання «чому код поводиться саме так на рівні виконання».

Профілювання — це вимірювання того, як саме витрачається час і пам'ять всередині процесу: які функції виконуються найдовше, де відбуваються алокації об'єктів, які lock-и блокують потоки.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Метрики" as M #dbeafe {
    note as NM
      P95 latency = 850ms
      (підвищилось)
    end note
}

rectangle "Трейси" as T #fce7f3 {
    note as NT
      Span: ProcessOrders()
      duration = 820ms
    end note
}

rectangle "Профілювання" as P #fef3c7 {
    note as NP
      ProcessOrders() → 820ms
       ├ JsonSerializer.Serialize → 640ms (78%!) ⚠
       ├ DbContext.SaveChanges → 120ms
       └ Validation.Run → 60ms
    end note
}

M -right-> T : "де саме?"
T -right-> P : "чому саме цей метод?"

note bottom of P
  Без профілювання ви б ніколи
  не дізнались, що 78% часу
  витрачається на серіалізацію —
  і шукали б проблему в базі даних.
end note

@enduml

Continuous profiling — це профілювання, що працює постійно у production з мінімальним overhead (< 2% CPU). На відміну від класичного профілювання (вмикаємо на хвилину при підозрі), continuous profiling дає змогу порівнювати профілі до і після деплою, бачити деградації у часі.

У цьому курсі continuous profiling покривається у файлі 17 (17.pyroscope-profiling.md) через Grafana Pyroscope.

📊 Метрики

Питання: що і скільки?

Агреговані числові виміри у часі. Ефективні, дешеві у зберіганні. Ідеальні для дашбордів та алертів.

Інструменти: Prometheus, Grafana

📋 Логи

Питання: що конкретно сталось?

Дискретні події з повним контекстом. Дозволяють відтворити точну послідовність подій.

Інструменти: Serilog, Loki

🔗 Трейси

Питання: де втрачається час?

Шлях одного запиту крізь систему. Незамінні для мікросервісів та дебагу latency.

Інструменти: OpenTelemetry, Tempo

🔥 Профілювання

Питання: чому код так поводиться?

Розподіл CPU і пам'яті по функціях. Знаходить неочевидні bottlenecks у коді.

Інструменти: Pyroscope


Еволюція: від Console.WriteLine до observability

Щоб зрозуміти, чому observability — це не «over-engineering», а необхідність, простежимо еволюцію підходів до розуміння поведінки програм.

Етап 1: Console.WriteLine та Debug.Print

Program.cs
app.MapPost("/orders", async (Order order) =>
{
    Console.WriteLine("Отримали замовлення");  // ← так робити не треба
    var result = await orderService.Create(order);
    Console.WriteLine("Замовлення створено: " + result.Id);
    return result;
});

Працює у розробці. Повністю марне у production: немає рівнів важливості, немає структури, немає можливості фільтрувати, у Docker-контейнері може бути буферизований і взагалі не з'явитись при краші.

Етап 2: Вбудований ILogger

Program.cs
app.MapPost("/orders", async (Order order, ILogger<Program> logger) =>
{
    logger.LogInformation("Отримали замовлення від {UserId}", order.UserId);
    var result = await orderService.Create(order);
    logger.LogInformation("Замовлення {OrderId} створено", result.Id);
    return result;
});

Значно краще: є рівні (Information, Warning, Error), є категорії, є конфігурація. Але стандартний вивід у текстовому форматі ускладнює машинний аналіз. У production при 1000 req/s логи перетворюються на нечитабельний потік рядків.

Етап 3: Structured Logging (Serilog)

Program.cs
// Лог зберігається як об'єкт, а не рядок:
// { "timestamp": "...", "level": "Information",
//   "message": "Order created",
//   "userId": 4521, "orderId": "ord-991",
//   "duration_ms": 47, "traceId": "abc123..." }
logger.LogInformation(
    "Order {OrderId} created for {UserId} in {Duration}ms",
    result.Id, order.UserId, stopwatch.ElapsedMilliseconds);

Тепер логи можна фільтрувати за userId, агрегувати за duration_ms, шукати всі події для конкретного traceId. Це вже справжня observability-практика.

Етап 4: Full Observability Stack

Structured logs — лише початок. Повноцінна observability додає:

  • Метрики — числові агрегати, що не залежать від обсягу логів (при 100k req/s зберігати лог кожного запиту — надто дорого; метрика «1000 req/s з P95=47ms» — копійки)
  • Трейси — автоматична кореляція між логами різних сервісів через traceId
  • Централізоване сховище — всі дані в одному місці, незалежно від кількості інстансів і сервісів
  • Алерти — автоматичне повідомлення, коли щось виходить за межі норми
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Еволюція observability у .NET проєкті"

rectangle "Рівень 1\nConsole.WriteLine" as L1 #fecaca {
    note as N1
      ❌ Немає структури
      ❌ Немає рівнів
      ❌ Марне у production
    end note
}

rectangle "Рівень 2\nILogger (вбудований)" as L2 #fde68a {
    note as N2
      ✅ Рівні логування
      ✅ Категорії
      ❌ Текстовий формат
      ❌ Немає метрик
    end note
}

rectangle "Рівень 3\nSerilog + structured logs" as L3 #bbf7d0 {
    note as N3
      ✅ JSON формат
      ✅ Фільтрація за полями
      ✅ Correlation ID
      ❌ Немає метрик, трейсів
    end note
}

rectangle "Рівень 4\nFull LGTM Stack" as L4 #bfdbfe {
    note as N4
      ✅ Метрики → Prometheus
      ✅ Логи → Loki
      ✅ Трейси → Tempo
      ✅ Профілі → Pyroscope
      ✅ Візуалізація → Grafana
      ✅ Алерти → OnCall
    end note
}

L1 -down-> L2 : додаємо DI та рівні
L2 -down-> L3 : переходимо на Serilog
L3 -down-> L4 : додаємо OTel + LGTM

@enduml

Чому одних логів недостатньо

Одна з найпоширеніших помилок — вважати, що якщо у додатку є логи, то «моніторинг налаштований». Розглянемо кілька конкретних сценаріїв, де логи не дають відповіді.

Сценарій 1: повільний деплой

Після деплою P95 latency зросла з 45ms до 280ms. Помилок немає, виключень немає — лише уповільнення. У логах: тисячі рядків Information: Request processed. Навіть якщо в кожному логу є duration_ms, щоб обрахувати P95 по 100 000 рядках за хвилину — потрібно виконати агрегацію. Метрики роблять це автоматично і миттєво.

Сценарій 2: memory leak

Пам'ять поступово зростає: 200MB → 400MB → 800MB → OOMKill через 6 годин. У логах немає жодної помилки — процес просто вичерпав пам'ять. Метрики б показали тренд зростання. Профілювання б показало, який саме об'єкт не звільняється.

Сценарій 3: мікросервісна проблема

Запит до OrderService займає 3 секунди. Але OrderService сам по собі швидкий — він чекає відповідь від InventoryService, який чекає ExternalShippingAPI. Без трейсів ви побачите тільки «замовлення повільне» в логах, але не зрозумієте де саме проблема.

Логи описують дискретні події. Вони не підходять для розуміння трендів у часі (метрики), розподілу часу між компонентами (трейси) або розподілу CPU/пам'яті по функціях (профілювання).

LGTM-стек: карта нашої подорожі

Цей курс будується навколо LGTM-стеку — набору open-source інструментів від компанії Grafana Labs, що реалізують усі чотири стовпи observability. Абревіатура LGTM розшифровується за назвами компонентів: Loki, Grafana, Tempo, Mimir (або Prometheus).

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Ваш .NET 10 додаток\n(ASP.NET Minimal API)" as APP #bfdbfe

rectangle "OpenTelemetry SDK" as OTEL #e0e7ff {
    rectangle "MeterProvider\n(метрики)" as MP #dbeafe
    rectangle "TracerProvider\n(трейси)" as TP #fce7f3
    rectangle "Serilog\n(логи)" as SER #d1fae5
}

rectangle "LGTM Storage" as LGTM #f8fafc {
    rectangle "Prometheus\n:9090" as PROM #fef3c7
    rectangle "Loki\n:3100" as LOKI #d1fae5
    rectangle "Tempo\n:3200" as TEMPO #fce7f3
    rectangle "Pyroscope\n:4040" as PYRO #fde68a
}

rectangle "Grafana Alloy\n(агент збору)" as ALLOY #e5e7eb

rectangle "Grafana\n:3000\n(єдиний UI)" as GRAFANA #bbf7d0

APP --> MP : System.Diagnostics.Metrics
APP --> TP : System.Diagnostics.Activity
APP --> SER : ILogger → Serilog

MP --> PROM : /metrics (scrape)
TP --> TEMPO : OTLP gRPC
SER --> ALLOY : logs (file або push)
APP --> PYRO : Pyroscope SDK

ALLOY --> LOKI : push logs

PROM --> GRAFANA : datasource
LOKI --> GRAFANA : datasource
TEMPO --> GRAFANA : datasource
PYRO --> GRAFANA : datasource

@enduml

Grafana виступає єдиним вікном для всіх чотирьох стовпів. Замість відкривати Prometheus UI для метрик, окремий інтерфейс для логів і ще один для трейсів — ви бачите все в одному місці, з кореляцією між ними.

Prometheus

Метрики. Time-series база даних з pull-моделлю збору. Scrape-ає /metrics ендпоінт вашого додатку кожні N секунд.

Покривається у файлах 04–06.

Grafana

Візуалізація. Єдиний UI для всіх datasource. Дашборди, alerts, Explore, provisioning.

Покривається у файлах 07–08, 14–15.

Loki

Логи. «Prometheus для логів» — індексує лише metadata, не текст. Cheapest спосіб централізувати логи.

Покривається у файлах 09–10.

Tempo

Трейси. Object storage-first tracing backend. Приймає трейси через OTLP, інтегрується з Grafana.

Покривається у файлах 11–12.

Додатково курс покриває Grafana Alloy (агент для збору логів, замінник Promtail), k6 (навантажувальне тестування з відображенням у Grafana) та Pyroscope (continuous profiling).


Сценарій розслідування: як три стовпи працюють разом

Щоб зрозуміти цінність повного стеку, розглянемо реальний сценарій інциденту від початку до кінця.

Ситуація: вівторок, 14:47. Надходить алерт: «Error rate перевищив 5%».

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Розслідування інциденту з LGTM стеком"

actor "Черговий інженер" as Eng
participant "Grafana\nDashboard" as GD #bbf7d0
participant "Prometheus\n(Метрики)" as PROM #fef3c7
participant "Loki\n(Логи)" as LOKI #d1fae5
participant "Tempo\n(Трейси)" as TEMPO #fce7f3

Eng -> GD : Відкриває dashboard після алерту

GD -> PROM : rate(http_server_request_errors_total[5m])
PROM --> GD : Spike о 14:32, ендпоінт POST /checkout

note over Eng, GD
  Крок 1 (Метрики):
  Визначили: яка саме операція,
  коли почалось, масштаб (~120 errors/min)
end note

Eng -> GD : Переходить до Explore → Loki

GD -> LOKI : {job="api"} |= "ERROR" | json | route="/checkout"
LOKI --> GD : SqlException: Connection timeout\nafter 30000ms at OrderRepository

note over Eng, GD
  Крок 2 (Логи):
  Визначили: причина — timeout до бази.
  UserId=4521..9832 (всі нові користувачі).
  TraceId=abc123def456...
end note

Eng -> GD : Клікає на TraceId → відкривається Tempo

GD -> TEMPO : { traceId = "abc123def456" }
TEMPO --> GD : Waterfall: \nOrderService: 30021ms\n└ SQL: INSERT users: 29998ms\n   └ Index rebuild on users table ⚠

note over Eng, GD
  Крок 3 (Трейси):
  Визначили: автоматичний rebuild index
  на таблиці users заблокував INSERT.
  Час розслідування: 8 хвилин.
end note

Eng -> Eng : Рішення: відкласти index rebuild на\nтихий час (нічне вікно обслуговування)

@enduml

Зверніть увагу: кожен крок звужує простір пошуку. Метрики дали напрямок (який ендпоінт, коли). Логи дали деталь (яка помилка, для кого). Трейси дали причину (де саме і чому). Без повного стеку кожен з цих кроків міг зайняти години замість хвилин.


Підсумок

Observability — це інвестиція, що окупається при першому ж серйозному інциденті. Вартість впровадження LGTM-стеку для одного ASP.NET додатку — кілька годин. Вартість розслідування одного непоміченого інциденту без observability — потенційно дні.

У цьому курсі ми побудуємо повноцінний LGTM-стек крок за кроком:

Метрики (файли 02–08)

Health Checks → вбудовані .NET 10 метрики → Prometheus → PromQL → Grafana дашборди.

Логи (файли 09–10)

Serilog structured logging → Grafana Alloy → Loki → LogQL.

Трейси (файли 11–12)

OpenTelemetry Activity API → Grafana Tempo → TraceQL → кореляції.

Повний стек (файли 13–17)

LGTM Docker Compose → Grafana Alerting → OnCall → k6 load testing → Pyroscope profiling.

Починаємо з першого практичного кроку: Health Checks — найпростіший і найважливіший механізм observability, що є у кожному ASP.NET Core додатку.

Copyright © 2026