Сервис упал в три часа ночи. Дежурный смотрит в дашборд - там красный график, но непонятно почему. Логи есть, но без контекста: куча строк, из которых непонятно, какой запрос привёл к сбою. Метрики показывают рост latency, но не объясняют, где именно в цепочке вызовов всё пошло не так. Через 40 минут выясняется, что виновата одна SQL-query в конкретном сценарии - и это можно было найти за две минуты, если бы данные были связаны между собой.
Именно здесь проявляется разница между «мы что-то мониторим» и «мы понимаем, что происходит внутри системы». Observability в backend - это не набор инструментов, а способность задать произвольный вопрос о состоянии системы и получить ответ по собранным данным, даже если такой вопрос не предусматривался заранее.
В этой статье разберём, как выстроить такую систему с нуля: что собирать, как структурировать, какие инструменты использовать и как связать метрики, логи и трейсы в единое целое.
Коротко:
- Три столпа observability - метрики, логи и трейсы. По отдельности каждый из них неполный, вместе они дают полную картину.
- Метрики показывают, что происходит; логи объясняют почему; трейсы показывают где именно в цепочке вызовов.
- Начинать лучше с RED-метрик (Rate, Errors, Duration) - они покрывают большинство инцидентов в сервисах.
- Логи должны быть структурированными с первого дня: произвольный текст нельзя надёжно агрегировать.
- OpenTelemetry - де-факто стандарт инструментирования: один SDK для метрик, логов и трейсов.
- Связь между тремя типами данных через trace_id позволяет переходить от «что сломалось» к «почему и где» за секунды, а не за час.
Чем observability отличается от обычного мониторинга
Классический мониторинг строится вокруг заранее известных вопросов: CPU выше 80%? Диск заполнен? Сервис не отвечает на health-check? Для этих сценариев достаточно пороговых алертов. Но когда система усложняется - несколько сервисов, асинхронные очереди, внешние зависимости - заранее предусмотреть все точки отказа невозможно.
Observability решает другую задачу: дать возможность исследовать состояние системы без предварительной подготовки. Вы не знаете заранее, что именно сломается, но если данные собраны правильно, сможете найти ответ постфактум. Это особенно критично при мониторинге микросервисов, где один пользовательский запрос может проходить через десятки сервисов и сотни функций.
Практически: мониторинг говорит «у нас проблема», observability помогает понять «где именно и почему».
Три столпа: что каждый из них делает
Метрики - это числовые значения во времени. Количество запросов в секунду, процент ошибок, медиана и 99-й перцентиль времени ответа, размер очереди. Метрики дёшевы в хранении, хорошо агрегируются и позволяют строить алерты. Но у них нет контекста: метрика «500 ошибок в минуту» не скажет, какие запросы падают и почему.
Логи - это события с контекстом. Каждая запись фиксирует, что произошло, с какими параметрами и в какой момент. Хорошо структурированные логи позволяют восстановить цепочку событий. Проблема: при высоком трафике объём логов растёт быстро, а поиск нужного события в неструктурированном тексте - медленный и ненадёжный.
Трейсы - это запись полного пути запроса через систему. Один трейс содержит спаны (spans): каждый спан описывает одну операцию (HTTP-вызов, SQL-запрос, обращение к кэшу) с временем начала, длительностью и атрибутами. Трейсы незаменимы, когда нужно понять, где именно в цепочке возникает задержка или ошибка. Детальнее о том, как читать трейсы в распределённых системах, - в отдельной статье про distributed tracing.
Вместе три типа данных закрывают разные углы одного вопроса. Метрика сигнализирует об аномалии, лог объясняет контекст конкретного события, трейс показывает полный маршрут запроса. Без одного из элементов картина остаётся неполной.
С чего начинать: RED и USE
Не нужно собирать всё подряд. Для большинства backend-сервисов достаточно начать с двух фреймворков метрик.
RED-метрики (предложены Томом Уилки из Grafana Labs) описывают поведение сервиса с точки зрения пользователя:
- Rate - количество запросов в секунду
- Errors - доля запросов, завершившихся ошибкой
- Duration - распределение времени ответа (медиана, p95, p99)
Если эти три показателя в норме, пользователи, скорее всего, не страдают. Если один из них отклонился - это сигнал для расследования.
USE-метрики (от Брендана Грегга) описывают состояние ресурсов:
- Utilization - насколько ресурс занят (CPU, память, пул соединений)
- Saturation - очередь ожидания ресурса
- Errors - ошибки самого ресурса
USE полезен для инфраструктурных компонентов: базы данных, брокера сообщений, сетевого интерфейса. RED отвечает на вопрос «как себя чувствует сервис», USE - «почему он так себя чувствует».
На практике стоит начинать с RED-метрик на каждый сервис, добавлять USE на ключевые зависимости и двигаться вглубь только там, где уже видна реальная проблема.
Метрики Prometheus: что и как собирать
Prometheus - стандарт де-факто для сбора метрик в backend-сервисах. Он работает по принципу pull: сам ходит к сервисам и забирает метрики с эндпоинта /metrics. Это удобно для мониторинга микросервисов, потому что не нужно настраивать отдельный push для каждого нового инстанса.
Четыре типа метрик в Prometheus:
| Тип | Что измеряет | Когда использовать |
|---|---|---|
| Counter | Монотонно растущее число | Количество запросов, ошибок, обработанных событий |
| Gauge | Значение, которое может расти и падать | Текущее использование памяти, размер очереди, число горутин |
| Histogram | Распределение значений по бакетам | Время ответа, размер payload |
| Summary | Перцентили на стороне клиента | Аналог histogram, но без возможности агрегации между инстансами |
Для RED-метрик нужны counter (для rate и errors) и histogram (для duration). Histogram позволяет вычислить p95 и p99 в PromQL, что невозможно с summary при нескольких инстансах.
Пример метрик, которые стоит добавить с первого дня:
http_requests_totalс лейбламиmethod,path,status_codehttp_request_duration_seconds(histogram) с теми же лейбламиdb_query_duration_secondsпо типу операции и таблицеexternal_calls_totalиexternal_call_errors_totalпо имени зависимости
Важный нюанс: не добавляйте в лейблы значения с высокой кардинальностью - user_id, request_id, полные URL с параметрами. Каждая уникальная комбинация лейблов создаёт отдельный временной ряд. Тысячи пользователей в лейбле = тысячи рядов = Prometheus начинает страдать.
Structured logging: почему произвольный текст не работает
Когда логи выглядят так: 2024-01-15 03:42:11 ERROR failed to process order 12345 for user 789 - их можно читать глазами, но нельзя надёжно агрегировать. Grep по тексту работает медленно, регулярные выражения ломаются при любом изменении формата, а добавить новое поле в середину строки означает сломать все существующие парсеры.
Structured logging - это запись каждого события как JSON-объекта с фиксированными полями:
{
"timestamp": "2024-01-15T03:42:11.234Z",
"level": "error",
"service": "order-service",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"order_id": "12345",
"user_id": "789",
"message": "failed to process order",
"error": "payment gateway timeout after 5000ms",
"duration_ms": 5023
}Такое событие можно фильтровать по любому полю, агрегировать по error, строить графики по duration_ms и - самое важное - находить все события, связанные с конкретным трейсом через trace_id.
Несколько правил, которые экономят время при инцидентах:
- Всегда логируйте
trace_idиspan_id- это мост между логами и трейсами - Добавляйте
duration_msдля всех операций, которые могут быть медленными - Логируйте ошибки с полным стектрейсом в отдельном поле, а не в строке message
- Используйте единый формат уровней: debug, info, warn, error - без вариаций
- Не логируйте в горячем пути на уровне debug в production: это убивает производительность
Для Go хорошо подходят slog (стандартная библиотека с 1.21) или zerolog. Для Java - Logback с JSON-encoder. Для Python - structlog. Все они поддерживают вывод в JSON без дополнительных усилий.
Grafana Loki: хранение и поиск по логам
Grafana Loki - это система хранения логов, которая индексирует только лейблы (метаданные), а не содержимое строк. Это делает её значительно дешевле Elasticsearch при сопоставимом функционале для большинства задач.
Как это работает: Promtail (или другой агент) собирает логи с сервисов, добавляет лейблы (service, environment, pod) и отправляет в Loki. В Grafana можно искать по лейблам через LogQL:
{service="order-service", level="error"}
| json
| trace_id="4bf92f3577b34da6a3ce929d0e0e4736"Главное преимущество Loki в связке с Grafana: из дашборда с метриками можно напрямую перейти к логам за тот же временной промежуток. Видите всплеск ошибок на графике Prometheus - один клик, и вы смотрите на конкретные строки логов за эти три минуты.
Ограничения Loki стоит учитывать сразу: полнотекстовый поиск по содержимому работает медленнее, чем в Elasticsearch. Если нужно часто искать по произвольным подстрокам в сообщениях - Loki будет тормозить на больших объёмах. Для большинства команд, которые только строят стек, это не проблема.
OpenTelemetry: единый стандарт инструментирования
До OpenTelemetry каждый инструмент трейсинга (Jaeger, Zipkin, Datadog) требовал собственного SDK. Переключиться с одного на другой означало переписать всё инструментирование. OpenTelemetry (OTel) решает эту проблему: один API и SDK для сбора метрик, логов и трейсов, а бэкенд для хранения можно выбирать независимо.
Архитектура типичного OTel-стека:
- Приложение использует OTel SDK для создания спанов, метрик и логов
- SDK отправляет данные в OpenTelemetry Collector
- Collector трансформирует и маршрутизирует данные: трейсы - в Jaeger или Tempo, метрики - в Prometheus, логи - в Loki
Collector здесь - ключевой элемент. Он позволяет менять бэкенды без изменения кода приложения, добавлять семплинг (не хранить 100% трейсов, а только репрезентативную выборку), обогащать данные метаданными (имя k8s-пода, версия деплоя).
Автоинструментирование OTel работает для большинства популярных фреймворков: FastAPI, Spring Boot, Express, Gin. Подключаете библиотеку - и HTTP-запросы, вызовы БД, gRPC-вызовы уже создают спаны автоматически. Для бизнес-специфичных операций добавляете ручное инструментирование.
# Пример ручного спана на Python с OTel
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span("process-payment") as span:
span.set_attribute("payment.order_id", order_id)
span.set_attribute("payment.amount", amount)
result = payment_gateway.charge(order_id, amount)
span.set_attribute("payment.status", result.status)Как связать всё вместе: trace_id как сквозной ключ
Три типа данных становятся по-настоящему полезными, когда между ними есть навигация. Связующий элемент - trace_id.
При входящем запросе OTel SDK генерирует trace_id и распространяет его через заголовки (W3C Trace Context: traceparent). Каждый сервис в цепочке принимает этот идентификатор и передаёт дальше. Все события, метрики и логи, связанные с этим запросом, получают тот же trace_id.
Результат: когда в Grafana видите всплеск ошибок, кликаете на точку на графике, переходите к логам за этот момент, находите trace_id из лога, переходите в Tempo или Jaeger и видите полный путь запроса со временем каждой операции. Это занимает 30-60 секунд вместо 30-60 минут ручного поиска.
В Grafana такая навигация настраивается через Derived Fields в настройках источника данных Loki: поле trace_id в логе становится кликабельной ссылкой в Tempo.
Практический стек для старта
Для нового проекта без жёстких инфраструктурных ограничений разумный набор выглядит так:
| Компонент | Инструмент | Зачем |
|---|---|---|
| Сбор метрик | Prometheus | Pull-модель, широкая экосистема экспортёров |
| Визуализация | Grafana | Дашборды для метрик, логов и трейсов в одном интерфейсе |
| Логи | Grafana Loki + Promtail | Дешевле Elasticsearch, нативная интеграция с Grafana |
| Трейсы | Grafana Tempo | Бесплатный бэкенд для OTel-трейсов, интеграция с Loki |
| Инструментирование | OpenTelemetry SDK | Единый стандарт, не привязывает к конкретному бэкенду |
| Алерты | Alertmanager | Маршрутизация алертов из Prometheus в Slack, PagerDuty и другие каналы |
Этот стек разворачивается через docker-compose за несколько часов и подходит как для локальной разработки, так и для небольших production-систем. Grafana публикует официальный репозиторий grafana/tempo с готовыми примерами для docker-compose.
Типичные ошибки при настройке
Не добавлять trace_id в логи. Логи и трейсы существуют параллельно, между ними нет навигации. При инциденте приходится вручную сопоставлять временные метки.
Логировать всё на уровне info или debug в production. Объём логов растёт экспоненциально при нагрузке, хранение становится дорогим, а поиск нужного события - медленным. Правило простое: debug только в dev, warn и error в production для исключительных ситуаций, info для значимых бизнес-событий.
Использовать высококардинальные лейблы в Prometheus. URL с параметрами, идентификаторы пользователей или версии запросов в лейблах создают миллионы временных рядов. Prometheus начинает потреблять гигабайты памяти, запросы замедляются, а в итоге приходится полностью пересобирать метрики.
Хранить 100% трейсов без семплинга. При тысячах запросов в секунду полное хранение трейсов обходится дорого. Tail-based sampling в OTel Collector позволяет сохранять только «интересные» трейсы: с ошибками, с высокой latency или случайную выборку 1-5% от успешных.
Строить алерты только по порогам, не по аномалиям. Алерт «latency выше 500ms» не сработает, если нормальная латентность сервиса - 400ms и она вдруг выросла до 450ms. Лучше алертить на значительное отклонение от baseline, а не на абсолютные значения.
Не тестировать алерты. Алерт, который ни разу не сработал - возможно, просто никогда не проверялся. Регулярно проверяйте, что правила в Alertmanager корректны и что уведомления реально приходят в нужные каналы.
Чеклист: что проверить перед выходом в production
- Сервис экспортирует RED-метрики: rate, error rate, duration (histogram с p95/p99)
- Все логи в JSON-формате с полями timestamp, level, service, trace_id, message
- OTel SDK подключён, trace_id проставляется в заголовки при исходящих вызовах
- Логи агрегируются в Loki, настроен Derived Field для перехода от trace_id к Tempo
- В Grafana есть базовый дашборд с RED-метриками для каждого сервиса
- Настроены алерты на error rate и p99 latency с маршрутизацией в Alertmanager
- Включён семплинг трейсов в OTel Collector (не храните 100% в production)
- Проверена навигация: метрика - лог - трейс по одному инциденту вручную
- Отсутствуют высококардинальные лейблы в метриках Prometheus
- Retention настроен явно: сколько хранить логи, метрики и трейсы