Наблюдаемость (Observability)

Назначение: свойства и практики, позволяющие понимать внутреннее состояние распределённой системы по её внешним проявлениям — телеметрии — и диагностировать как известные, так и ранее не встречавшиеся отказы.

Аудитория: архитекторы, техлиды, backend- и platform-разработчики, SRE-инженеры.

Статус: устоявшаяся дисциплина; термин — из теории управления (Рудольф Калман, 1960), современная трактовка — практики cloud-native и Google SRE (2016), Charity Majors и сообщество observability-платформ (2017+); фактический стандарт инструментирования — OpenTelemetry (CNCF, с 2019).

Не путать с: мониторингом (подмножество наблюдаемости — наблюдение за заранее известными режимами отказа), логированием как таковым (один из трёх сигналов), APM-продуктом (инструмент, а не свойство системы).

Общее

Наблюдаемость (observability) — свойство системы, позволяющее понять её внутреннее состояние по внешним проявлениям: не вскрывая систему, по сигналам, которые она испускает, ответить на вопросы о том, что происходит внутри. Термин пришёл из теории управления: в основополагающей работе Рудольфа Калмана (Rudolf Kálmán) On the General Theory of Control Systems (1960) система называется наблюдаемой, если её внутреннее состояние в некоторый момент времени может быть восстановлено по знанию её внешних выходов на конечном интервале. Наблюдаемость там — двойник управляемости (controllability): они определяют вместе, насколько системой вообще можно управлять на основании измерений. Перенос термина в программную инженерию сохраняет суть: внутренние состояния — это работа сервисов, баз данных и очередей, а «внешние проявления» — телеметрия: логи, метрики, трассировки.

Потребность в наблюдаемости резко возрастает с распределённостью системы. В монолите диагностика опирается на инструменты одного процесса: отладчик, стек-трейс, профиль — путь от симптома к причине лежит внутри одного адресного пространства. В микросервисной системе сбой не имеет адреса: отказ проявляется как тайм-аут на внешней границе, а его причина — истощённый пул соединений или медленный запрос — скрыта несколькими сервисами ниже; ни один отдельный журнал не содержит всей картины. Наблюдаемость — это ответ на асимметрию: сложность внутренней структуры растёт быстрее, чем способность человека её непосредственно обозревать, и единственный способ сохранить понимание — сделать так, чтобы система сама достаточно богато рассказывала о себе.

Отсюда — главное различие, которое стоит зафиксировать с самого начала: наблюдаемость против мониторинга. Эти слова часто используют как синонимы, но за ними разные задачи. Мониторинг — наблюдение за системой по заранее определённому набору условий: метрики, дашборды и алерты, настроенные под известные режимы отказа (диск заполнен, ответ 500, задержка выше порога). Мониторинг отвечает на вопросы, которые сформулированы до инцидента. Наблюдаемость — свойство, позволяющее задавать вопросы, которые не удалось предвидеть: исследовать новые, ранее не встречавшиеся отказы (novel failure modes), комбинировать данные произвольными способами и спускаться от симптома к причине без повторного релиза с добавленным логированием. Такое различие — «известные неизвестные против неизвестных неизвестных» — популяризировала Чарити Мейджорс (Charity Majors, Honeycomb, 2017+). Соотношение между понятиями иерархично: мониторинг — подмножество наблюдаемости: алерты и дашборды остаются нужными, но наблюдаемая система позволяет большее, чем реагирование на предписанный список условий.

Важно понимать и то, чем наблюдаемость не является. Она не продукт, который можно купить, и не стек, который достаточно развернуть. Наблюдаемость — свойство дизайна системы: она возникает из инструментирования (сервисы испускают структурированные, связные сигналы), из организации данных (сигналы коррелируют между собой) и из культуры эксплуатации (вопрос «что случилось?» должен решаться запросом, а не гаданием). Система, пишущая неструктурированные логи без идентификаторов, не станет наблюдаемой от развёртывания самого дорогого конвейера. Обратное тоже верно: цена наблюдаемости реальна — хранение телеметрии, эксплуатация конвейера, дисциплина команд, — и зрелая команда выбирает глубину наблюдаемости соразмерно сложности системы (раздел «Плюсы и минусы»).

