Наблюдаемость сервиса заявок
Сервис заявок «Пульс» принимает запрос на отчёт, записывает задание в PostgreSQL и отдаёт его фоновому worker. Пользователь получил 202, но отчёт ещё не готов. Если очередь задерживается, одного ответа HTTP недостаточно для объяснения проблемы. Начнём с модели наблюдаемости: какие факты нужны, чтобы связать запрос API, SQL-операцию и отдельный процесс выполнения.
В серии мы будем добавлять телеметрию к одному небольшому приложению. OpenTelemetry формирует и передаёт сигналы, Collector обрабатывает их, Tempo, Loki и Prometheus хранят соответствующие данные, а Grafana помогает искать и сопоставлять результаты. OpenTelemetry не является базой хранения или готовым дашбордом. Эти обязанности разделены, поэтому ошибка на одном этапе не обязательно означает отказ всех остальных.
Одна операция, три сигнала
Лог — событие с временем и полями, например report.accepted. Он сообщает, что код дошёл до конкретной точки. Метрика — числовое измерение, объединяемое за время: количество принятых запросов или возраст очереди. Trace — связанная последовательность интервалов выполнения, которая показывает путь отдельной операции через компоненты. У каждого сигнала есть собственный вопрос, и ни один не содержит весь ответ автоматически.
Если число запросов выросло, метрика покажет масштаб изменения. Если отдельный SQL-запрос задержался, span внутри trace покажет, где потрачено время. Если worker отказал по известной причине, лог объяснит переход состояния. При этом сообщение «задача начата» не измеряет её завершение, а низкое число ошибок HTTP не доказывает своевременную обработку очереди. API может быстро принимать запросы, пока задания накапливаются.
Наш пример намеренно не содержит пользовательского текста отчёта. Endpoint создаёт служебное задание с UUID, а worker выполняет учебную операцию и отмечает завершение. Это позволяет изучить корреляцию, не копируя в наблюдение содержимое документов или персональные сведения. Даже в таком простом проекте Authorization, тела запросов, query и параметры SQL не должны попадать в сигналы.
Данные «Пульса»
Начальная схема содержит очередь pulse_jobs. После INSERT задача имеет queued; worker получает processing, затем фиксирует done или возвращает задачу на повтор. Поля lease предусмотрены сразу, чтобы фоновая операция не зависела от памяти одного процесса. Поле carrier пока пустое: в четвёртом уроке туда попадёт ограниченный W3C trace context, а не произвольные заголовки клиента.
Полный файл schema.sql состояния 01:
CREATE TABLE pulse_jobs (
id uuid PRIMARY KEY, state text NOT NULL DEFAULT 'queued',
carrier jsonb NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now(),
available_at timestamptz NOT NULL DEFAULT now(),
lease_until timestamptz, lease_token uuid,
attempts integer NOT NULL DEFAULT 0, completed_at timestamptz
);
CREATE INDEX pulse_queue ON pulse_jobs(available_at) WHERE state='queued';
UUID нужен для связи ответа API с заданием, но мы не превращаем его в метку метрики. Каждый новый UUID создавал бы новую временную серию, и стоимость наблюдения росла бы вместе с количеством операций. Для конкретного запроса будет trace ID, а для общих счётчиков — ограниченные значения маршрута, класса статуса и сервиса. Различие между идентификатором случая и группой измерений заложим до добавления SDK.
PostgreSQL в лаборатории использует отдельную базу и пустой volume. Схема из schema.sql является начальным описанием, а не миграцией существующего приложения. В скачиваемом состоянии есть Compose для базы, но контейнеры при подготовке материала не запускались. Мы не знаем наблюдённой задержки INSERT или обработки и не подставляем вымышленные миллисекунды в качестве фактического результата.
Полный файл compose.yaml состояния 01:
services:
postgres:
image: postgres:17.4
environment:
POSTGRES_DB: pulse
POSTGRES_USER: pulse
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
ports: ["127.0.0.1:5433:5432"]
volumes: ["pg:/var/lib/postgresql/data", "./schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro"]
volumes:
pg: {}
API и worker запускаются как отдельные процессы Node.js на хосте лаборатории после самостоятельной установки среды. PostgreSQL доступен через loopback-порт 5433. Такое разделение позволяет позднее присвоить процессам разные service.name. Если оба называют себя одинаково, Grafana покажет смешанные данные и будет труднее понять, какой процесс создаёт задержку. Продуктовое имя «Пульс» и имя отдельного сервиса наблюдения решают разные задачи.
Время пользователя и время компонентов
POST возвращает 202 после фиксации задания. Время HTTP включает приём запроса и INSERT, но не включает ожидание очереди и работу worker после ответа. Поэтому хороший HTTP p95 не является гарантией быстрого готового отчёта. Для пользователя важна задержка от создания задания до завершения, а для диагностики нужны её части: время записи, ожидание, вычисление и финальный UPDATE.
В worker есть искусственная учебная пауза в сто миллисекунд. Она обозначает место будущей прикладной операции и не является измерением реального сервиса. Такой код помогает увидеть вложенный span, но не даёт основания оценивать производительность отчётов или capacity. Длительность работы в собственной лаборатории зависит от среды, а промышленный вычислитель потребует собственного контракта ресурсов и ошибок.
Первоначальный сервис не экспортирует данные в постоянный backend. События могут появляться в stdout, но вывод терминала не заменяет сохранённый журнал с запросами. В третьем уроке добавим spans, в пятом метрики, в седьмом Collector и в восьмом хранилища. Эта последовательность позволяет понять каждый разрыв: сигнал может быть создан кодом, но потерян до отображения на панели.
Что считать успешным наблюдением
Нужно проверять два независимых пути. Первый — пользовательская операция: запрос принят, задание выполнено. Второй — путь телеметрии: данные созданы, экспортированы, приняты Collector, сохранены backend и найдены запросом Grafana. Успех одного пути не доказывает другой. Например, отчёты могут работать при отказавшем Collector, а Collector может принимать health-сигналы от API, который уже не пишет задания.
Сигналы должны помогать принять конкретное действие. Панель количества запросов отвечает, изменилась ли нагрузка. Возраст очереди отвечает, насколько отстаёт обработка. Trace показывает компонент задержки. Лог сообщает безопасный код отказа. Если поле не помогает такой операции и раскрывает данные пользователя, его не следует собирать «на всякий случай». Объём наблюдения сам по себе не равен полезности.
Для учебной среды используем закреплённые версии, а прямые зависимости записаны в каждом снимке. Lockfile не разрешён, зависимости не устанавливались и примеры не исполнялись. Поэтому ожидаемые события и запросы дальнейших глав служат проверяемым проектом будущей лаборатории. Они не представлены как результаты уже работающего стенда или как свидетельство реального инцидента.
После этой главы вы можете объяснить путь запроса «Пульса» и выбрать сигнал для каждого вопроса. Следующий урок начнёт с самого простого полезного изменения: структурированного события и контекста запроса. Затем тот же контекст станет частью traces и будет перенесён через PostgreSQL к worker, сохраняя один узнаваемый пример на протяжении всей серии.
Следующий урок: Логи и контекст запроса
Оглавление серии · Исходники всех состояний.
Документация: OpenTelemetry: сигналы, PostgreSQL: SELECT.