# Observability And Instrumentation

> Инструментирует код так, чтобы поведение в продакшне было видимым и диагностируемым. Используй при добавлении логирования, метрик, трассировки или алертов. Используй при выпуске любой функциональности, которая пойдёт в продакшн и для которой нужны доказательства работоспособности. Используй, когда о проблемах в продакшне сообщают, но по имеющимся данным невозможно понять, что произошло.

- Skill: `aleksandr-litvinenko/observability-and-instrumentation` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aleksandr-litvinenko/observability-and-instrumentation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aleksandr-litvinenko/observability-and-instrumentation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Aleksandr-Litvinenko (https://skillmd.com/u/aleksandr-litvinenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aleksandr-litvinenko/observability-and-instrumentation

---


# Наблюдаемость и инструментирование

## Обзор

Код, который ты не можешь наблюдать, — это код, который ты не можешь эксплуатировать. Наблюдаемость — это способность ответить на вопрос «что система делает и почему?» снаружи, по телеметрии, которую код испускает. Инструментирование не прикручивается после запуска: оно пишется вместе с функциональностью, ровно как тесты. Если фича уехала без телеметрии, первый же баг от пользователя превращается в археологию вместо запроса к данным.

## Когда применять

- Строишь любую функциональность, которая пойдёт в продакшн
- Добавляешь новый сервис, эндпоинт, фоновую задачу или внешнюю интеграцию
- Продакшн-инцидент диагностировался слишком долго («мы не могли понять, что произошло»)
- Настраиваешь или ревьюишь правила алертинга
- Ревьюишь пулл-реквест, добавляющий ввод-вывод, повторы, очереди или межсервисные вызовы

**НЕ для:**
- Диагностики сбоя, происходящего прямо сейчас — используй скилл `debugging-and-error-recovery` (наблюдаемость это то, что делает тот скилл быстрым в следующий раз)
- Профилирования и оптимизации измеренной медленности — используй скилл `performance-optimization`
- Чеклистов мониторинга в день запуска и триггеров отката — см. скилл `shipping-and-launch`; этот скилл покрывает инструментирование, которое их питает

## Процесс

### 1. Определи, что значит «работает», до инструментирования

Телеметрия без вопроса — это шум. Прежде чем добавлять инструментирование, запиши 2–4 вопроса, которые задаст дежурный инженер об этой функциональности:

```
ФУНКЦИОНАЛЬНОСТЬ: повтор платежа на оформлении заказа
ВОПРОСЫ, КОТОРЫЕ ЗАДАСТ ДЕЖУРНЫЙ:
1. Какая доля платежей проходит с первой попытки, а какая после повтора?
2. Когда платёж окончательно не проходит, почему? (ошибка провайдера? таймаут? валидация?)
3. Не стал ли платёжный провайдер медленнее обычного?
→ Каждый сигнал ниже должен помогать ответить на один из этих вопросов.
```

Если ты не можешь назвать вопросы — ты не готов инструментировать: залогируешь всё и не узнаешь ничего.

### 2. Выбери правильный сигнал под каждый вопрос

| Сигнал | На что отвечает | Профиль стоимости | Пример |
|---|---|---|---|
| **Структурный лог** | «Что произошло в этом конкретном случае?» | На событие; растёт с трафиком | `payment_failed` с кодом ошибки провайдера |
| **Метрика** | «Как часто / как быстро в совокупности?» | Фиксированная на серию; дёшево запрашивать | p99 задержки вызовов провайдера |
| **Трассировка** | «Куда ушло время между сервисами?» | На запрос; обычно с семплированием | Одно медленное оформление, разложенное по шагам |

Правило большого пальца: метрики говорят, **что** что-то не так, трассировки — **где**, логи — **почему**.

### 3. Структурное логирование

Логируй события, а не прозу. Каждая строка лога — это JSON-объект со стабильным именем события и машиночитаемыми полями:

```typescript
// ПЛОХО: интерполяция строк — непригодно для запросов, несогласованно
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` | Диагностические подробности | В продакшне по умолчанию выключено |

**Идентификаторы корреляции обязательны.** Генерируй (или принимай) идентификатор запроса на границе системы и прикрепляй его к каждой строке лога, каждому спану и каждому исходящему вызову. Без него ты не восстановишь один запрос из перемешанных логов:

```typescript
// 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** на каждом эндпоинте и каждой внешней зависимости: **R**ate (запросов в секунду), **E**rrors (доля отказов), **D**uration (гистограмма задержек, а не среднее). Для ресурсов (очереди, пулы, хосты) используй **USE**: **U**tilization (загрузка), **S**aturation (насыщение), **E**rrors (ошибки).

Как и с трассировкой, независимый от вендора путь — API метрик OpenTelemetry (тот же SDK и контекст, что в шаге 5). Пример ниже использует `prom-client` от Prometheus — один из распространённых бэкендов, а не единственный; правила RED/USE и по кардинальности одинаковы в любом случае.

```typescript
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 и распространённые клиенты БД почти без кода:

```typescript
// 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%
```

Алерты по причинам срабатывают, когда всё в порядке, и пропускают отказы, которых ты не предвидел. Алерты по симптомам срабатывают ровно тогда, когда пользователям больно, независимо от причины.

Правила для каждого создаваемого алерта:

1. **Он должен требовать действия.** Если реакция — «забей, само пройдёт», удали алерт.
2. **Он ссылается на runbook** — пусть даже на три строки: что это значит, какой запрос выполнить первым, куда эскалировать.
3. **У него есть порог и длительность**, обоснованные SLO или историческими данными, а не догадкой.
4. Используй только две степени серьёзности: **побудка** (затрагивает пользователей, действовать сейчас) и **тикет** (деградация, действовать на этой неделе). Третий уровень превращается в шум, который приучает людей игнорировать всё.

### 7. Проверь саму телеметрию

Инструментирование — это код, и он может быть неверным. Прежде чем считать работу законченной, пройди по путям и посмотри на реальный вывод:

- Вызови ошибку на стенде → найди её в логах по `requestId`, убедись, что поля структурны (а не `[object Object]`)
- Пусти тестовый трафик → убедись, что серии метрик появляются с ожидаемыми метками и вменяемыми значениями
- Проследи один запрос через сервисы в интерфейсе трассировки → нет обрывов спанов
- Запусти каждый новый алерт один раз (временно понизив порог) → убедись, что он доходит до нужного канала, а ссылка на runbook работает

## Типовые самооправдания

| Самооправдание | Как на самом деле |
|---|---|
| «Добавлю логирование, когда заработает» | «Потом» превращается в «после первого инцидента», а это самый дорогой момент обнаружить, что ты слепой. Инструментируй по ходу стройки. |
| «Больше логов = больше наблюдаемости» | Неструктурированный шум делает инциденты медленнее, а не быстрее. Три события, пригодных для запроса, лучше трёхсот строк прозы. |
| «console.log пока сойдёт» | Неструктурированный вывод нельзя отфильтровать, скоррелировать и повесить на него алерт. Структурный логгер стоит пять лишних минут один раз. |
| «Когда что-то сломается, просто посмотрим на дашборды» | Дашборды, построенные без заданных вопросов, показывают всё, кроме ответа. Начинай с вопросов дежурного. |
| «Алерты на всё важное, потом настроим» | Шумный пейджер приучает людей его игнорировать. Настройка так и не случается, а пропущенная настоящая побудка случается. |
| «Идентификатор пользователя в метке метрики упростит отладку» | И заодно уронит твой бэкенд метрик. Поиск по значениям высокой кардинальности — это про логи и трассировки. |
| «Трассировка избыточна для наших двух сервисов» | Два сервиса — это уже вопросы о межсервисной задержке, на которые логи не отвечают. Автоинструментирование делает цену ничтожной. |

## Тревожные признаки

- Пулл-реквест с повторами, очередями или внешними вызовами и нулём новой телеметрии
- Строки логов, собранные интерполяцией строк вместо структурных полей
- Нет идентификатора корреляции/запроса — каждая строка лога сирота
- Метрики с метками из идентификаторов пользователей, сырых URL или текстов ошибок (бомба кардинальности)
- Задержка отслеживается как среднее, без перцентилей
- Алерты, которые срабатывают ежедневно и подтверждаются без действий
- Алерты по причинам (CPU, память) будят людей, а доля пользовательских ошибок не мониторится
- Секреты, токены или тела запросов целиком, попадающие в логи
- «У меня на машине работает» как единственное доказательство здоровья продакшн-фичи

## Проверка

После инструментирования функциональности убедись:

- [ ] Вопросы дежурного по этой функциональности записаны, и каждый сигнал соответствует одному из них
- [ ] Весь вывод логов структурен (JSON), со стабильными именами событий и идентификатором корреляции в каждой строке
- [ ] Ни в одной строке лога нет секретов, токенов или незамаскированных персональных данных (проверь выборочно реальный вывод)
- [ ] RED-метрики есть для каждого нового эндпоинта и каждой внешней зависимости, с ограниченными множествами меток
- [ ] Задержка — это гистограмма; p95/p99 доступны для запросов
- [ ] Один запрос можно проследить целиком в интерфейсе трассировки без обрывов спанов
- [ ] Каждый новый алерт основан на симптоме, имеет ссылку на runbook и был запущен на тесте один раз
- [ ] Вызванный на стенде сбой был локализован только по телеметрии, без чтения исходного кода

Краткую версию этого списка, включая предрелизные ворота по инструментированию, см. в `../../references/observability-checklist.md`.