Полезно держать в уме и лестницу вопросов, на которые отвечает телеметрия. Первый уровень — «что сломалось?»: ответил ли сервис, жив ли процесс, доступен ли эндпоинт; этого достаточно мониторингу. Второй — «где сломалось?»: какой сервис, какой ресурс, какой участок пути запроса; это территория метрик и трассировок. Третий — «почему сломалось?»: какое конкретное событие, с какими параметрами, в каком контексте; это территория логов и связных идентификаторов. Мониторинг закрывает первый уровень уверенно, второй — частично, третий — почти никогда; наблюдаемость — это про всю лестницу целиком. Как нефункциональное требование наблюдаемость закладывается при проектировании: решения о телеметрии, принимаемые после релиза, стоят на порядок дороже решений, заложенных в архитектуру с самого начала — об этом же напоминает и введение в архитектуру в обзоре сквозных задач архитектуры ПО.

Практическая проверка наблюдаемости конкретной системы — не инвентаризация инструментов, а дежурные вопросы. «Какая доля оформлений заказа за последний час завершилась ошибкой и с каким кодом?» — метрики. «Покажи медленные запросы пользователя u_771 за вчера» — трейсы или логи. «Что происходило с заказом 10427 на пути через пять сервисов?» — связные логи или трейс. Если ответ находится запросом за минуты — система наблюдаема; если требует правки кода, релиза и ожидания повторения события — нет, сколько бы телеметрии ни собиралось.

Историческая рамка коротка. Поколение систем мониторинга 2000-х (Nagios, Zabbix) наблюдало преимущественно инфраструктуру — доступность хостов и процессов. Внутри Google в 2000-е сложилась практика SRE — измерение уровня сервиса, а не железа, увековеченная книгой Site Reliability Engineering (2016); там же появилась статья о распределённом трейсинге Dapper (2010), доказавшая, что сквозную трассировку можно собирать на масштабе миллиардов запросов. Cloud-native экосистема вынесла эти идеи в open source (Prometheus, Zipkin, Jaeger, ELK), а слияние инструментационных стандартов в OpenTelemetry (2019) сделало vendor-neutral телеметрию нормой. Сегодня наблюдаемость — одна из сквозных забот распределённой архитектуры наряду с масштабируемостью и отказоустойчивостью: без неё ни та, ни другая не диагностируемы.

Три кита: логи, метрики, трассировки

Классическая модель наблюдаемости опирается на три типа телеметрии — «три кита» (three pillars): метрики, логи и трассировки. Каждый отвечает на собственный класс вопросов, имеет собственную экономику и собственные ограничения.

Метрики — агрегированный пульс системы

Метрики (metrics) — числовые временные ряды: агрегированные измерения с течением времени. Типы базовые:

  • счётчик (counter) — монотонно растущая величина: сколько раз событие произошло (запросы, ошибки, сообщения очереди);
  • датчик (gauge) — текущее значение, которое может расти и падать: число соединений, длина очереди, размер кэша;
  • гистограмма (histogram) — распределение величины по бакетам: задержки, размеры ответов; из бакетов вычисляются квантили (p50, p95, p99).

Так выглядит экспозиция метрик в формате Prometheus:

# HELP http_requests_total Общее число HTTP-запросов
# TYPE http_requests_total counter
http_requests_total{service="orders",code="200",method="POST"} 41803
http_requests_total{service="orders",code="500",method="POST"}   127
# HELP http_request_duration_seconds Время обработки запросов
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{service="orders",le="0.1"} 21744
http_request_duration_seconds_bucket{service="orders",le="0.3"} 39022
http_request_duration_seconds_bucket{service="orders",le="+Inf"} 41930

