Наблюдаемость и инструментирование
Обзор
Код, который ты не можешь наблюдать, — это код, который ты не можешь эксплуатировать. Наблюдаемость — это способность ответить на вопрос «что система делает и почему?» снаружи, по телеметрии, которую код испускает. Инструментирование не прикручивается после запуска: оно пишется вместе с функциональностью, ровно как тесты. Если фича уехала без телеметрии, первый же баг от пользователя превращается в археологию вместо запроса к данным.
Когда применять
- Строишь любую функциональность, которая пойдёт в продакшн
- Добавляешь новый сервис, эндпоинт, фоновую задачу или внешнюю интеграцию
- Продакшн-инцидент диагностировался слишком долго («мы не могли понять, что произошло»)
- Настраиваешь или ревьюишь правила алертинга
- Ревьюишь пулл-реквест, добавляющий ввод-вывод, повторы, очереди или межсервисные вызовы
НЕ для:
- Диагностики сбоя, происходящего прямо сейчас — используй скилл
debugging-and-error-recovery(наблюдаемость это то, что делает тот скилл быстрым в следующий раз) - Профилирования и оптимизации измеренной медленности — используй скилл
performance-optimization - Чеклистов мониторинга в день запуска и триггеров отката — см. скилл
shipping-and-launch; этот скилл покрывает инструментирование, которое их питает
Процесс
1. Определи, что значит «работает», до инструментирования
Телеметрия без вопроса — это шум. Прежде чем добавлять инструментирование, запиши 2–4 вопроса, которые задаст дежурный инженер об этой функциональности:
ФУНКЦИОНАЛЬНОСТЬ: повтор платежа на оформлении заказа
ВОПРОСЫ, КОТОРЫЕ ЗАДАСТ ДЕЖУРНЫЙ:
1. Какая доля платежей проходит с первой попытки, а какая после повтора?
2. Когда платёж окончательно не проходит, почему? (ошибка провайдера? таймаут? валидация?)
3. Не стал ли платёжный провайдер медленнее обычного?
→ Каждый сигнал ниже должен помогать ответить на один из этих вопросов.
Если ты не можешь назвать вопросы — ты не готов инструментировать: залогируешь всё и не узнаешь ничего.
2. Выбери правильный сигнал под каждый вопрос
| Сигнал | На что отвечает | Профиль стоимости | Пример |
|---|---|---|---|
| Структурный лог | «Что произошло в этом конкретном случае?» | На событие; растёт с трафиком | payment_failed с кодом ошибки провайдера |
| Метрика | «Как часто / как быстро в совокупности?» | Фиксированная на серию; дёшево запрашивать | p99 задержки вызовов провайдера |
| Трассировка | «Куда ушло время между сервисами?» | На запрос; обычно с семплированием | Одно медленное оформление, разложенное по шагам |
Правило большого пальца: метрики говорят, что что-то не так, трассировки — где, логи — почему.
3. Структурное логирование
Логируй события, а не прозу. Каждая строка лога — это JSON-объект со стабильным именем события и машиночитаемыми полями:
// ПЛОХО: интерполяция строк — непригодно для запросов, несогласованно
logger.info(`Payment ${id} failed for user ${userId} after ${n} retries`);
// ХОРОШО: стабильное имя события + структурные поля
logger.warn({
event: 'payment_failed',
paymentId: id,
provider: 'stripe',
errorCode: err.code,
attempt: n,
}, 'payment failed');
Уровни логирования — используй их согласованно:
| Уровень | Что означает | Действие дежурного |
|---|---|---|
error |
Нарушен инвариант; возможно, кому-то надо вмешаться | Разобраться |
warn |
Деградация, но обработанная (повтор удался, использован запасной путь) | Следить за трендом |
info |
Значимое бизнес-событие (заказ размещён, задание завершено) | Никакого |
debug |
Диагностические подробности | В продакшне по умолчанию выключено |
Идентификаторы корреляции обязательны. Генерируй (или принимай) идентификатор запроса на границе системы и прикрепляй его к каждой строке лога, каждому спану и каждому исходящему вызову. Без него ты не восстановишь один запрос из перемешанных логов:
// Express: дочерний логгер на запрос, идентификатор пробрасывается дальше
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] ?? crypto.randomUUID();
req.log = logger.child({ requestId: req.id });
res.setHeader('x-request-id', req.id);
next();
});
Никогда не логируй секреты, токены, пароли и персональные данные целиком. Это жёсткое правило из скилла security-and-hardening: конвейеры телеметрии — классический путь утечки данных. Веди белый список полей; не логируй тела запросов целиком.
4. Метрики
Для сервисов, работающих на запросах, инструментируй RED на каждом эндпоинте и каждой внешней зависимости: Rate (запросов в секунду), Errors (доля отказов), Duration (гистограмма задержек, а не среднее). Для ресурсов (очереди, пулы, хосты) используй USE: Utilization (загрузка), Saturation (насыщение), Errors (ошибки).
Как и с трассировкой, независимый от вендора путь — API метрик OpenTelemetry (тот же SDK и контекст, что в шаге 5). Пример ниже использует prom-client от Prometheus — один из распространённых бэкендов, а не единственный; правила RED/USE и по кардинальности одинаковы в любом случае.
import { Histogram } from 'prom-client';
const httpDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route', 'status_class'], // '2xx', а не '200'
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
Кардинальность — вот где всё ломается. Каждая уникальная комбинация меток — отдельный временной ряд. Метки должны браться из небольших фиксированных множеств (шаблон маршрута, класс статуса, имя провайдера). Никогда не используй идентификаторы пользователей, сырые URL, тексты ошибок и прочие неограниченные значения как метки — им место в логах и трассировках.
НОРМАЛЬНО как метка: route="/api/tasks/:id" status_class="5xx" provider="stripe"
НИКОГДА не метка: user_id, email, request_id, полный URL, текст сообщения об ошибке
Средние — никогда, перцентили — всегда: среднее прячет тот 1% пользователей, которым сейчас очень плохо. Используй гистограммы и смотри p50/p95/p99.
5. Распределённая трассировка
Используй OpenTelemetry — это независимый от вендора стандарт, а автоинструментирование покрывает HTTP, gRPC и распространённые клиенты БД почти без кода:
// tracing.ts — должен импортироваться раньше всего остального
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
serviceName: 'checkout-service',
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Добавляй ручные спаны только вокруг осмысленных внутренних единиц работы (например, applyDiscounts, chargeProvider) и прикрепляй атрибуты, по которым дежурный будет фильтровать. Пробрасывай контекст через каждую асинхронную границу — HTTP-заголовки, метаданные сообщений очереди, — иначе трассировка обрывается на этом разрыве. По умолчанию семплируй на входе с низкой частотой; сохраняй 100% ошибок, если бэкенд поддерживает семплирование по итогу.
6. Алертинг
Оповещай о симптомах, которые чувствуют пользователи, а не о причинах:
СИМПТОМ (достоин побудки): ПРИЧИНА (на дашборд, не в побудку):
доля ошибок > 1% в течение 5 мин CPU на 85%
p99 задержки > 2 с перезапустился один под
возраст очереди > 10 мин диск на 70%
Алерты по причинам срабатывают, когда всё в порядке, и пропускают отказы, которых ты не предвидел. Алерты по симптомам срабатывают ровно тогда, когда пользователям больно, независимо от причины.
Правила для каждого создаваемого алерта:
- Он должен требовать действия. Если реакция — «забей, само пройдёт», удали алерт.
- Он ссылается на runbook — пусть даже на три строки: что это значит, какой запрос выполнить первым, куда эскалировать.
- У него есть порог и длительность, обоснованные SLO или историческими данными, а не догадкой.
- Используй только две степени серьёзности: побудка (затрагивает пользователей, действовать сейчас) и тикет (деградация, действовать на этой неделе). Третий уровень превращается в шум, который приучает людей игнорировать всё.
7. Проверь саму телеметрию
Инструментирование — это код, и он может быть неверным. Прежде чем считать работу законченной, пройди по путям и посмотри на реальный вывод:
- Вызови ошибку на стенде → найди её в логах по
requestId, убедись, что поля структурны (а не[object Object]) - Пусти тестовый трафик → убедись, что серии метрик появляются с ожидаемыми метками и вменяемыми значениями
- Проследи один запрос через сервисы в интерфейсе трассировки → нет обрывов спанов
- Запусти каждый новый алерт один раз (временно понизив порог) → убедись, что он доходит до нужного канала, а ссылка на runbook работает
Типовые самооправдания
| Самооправдание | Как на самом деле |
|---|---|
| «Добавлю логирование, когда заработает» | «Потом» превращается в «после первого инцидента», а это самый дорогой момент обнаружить, что ты слепой. Инструментируй по ходу стройки. |
| «Больше логов = больше наблюдаемости» | Неструктурированный шум делает инциденты медленнее, а не быстрее. Три события, пригодных для запроса, лучше трёхсот строк прозы. |
| «console.log пока сойдёт» | Неструктурированный вывод нельзя отфильтровать, скоррелировать и повесить на него алерт. Структурный логгер стоит пять лишних минут один раз. |
| «Когда что-то сломается, просто посмотрим на дашборды» | Дашборды, построенные без заданных вопросов, показывают всё, кроме ответа. Начинай с вопросов дежурного. |
| «Алерты на всё важное, потом настроим» | Шумный пейджер приучает людей его игнорировать. Настройка так и не случается, а пропущенная настоящая побудка случается. |
| «Идентификатор пользователя в метке метрики упростит отладку» | И заодно уронит твой бэкенд метрик. Поиск по значениям высокой кардинальности — это про логи и трассировки. |
| «Трассировка избыточна для наших двух сервисов» | Два сервиса — это уже вопросы о межсервисной задержке, на которые логи не отвечают. Автоинструментирование делает цену ничтожной. |
Тревожные признаки
- Пулл-реквест с повторами, очередями или внешними вызовами и нулём новой телеметрии
- Строки логов, собранные интерполяцией строк вместо структурных полей
- Нет идентификатора корреляции/запроса — каждая строка лога сирота
- Метрики с метками из идентификаторов пользователей, сырых URL или текстов ошибок (бомба кардинальности)
- Задержка отслеживается как среднее, без перцентилей
- Алерты, которые срабатывают ежедневно и подтверждаются без действий
- Алерты по причинам (CPU, память) будят людей, а доля пользовательских ошибок не мониторится
- Секреты, токены или тела запросов целиком, попадающие в логи
- «У меня на машине работает» как единственное доказательство здоровья продакшн-фичи
Проверка
После инструментирования функциональности убедись:
- Вопросы дежурного по этой функциональности записаны, и каждый сигнал соответствует одному из них
- Весь вывод логов структурен (JSON), со стабильными именами событий и идентификатором корреляции в каждой строке
- Ни в одной строке лога нет секретов, токенов или незамаскированных персональных данных (проверь выборочно реальный вывод)
- RED-метрики есть для каждого нового эндпоинта и каждой внешней зависимости, с ограниченными множествами меток
- Задержка — это гистограмма; p95/p99 доступны для запросов
- Один запрос можно проследить целиком в интерфейсе трассировки без обрывов спанов
- Каждый новый алерт основан на симптоме, имеет ссылку на runbook и был запущен на тесте один раз
- Вызванный на стенде сбой был локализован только по телеметрии, без чтения исходного кода
Краткую версию этого списка, включая предрелизные ворота по инструментированию, см. в ../../references/observability-checklist.md.