Метрики предагрегированы и компактны: одна серия — один столбец чисел; поэтому они дёшевы в хранении, быстры в запросах и идеальны для алертов, трендов и дашбордов. Доставка бывает двух моделей: pull — сервер метрик сам опрашивает экспозиции приложений (модель Prometheus; опрос по расписанию упрощает обнаружение «молчащих» сервисов) — и push — приложение само отправляет значения (StatsD, OTLP; удобнее для краткоживущих процессов и serverless). Их ограничение общее — кардинальность: каждое уникальное сочетание меток (имя сервиса, эндпоинт, код ответа) образует отдельный временной ряд, и метрика с меткой user_id на десяти тысячах пользователей порождает десять тысяч рядов — хранилища временных рядов этого не прощают. Метрика отвечает «сколько» и «как быстро», но не «почему»: счётчик знает, что ошибок стало больше, — но не знает, каких именно заказов и из-за чего.

Логи — события с контекстом

Логи (logs) — дискретные события с текстом и атрибутами, записываемые в момент происшествия: «заказ 10427 оплачен», «пул соединений исчерпан», «пользователь аутентифицирован». Лог — атомарная единица контекста: в отличие от метрики он может нести произвольные поля — идентификаторы, параметры, стек ошибки, решение бизнес-правила. Именно поэтому расследование любого инцидента рано или поздно упирается в журналы: это единственный сигнал, в котором единичное событие видно целиком.

Плата — объём и цена. Полнотекстовое хранение логов на больших системах — одна из самых дорогих статей инфраструктуры: журналы растут пропорционально трафику, поэтому в дело идут retention-политики (логи ошибок — месяц, отладочные — дни), семплирование отладочных событий и разделение уровней. Лог отвечает «что именно случилось», но по одному событию нельзя понять распределённый путь и нельзя посчитать агрегат.

Трейсы — путь запроса через границы

Трейсы (traces) — путь запроса через систему: дерево сегментов (спанов), показывающее, через какие сервисы, в каком порядке и с какими задержками прошёл один логический запрос. Трейс связывает причинно-следственную цепочку: тот самый «путь от тайм-аута на границе до медленного запроса в базе двумя сервисами ниже». Ни метрика, ни лог через границы процессов не смотрят — это делает только трассировка.

Плата — объём: трейс на каждый запрос порождает гигабайты данных, поэтому трейсы почти всегда семплируются (каким образом — в разделе о трейсинге). Трейс отвечает «где именно», но без логов и метрик беден деталями: спан знает длительность операции, но не её содержание.

Сравнение

ПризнакМетрикиЛогиТрейсы
Характер данныхЧисловые временные ряды, агрегатыДискретные события с текстом и полямиДерево спанов одного запроса
КардинальностьОграничена числом рядов; высокие метки дорогиПроизвольная (любые поля события)Ограничена семплированием
Хранение и ценаСамые дёшевы: число на интервалСамые дорогие: полный текст с retentionПромежуточные; почти всегда семплируются
Типовой запрос«Сколько и как быстро» — тренды, алерты«Что именно случилось» — поиск по событию«Где запрос провёл время» — путь и вклад
СилаДешёвый агрегированный взгляд в реальном времениМаксимальный контекст единичного событияПричинно-следственная связь между сервисами
Типовые инструментыPrometheus, Grafana, StatsDELK, Loki, Fluent BitJaeger, Tempo, Zipkin

Ключевой тезис таблицы: три кита не заменяют друг друга. Попытка «обойтись метриками» лишает систему контекста: алерт сработал, а что случилось — неизвестно. Попытка «писать всё в логи» лишает агрегатов: на вопрос «какая медианная задержка за час» полнотекстовый поиск отвечает ценой полного скана. Третья крайность — «трейсы вместо логов» — обедняет детали: спан знает длительность, но не судьбу конкретного заказа. Зрелый конвейер использует все три сигнала связанными: метрика поднимает алерт, трейс показывает путь, лог раскрывает событие (связь обеспечивает общий идентификатор — о нём в разделе о структурном логировании).

Когда чего достаточно: небольшому сервису с одним процессом и одной базой нередко хватает метрик и логов — путь запроса тривиален. Распределённой системе из десятков сервисов трассировка становится обязательной: без неё расследование распределённого сбоя вырождается в ручную сборку хронологии по журналам разных команд. Обратное неверно и для малых систем: наблюдаемость начинается не с количества инструментов, а со связности и структурированности сигналов.

Распределённый трейсинг и OpenTelemetry

Распределённый трейсинг (distributed tracing) — восстановление пути одного логического запроса через границы процессов и сервисов. Терминология:

  • Span (спан) — один сегмент работы: обработка HTTP-запроса, вызов БД, публикация сообщения. У спана есть имя, начало, конец, атрибуты (эндпоинт, код ответа, размер), события и статус.
  • Trace (трейс) — дерево спанов, объединённых общим trace_id: от входа запроса в систему до последнего каскадного вызова. Родительские и дочерние спаны связаны parent_id.
  • Context propagation (распространение контекста) — механизм, передающий идентичность трейса через границы сервисов: вызывающий сервис кладёт trace_id текущего спана в заголовки запроса, вызываемый — извлекает и продолжает трассу как родительский контекст.

Распространение контекста — сердце трейсинга: без него спаны соседних сервисов нельзя сшить в одно дерево. Форматов два основных: W3C Trace Context — стандарт консорциума W3C, заголовок traceparent с trace_id, parent_id и флагами (современный выбор по умолчанию), — и B3 — заголовки X-B3-TraceId/X-B3-SpanId, появившиеся в экосистеме Zipkin и всё ещё распространённые. К ним примыкает W3C Baggage — механизм передачи бизнес-контекста (идентификатор пользователя, A/B-вариант) вдоль той же цепочки.

Заголовок traceparent в HTTP-запросе выглядит так:

GET /orders/10427 HTTP/1.1
Host: orders.internal
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
              │  │                               │                │
версия ────────┘  │                               │                └─ флаги (sampled)
trace_id ─────────┘                               └─ parent_id (id родительского спана)

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

Так выглядит дерево спанов запроса «оформление заказа» (числа — длительности спанов):

trace_id: 4bf92f3577b34da6a3ce929d0e0e4736

[ gateway: POST /checkout ]─────────────────────────── 234 ms
  │
  ├─[ orders: create_order ]────────────────────────── 189 ms
  │    ├─[ postgres: INSERT orders ]───────── 12 ms
  │    └─[ payments: charge ]─────────────── 152 ms
  │         └─[ postgres: UPDATE ledger ]──── 23 ms
  ├─[ catalog: GET /items ]───────────────────────────── 31 ms
  └─[ notifications: publish order_created ]───────────── 8 ms

Дерево мгновенно отвечает на вопрос, недоступный ни логам, ни метрикам по отдельности: из 234 мс оформления заказа 152 мс занимает платёжный сервис, а внутри него — обновление журнала операций. Алерт видел бы только «оформление медленное»; трейс показывает, где именно. Один спан этого дерева в представлении бэкенда:

{
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "parent_span_id": "b7ad6b7169203331",
  "name": "POST /payments/charge",
  "kind": "server",
  "start": "2026-09-30T12:04:11.482Z",
  "duration_ms": 152,
  "attributes": { "http.status_code": 200, "payment.method": "card" },
  "status": "OK"
}

Возможность трейсинга на промышленном масштабе доказала статья Google о системе Dapper (2010): сквозная трассировка всех запросов с семплированием оказалась настолько дешёвой, что стала обязательной частью инфраструктуры. Идея Dapper была воспроизведена open source: Zipkin (Twitter, 2012) и Jaeger (Uber, 2017) дали бэкенды хранения и визуализации, а библиотеки инструментирования плодились по языкам и вендорам — пока не стало очевидно, что каждое приложение «пришивается» к конкретному бэкенду намертво. Ответом стала консолидация: в 2019 году проекты OpenTracing (CNCF, API трейсинга) и OpenCensus (Google, трейсинг и метрики) объединились в OpenTelemetry (OTel).

OpenTelemetry — проект CNCF, ставший фактическим стандартом vendor-neutral телеметрии. Его архитектура разделена на слои:

  • API — интерфейсы в каждом языке: спаны, метрики, логи, распространение контекста. Прикладной код зависит только от API.
  • SDK — реализация API: конфигурация, семплирование, пакетирование, экспорт. Выбор бэкенда — конфигурация SDK, а не код.
  • Collector — автономный конвейер: принимает телеметрию (протокол OTLP и десятки legacy-форматов), обрабатывает (батчинг, фильтрация, обогащение, tail-семплирование) и экспортирует в бэкенды — один или несколько одновременно.
  • Protocol (OTLP) — единый протокол передачи, на котором сходятся все три сигнала.

Ценность этой схемы — нейтральность к бэкенду: инструментирование пишется один раз, а смена хранилища трейсов или метрик — это переконфигурация Collector’а, не переписывание сервисов. Телеметрия становится инфраструктурой, а не привязкой к вендору. Collector допускает два режима развертывания, которые сочетаются: agent — лёгкий экземпляр рядом с приложением (sidecar-контейнер или daemonset узла), принимающий телеметрию локально, — и gateway — центральный кластер, агрегирующий потоки от агентов перед экспортом; между сервисами и бэкендами никогда не строят прямых зависимостей — всё идёт через конвейер.

Инструментирование при этом бывает двух родов. Ручное — спаны и метрики создаются в коде явно: разработчик оборачивает доменно значимые операции («проверка антифрода», «резервирование товара») и добавляет бизнес-атрибуты; только так в телеметрии появляется предметная семантика. Автоматическое — SDK-агент или инфраструктура создают спаны за разработчика: входящие HTTP-запросы, вызовы БД и очередей, клиенты внешних сервисов инструментируются библиотекой-агентом без правок кода (в JVM-мире — javaagent, в .NET — OpenTelemetry profiler; прокси service mesh трассируют пересылки сами). Практическая норма — автоматическая база плюс ручная доменная надстройка: первая закрывает транспортную трассировку, второй добавляет то, что транспорт не знает.

Типовой конвейер телеметрии на OpenTelemetry:

 ┌─────────────┐   ┌─────────────┐   ┌─────────────┐
 │  Сервис A   │   │  Сервис B   │   │  Сервис C   │
 │ OTel SDK    │   │ OTel SDK    │   │ авто-инстр. │
 └──────┬──────┘   └──────┬──────┘   └──────┬──────┘
        │ OTLP             │ OTLP             │ OTLP
        └────────┬─────────┴──────────┬──────┘
                 ▼                    ▼
         ┌───────────────────────────────────┐
         │   OpenTelemetry Collector         │
         │ приём · батчинг · фильтрация ·    │
         │ обогащение · tail-семплирование   │
         └────┬─────────────┬────────────┬───┘
              ▼             ▼            ▼
        ┌──────────┐  ┌──────────┐  ┌──────────┐
        │ metrics  │  │   logs   │  │  traces  │
        │Prometheus│  │ Loki/ES  │  │Jaeger/   │
        │          │  │          │  │Tempo     │
        └──────────┘  └──────────┘  └──────────┘

Отдельного слова требует семплирование трейсов — компромисс между полнотой и стоимостью. Head-based семплирование решает в начале трейса (SDK): трассировать каждый сотый запрос; дёшево, но редкие ошибки почти наверняка выпадут из выборки. Tail-based решает в конце (Collector): сохранять все ошибочные и медленные трейсы, из успешных — каждый сотый; дороже в эксплуатации, но существенно полезнее при расследованиях. Практическая норма — хранить 100% ошибочных трейсов и 1–10% успешных.

Связь трейсинга с интеграционными паттернами двусторонняя: service mesh (Envoy, Linkerd) распространяет заголовки трассировки и генерирует спаны на каждой пересылке даже для неинструментированных сервисов, а шлюзы часто выступают точкой зарождения трейсы — первым спаном, которому доверяется trace_id.

Практики: RED, USE, Four Golden Signals

Три кита дают сырьё; методики наблюдаемости дают ответ, какие именно метрики собирать. Три канонические системы отвечают на этот вопрос с двух сторон: RED и Four Golden Signals — для сервисов, USE — для ресурсов.

RED — методика для сервисов и API: на каждый эндпоинт наблюдать три метрики. Название закрепил Том Уилки (Tom Wilkie, Weaveworks, 2015) в связке с Prometheus:

  • Rate — интенсивность запросов (запросов в секунду);
  • Errors — доля ошибочных ответов (5xx, тайм-ауты, бизнес-отказы);
  • Duration — распределение времени обработки (не среднее, а квантили: p50, p95, p99).

USE — методика для ресурсов: для каждого ресурса (CPU, память, диск, сеть, пул соединений) наблюдать:

  • Utilization — загрузка (какая доля времени/ёмкости ресурс занят);
  • Saturation — насыщенность (длина очереди к ресурсу, ожидание);
  • Errors — ошибки самого ресурса (сбросные пакеты, ошибки устройства).

Автор — Брендан Грегг (Brendan Gregg, Netflix/Intel), методика опубликована в 2012 году как систематический чек-лист: «для каждого ресурса проверь использование, насыщение и ошибки». Проход по типовым ресурсам серверного приложения выглядит так:

РесурсUtilizationSaturationErrors
CPUУтилизация ядерДлина очереди планировщика (load average)— (аппаратные сбои)
ПамятьЗанятая ёмкостьSwapping, major page faultsOOM-killer
Диск (I/O)Время занятости устройстваДлина очереди запросовОшибки ввода-вывода
СетьПропускная способностьОчереди, дропы пакетовРетрансмиты, сбросы соединений
Пул соединенийЗанятые слотыОчередь ожидания слотаТайм-ауты получения соединения

Four Golden Signals — синтез из книги Site Reliability Engineering (Google, 2016): четыре сигнала, достаточные для первичного наблюдения за любым сервисом:

  • Latency — задержка (с разделением успешных и ошибочных ответов: медленная ошибка — не то же, что медленный успех);
  • Traffic — нагрузка (запросы в секунду, объём данных);
  • Errors — ошибки (по протоколу и по доменным правилам);
  • Saturation — насыщенность (полнота ресурсов: очереди, утилизация, давление на память).
МетодикаОбъект наблюденияМетрикиПроисхождение
REDСервисы и APIRate, Errors, DurationТом Уилки, Weaveworks, 2015
USEРесурсы (CPU, диск, пулы)Utilization, Saturation, ErrorsБрендан Грегг, 2012
Four Golden SignalsСервис (минимальный набор)Latency, Traffic, Errors, SaturationGoogle SRE Book, 2016

Соотношение методик прагматично: Four Golden Signals — это «RED + насыщенность» на уровне всего сервиса (минимальный стандарт алертинга), USE — ортогональный проход по ресурсам, на которых сервис работает. Практическое правило: золотые сигналы на каждый сервис, USE на каждый ресурс — вместе они закрывают и «сервис тормозит», и «почему у него кончился пул соединений». На Prometheus RED-тройка для сервиса выражается тремя запросами:

# Rate: интенсивность запросов (в секунду, окно 5 минут)
rate(http_requests_total{service="orders"}[5m])

# Errors: доля ошибок 5xx среди всех ответов
sum(rate(http_requests_total{service="orders",code=~"5.."}[5m]))
  / sum(rate(http_requests_total{service="orders"}[5m]))

# Duration: p99 задержки обработки
histogram_quantile(0.99,
  sum(rate(http_request_duration_seconds_bucket{service="orders"}[5m])) by (le))

Три выражения — и алертная база сервиса готова: скорость падает к нулю — трафика нет; доля ошибок растёт — сервис отказывает; p99 ползёт вверх — деградация под нагрузкой.

Над метриками сервисов строится управление надёжностью: SLI/SLO и бюджет ошибок. SLI (service level indicator) — измеримый индикатор качества: доля успешных ответов, квантиль задержки. SLO (service level objective) — целевой уровень SLI: «99,9% запросов быстрее 300 мс за 30 дней». Бюджет ошибок (error budget) — допустимый объём нарушений: разница между 100% и SLO. Цена каждой «девятки» наглядна в пересчёте на месяц:

SLOБюджет ошибок (на 30 дней)
99%~7,2 часа простоя или 1% ошибок
99,9%~43 минуты
99,99%~4,3 минуты

Бюджет превращает надёжность из лозунга в ресурс: пока бюджет не истрачен, можно агрессивно поставлять новые функции; исчерпан — разработка замедляется в пользу стабильности. Измеримая надёжность опирается на наблюдаемость напрямую: без честных SLI нет и осмысленных SLO. Глубокий разбор этих механизмов — в статье «Отказоустойчивость и High Availability».

Структурное логирование и correlation IDs

Лог — старейший сигнал наблюдаемости, и его качество определяется форматом. Структурное логирование — запись каждого события как машиночитаемой записи с полями (обычно JSON) вместо свободного текста:

{"ts":"2026-09-30T12:04:11Z","level":"info","service":"orders","msg":"order_paid",
 "order_id":10427,"user_id":"u_771","amount":15990,"trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7"}

Преимущество перед plain text — запрашиваемость: событие с полями находится фильтром (order_id=10427 AND level=error), группируется, считается; строка «Order 10427 paid successfully» находится только полнотекстовым поиском и не объединяется ни с чем. Структурный формат делает логи пригодными к автоматической обработке — от алертов по шаблонам до аналитики. Практические следствия:

  • Уровни (TRACE, DEBUG, INFO, WARN, ERROR) — фильтр значимости: ERROR должен означать «требует вмешательства», WARN — «аномалия, но обработана»; система, пишущая всё в ERROR, приучает дежурного игнорировать журнал.
  • Семплирование — на высоких нагрузках часть отладочных событий отбрасывается; ошибки семплировать нельзя.
  • Поля вместо конкатенации — идентификаторы, суммы, коды — отдельными полями, не вшитыми в текст.

В коде структурное логирование выглядит как явная передача полей и сквозного контекста:

// событие с полями и трассировочным контекстом из текущего span'а
log.info('order_paid', {
  order_id: order.id,
  user_id: order.userId,
  amount: order.total,
  trace_id: ctx.traceId,   // сквозной идентификатор трейса
  span_id: ctx.spanId,
});

Второй столп логирования в распределённой системе — correlation ID (он же request ID): идентификатор, который присваивается запросу на входе в систему (шлюзом или первым сервисом) и сквозным образом передаётся через все сервисы и очереди, записываясь каждым участником обработки. Заголовок X-Request-ID, передача через исходящие вызовы и свойства сообщений очереди, запись в каждый лог — и событие «заказ 10427» собирается из журналов пяти сервисов одним фильтром:

 клиент ─────► шлюз ─────► orders ─────► payments ─────► БД
               │            │              │
               │  X-Request-ID: 7f3a9c... передаётся дальше
               │  и пишется в каждое лог-событие каждого сервиса
               │
            генерирует
          X-Request-ID

Без correlation ID журналы распределённой системы — куча несшиваемых осколков; с ним — хронология одного сценария. В асинхронных связях корреляция переносится в тело или заголовки сообщений брокера (подробнее — в готовящейся статье об очередях сообщений); в событийно-ориентированной архитектуре роль correlation ID часто играет само событие с его event_id и причинными ссылками.

Зрелое решение — не заводить отдельный request ID, а использовать trace_id трейсинга как единый correlation ID: записывать его в каждое лог-событие (как в примере выше). Тогда три кита связываются в одну систему координат: алерт по метрике открывает дашборд, трейс показывает путь, логи с тем же trace_id — события; переход от агрегата к событию — один клик, а не ручная сверка времени. Именно эта связность, а не количество собранных байтов, отличает наблюдаемую систему от «просто логирующей». Так выглядит расследование инцидента в связанной системе:

 алерт: p99 POST /checkout > 1 s                    (метрика)
   │   дашборд: деградация с 12:04, только prod
   ▼
 трейсы с duration > 1 s                            (трейс)
   │   152 мс из 234 — payments: charge,
   │   внутри — UPDATE ledger: 23 мс → 980 мс
   ▼
 логи с trace_id найденного трейса                  (логи)
   │   lock_timeout на таблице ledger
   ▼
 причина: блокировка от миграции, запущенной в 12:04

Ни один сигнал по отдельности этого пути не проходит: метрика не знает «где», трейс не знает «что», лог без trace_id не находится из первых двух шагов.

Контракт приложения в этой схеме зафиксирован Twelve-Factor App, фактором XI (Logs): приложение пишет события в стандартные потоки stdout/stderr и не управляет их дальнейшей судьбой — сбор, буферизация, доставка и хранение — задача платформы. Разделение ответственности повторяет архитектуру конвейера из раздела о трейсинге: сервис производит структурированные события, инфраструктура (агенты узлов, Fluent Bit, OTel Collector) транспортирует их в централизованное хранилище.

Хранилища логов различаются компромиссом «индекс против меток». Elasticsearch (стек ELK) строит полнотекстовый индекс по всем полям — максимальная гибкость поиска ценой дорогого индексирования и хранения. Loki (Grafana) индексирует только метки событий (сервис, уровень), а содержимое хранит сжатыми чанками и ищет по ним при запросе — на порядок дешевле, но поиск «по любому полю без метки» требует скана. Выбор отражает честный ответ на вопрос «какие вопросы будут задаваться к логам»: если метки известны заранее — Loki экономит бюджет; если вопросы произвольны — платят за индекс.

Плюсы и минусы

Плюсы

Скорость диагностики распределённых сбоев. Связанные сигналы сокращают расследование с часов ручной сверки журналов до минут: метрика локализует симптом, трейс — путь и вклад, лог — событие. Для систем, где микросервисы размазывают отказ по цепочкам вызовов, это не удобство, а способность вообще найти причину.

Готовность к неизвестным отказам. Мониторинг покрывает известные сценарии; наблюдаемая система допускает произвольные вопросы к прошлому — novel-сбои расследуются по уже собранным данным, без релиза с «добавленным логированием» и ожидания повторения.

Управляемая надёжность. SLI/SLO и бюджеты ошибок делают качество измеримым и обсуждаемым — между командой и бизнесом появляется общий язык уровня сервиса, а приоритет работы со стабильностью — не лозунг, а арифметика бюджета.

Слепые поставки становятся зрячими. Канареечные релизы и быстрые откаты — инструменты управления трафиком — опираются на телеметрию: решение «раскатывать дальше или откатывать» — это сравнение метрик новой и старой версий на ограниченном трафике. Без наблюдаемого сигнала поставка возможна только «вслепую», малыми шагами и с ручной проверкой.

Нейтральность к вендору. OpenTelemetry выносит инструментирование в стандарт: смена бэкендов телеметрии — переконфигурация конвейера, а не переписывание сервисов; телеметрия перестаёт быть lock-in’ом.

Выравнивание контекста между командами. Единый correlation ID и структурированные события создают общий язык диагностики: инженер соседней команды читает те же сигналы, что и автор сервиса.

Минусы

Стоимость хранения. Логи и трейсы — самые дорогие сигналы: полнотекстовые хранилища растут быстрее продукта, retention-политики и семплирование — не оптимизация, а необходимость. Метрики дёшевы ровно потому, что агрегированы и ограничены по кардинальности.

Кардинальность метрик. Каждая уникальная метка — новый временной ряд; необдуманная метка user_id или request_path с параметрами способна положить хранилище временных рядов. Работа с кардинальностью — постоянная дисциплина: высококардинальные вопросы уводятся в логи и трейсы, метрики остаются агрегатами.

Семплирование как компромисс. Трейсы и отладочные логи собираются выборочно — значит, конкретное событие могло не попасть в выборку. Head-семплирование жертвует редкими ошибками, tail-семплирование усложняет конвейер; идеала «хранить всё» не выдерживает экономика ни одной крупной системы.

Эксплуатационная цена конвейера. Сборщики, хранилища, алертинг и дашборды — сами по себе распределённые системы: их нужно версионировать, масштабировать, мониторить (ирония, но конвейер наблюдаемости должен быть наблюдаем). Малой команде конвейер полной сложности может стоить дороже пользы.

Безопасность данных телеметрии. Логи — классическое место утечек: токены, пароли, персональные данные, номера карт попадают в события и дальше — в хранилища с широким доступом и длинным retention. Требуются явные правила санитизации полей и разграничение доступа к телеметрии.

«Тонна данных ≠ наблюдаемость». Центральный минус-тезис: наблюдаемость — свойство дизайна системы, а не покупка стека. Система, испускающая несвязанные неструктурированные сигналы, не становится наблюдаемой ни от объёма данных, ни от количества дашбордов; обратно — скромный, но связанный набор сигналов может отвечать на вопросы лучше петабайта шума.

Связанные статьи