# OpenTelemetry: практическая лаборатория Node.js

10 опытов: внутренние spans, два HTTP-сервиса, последовательные и параллельные вызовы, ошибка, async context, worker_threads, родительский sampling, приватность и graceful shutdown. Исходники MIT. Bootstrap и OpenTelemetry имеют собственные лицензии в THIRD-PARTY-LICENSES.txt.

## Что выполняется на странице

Публичная мастерская — интерактивный просмотр **записи настоящего запуска Node.js**. Фильтры, дерево, шкала времени, детали и экспорт работают в браузере. Браузер не запускает Node SDK, Collector, Docker или Jaeger. Запись `records/recording.json` содержит реальные trace/span ID, атрибуты, события, временные метки SDK, HTTP-ответы и связанные обычные JSON-логи. Исходные JSONL сохранены в каталоге запуска, указанном в `records/latest-run.json`. `sourceHashes` связывает запись с кодом. Никакие spans не дорисованы.

В последовательном и параллельном опытах код **намеренно ожидает 45/80 мс**, имитируя внешний сервис. Полосы отражают фактически измеренное время, а не эти константы. Это опыт по структуре трассы, не бенчмарк серверов. Временные метки нескольких машин требуют синхронизации часов; длительности родителей и детей не суммируют.

## Версии

Проверено: Node.js 24.19.0, `@opentelemetry/api` 1.10.0, `sdk-node` 0.301.1, `sdk-trace` 3.1.1, OTLP/HTTP exporter 0.301.1; Bootstrap 5.3.8; Collector Contrib 0.165.0; Jaeger 2.23.0. Lock фиксирует точные зависимости. В SDK3 новый пакет `@opentelemetry/sdk-trace`, а `BatchSpanProcessor` получает объект `{exporter, ...options}`. Старые примеры с двумя аргументами конструктора здесь не подходят.

NodeSDK настроен вручную на трассы. Массивы `metricReaders` и `logRecordProcessors` пусты. JSON-логи приложения не выдаются за OTel Logs. Автоматическая инструментация намеренно не включена: HTTP SERVER/CLIENT spans, инъекция и извлечение контекста видны в коде. При добавлении auto-instrumentation избегайте дублирования этих spans и инициализируйте её до импорта инструментируемых модулей.

## Быстрый запуск Windows / PowerShell

Установите Node.js 24.19+ из официального дистрибутива. Распакуйте архив, откройте терминал в каталоге с package.json.

```powershell
npm ci --ignore-scripts
npm test
npm run capture
npm run build
npm run serve
```

Откройте `http://127.0.0.1:8403`. Последняя команда держит сервер мастерской открытым; дальнейшие команды выполняйте во втором терминале. Ctrl+C останавливает только этот сервер. Для исследования работающих Node-сервисов:

```powershell
npm start
# Во втором терминале:
Invoke-RestMethod 'http://127.0.0.1:8413/demo?scenario=parallel'
Invoke-RestMethod 'http://127.0.0.1:8413/demo?scenario=error'
```

Второй вызов намеренно возвращает HTTP500; PowerShell покажет ошибку HTTP. Ctrl+C в первом терминале завершает оба дочерних сервиса и отправляет накопленные spans в JSONL. `npm run capture` запускает свои процессы, выполняет весь набор и сам их останавливает. Он откажется стартовать при занятых 8413/8414, не завершая чужие процессы.

## Linux / Bash

Установите поддерживаемый Node.js 24.19+ способом, принятым на вашем сервере. Не используйте случайный старый системный Node без проверки версии.

```bash
node --version
npm ci --ignore-scripts
npm test
npm run capture
npm run build
npm run serve
# Другой терминал, если запущен npm start:
curl 'http://127.0.0.1:8413/demo?scenario=distributed'
```

Нужны свободные 8403 (статическая мастерская), 8413/8414 (Node). Native capture не требует Docker. Записи `records` локальные и не содержат пользовательских данных; права записи нужны процессу Node. После изменения записи выполните `npm run build`, иначе public продолжит показывать предыдущую.

## Collector + Jaeger: реальный OTLP/HTTP

Docker Engine/Desktop с Compose v2 должен быть запущен. Образы фиксированы версиями и SHA256 digest. При конфликте subnet задайте свободный `OTEL_DEMO_SUBNET` до первого `up`; стандарт — 10.254.243.0/24. Не удаляйте чужие сети и контейнеры.

```powershell
docker compose config --quiet
docker compose up -d collector jaeger
docker compose logs --tail 20 collector
$env:OTEL_EXPORTER_OTLP_TRACES_ENDPOINT='http://127.0.0.1:14318/v1/traces'
npm run capture
npm run verify:collector
npm run build
Remove-Item Env:OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
```

Linux отличается установкой переменной окружения:

```bash
docker compose up -d collector jaeger
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:14318/v1/traces npm run capture
npm run verify:collector
npm run build
```

Дождитесь готовности Collector, если первый запрос сделан сразу после запуска. `docker compose logs collector` покажет строку готовности или ошибку конфигурации. Jaeger: `http://127.0.0.1:16687`, сервис `workshop-checkout`, операция `GET /demo`, затем Find Traces. Рядом видны `workshop-pricing` и `workshop-cpu-worker`. Вставьте trace ID из JSONL в поиск Jaeger. Скрипт `verify:collector` запрашивает его API, проверяет каждый span ID и сохраняет необработанные ответы в `records/jaeger`.

Коллектор слушает 4318 внутри сети, опубликован на **127.0.0.1:14318** хоста. SDK endpoint содержит `/v1/traces`; Collector exporter endpoint `http://jaeger:4318` — базовый URL, путь добавляет экспортер. В текущем Collector используется имя `otlp_http`. Нельзя подставить UI-порт16686 вместо OTLP4318. Хост `localhost` внутри контейнера обозначает сам контейнер, поэтому используются DNS-имена `collector` и `pricing`.

## Полностью контейнерный вариант

```bash
docker compose --profile full up -d --build
curl 'http://127.0.0.1:8413/health'
curl 'http://127.0.0.1:8413/demo?scenario=distributed'
curl 'http://127.0.0.1:8413/demo?scenario=parallel'
docker compose --profile full logs --tail 30 checkout pricing
docker compose --profile full down
```

`depends_on` задаёт порядок запуска, а не гарантирует готовность приложения. Проверяйте /health и логи. `npm run capture` и full profile одновременно не запускайте: порты одинаковы. В контейнерном варианте JSONL находится в `/tmp/otel` каждого контейнера; перед `down` можно сохранить через `docker compose cp checkout:/tmp/otel ./records/container-checkout`. Для постоянного хранения настройте отдельный volume/каталог с владельцем UID1000. Jaeger здесь использует память, поэтому `down` уничтожает его историю. Это учебный single-node стенд, не конфигурация промышленного хранилища.

## Опыт за опытом

1. `basic`: SERVER → checkout.business → quote.local. Сравните parentSpanId.
2. `distributed`: CLIENT передаёт traceparent, Pricing извлекает удалённого родителя; в HTTP-ответе можно сравнить отправленный и полученный заголовок.
3. `sequential`: два обращения идут последовательно. Вторая CLIENT-операция начинается после завершения первой.
4. `parallel`: Promise.all создаёт две одновременно открытые ветви; время запроса не равно сумме всех полос.
5. `error`: контролируемая ошибка Pricing, HTTP500, span status ERROR, событие exception, лог request.failed с trace ID.
6. `async-context`: два одновременных запроса проверяют свой текущий span после await и setImmediate. Контексты не смешиваются.
7. `worker`: реальное вычисление checksum в worker_threads. Carrier передан явно через workerData; отдельный SDK в worker извлекает родителя и отправляет span перед результатом. На холодную загрузку модулей отведено до 15 секунд; после явного ready вычисление и flush имеют отдельный предел 5 секунд. Старт не съедает бюджет самой задачи, особенно на медленном bind mount.
8. `sampling`: два реальных входных traceparent, sampled1/0. ParentBased принимает решение родителя, поэтому неэкспортированная трасса не означает отсутствие запроса. Внешний span ID не выдумывается в дереве.
9. `privacy`: только plan/channel из списка допустимых значений. Вымышленные email/token исключены, health не трассируется.
10. `shutdown`: после последнего HTTP-запроса сервер перестаёт принимать новые, дожидается активных, затем sdk.shutdown отправляет буфер. Конечные span ID есть и в JSONL, и в Jaeger.

## Linux/VPS и служба systemd

Для изучения на VPS достаточно SSH-туннеля: `ssh -L 16687:127.0.0.1:16687 user@server`. Открывайте Jaeger через localhost своего компьютера. Не публикуйте учебный Collector и Jaeger UI без аутентификации в интернет. Production обычно использует отдельный collector/gateway, TLS, ограниченную сеть и долговременное хранилище с retention.

Шаблон `deploy/otel-workshop@.service` запускает отдельные checkout/pricing от пользователя `otel-demo`. Создайте его, положите проект в `/opt/otel-workshop`, создайте `/var/lib/otel-workshop/{checkout,pricing}` с правом записи, установите зависимости от административного пользователя и загрузите unit. Конкретный путь Node уточните `command -v node`. Файлы окружения образцы, секретов в них нет.

```bash
sudo useradd --system --home /opt/otel-workshop --shell /usr/sbin/nologin otel-demo
sudo install -d -o otel-demo -g otel-demo /var/lib/otel-workshop/checkout /var/lib/otel-workshop/pricing
sudo install -m 644 deploy/otel-workshop@.service /etc/systemd/system/
sudo install -d -m 755 /etc/otel-workshop
sudo install -m 640 -o root -g otel-demo deploy/checkout.env deploy/pricing.env /etc/otel-workshop/
sudo systemctl daemon-reload
sudo systemctl enable --now otel-workshop@pricing otel-workshop@checkout
journalctl -u otel-workshop@checkout -n 50 --no-pager
sudo systemctl stop otel-workshop@checkout otel-workshop@pricing
```

Перед запуском укажите рабочий Collector endpoint в env и подтвердите `node --version`. `TimeoutStopSec` больше серверных тайм-аутов и времени экспорта. SIGKILL и аварийное отключение питания не дают гарантий flush. Для постоянного приложения JSONL exporter обычно убирают или заменяют ротацией; иначе файл растёт. Этот unit — шаблон адаптации, systemd здесь не запускался на настоящем VPS. Linux runtime воспроизводится контейнерным образом, отдельная приёмка сервера остаётся задачей владельца.

## Ограничения и данные

Только фиксированные GET маршруты, 16 одновременных запросов, допустимые значения параметров, фиксированный локальный upstream, ограниченные CPU-итерации. HTTP-сервер не является авторизованным production API. Передаваемые входные traceparent считаются доверенными только для учебного опыта. На публичном входе политика sampling должна учитывать возможность клиента прислать sampled1; не позволяйте внешнему пользователю бесконтрольно увеличивать стоимость телеметрии.

Секреты не помещайте в spans, baggage, события, stack trace или URL. `attributes/remove_sensitive` в Collector удаляет известные поля как второй рубеж, но не очищает произвольный текст и не отменяет утечку в локальный JSONL. Основная защита — allowlist в приложении и ограниченные значения. Идентификатор заказа часто полезнее email; реальную политику хранения согласуют отдельно. Ошибки стенда содержат только фиксированные учебные сообщения.

Head sampling выбирает трассу в начале. При ratio0.1 нельзя гарантировать сохранение всех ошибок, случившихся позже. Tail sampling работает после накопления spans в Collector и требует памяти, времени ожидания и направления всех частей трассы одному принимающему узлу. В данном проекте tail sampling не реализован. Для демонстрации root ratio1, ParentBased сохраняет решение входного родителя.

## Проверки и файлы

`npm test`: настоящие два Node-сервиса на случайных свободных loopback-портах; HTTP propagation, корректная вложенность, overlap/sequence, ERROR/exception, async isolation, worker parent, sampling, отсутствие секретов/health, последний flush. Тесты не требуют Docker и не сравнивают абсолютную производительность. Тесты модели проверяют работу фильтров/дерева с сохранённой записью. DOM-проверки используют jsdom, не заменяя настоящий мобильный браузер.

`npm run verify:collector`: отдельная интеграционная проверка доставки фактических span ID в Jaeger. Без запущенного Collector/Jaeger этот шаг должен упасть. Запись не генерируется из заранее нарисованной схемы.

Структура: `src` — реальный Node-код и чистая модель дерева, `ui` — браузерный интерфейс, `public` — готовая статическая мастерская, `tools` — запуск/запись/проверка/сборка, `deploy` — Collector и systemd, `records` — сырые доказательства, `tests` — автоматические проверки. После правок выполняйте test → capture с OTLP → verify:collector → build. Файл public/app.js имеет расширение .js для совместимости с MIME хостинга.

## Официальные источники

- https://opentelemetry.io/docs/languages/js/getting-started/nodejs/
- https://opentelemetry.io/docs/languages/js/instrumentation/
- https://opentelemetry.io/docs/concepts/context-propagation/
- https://opentelemetry.io/docs/concepts/sampling/
- https://opentelemetry.io/docs/collector/configuration/
- https://github.com/open-telemetry/opentelemetry-js
- https://github.com/open-telemetry/opentelemetry-collector-releases/releases/tag/v0.165.0
- https://www.jaegertracing.io/docs/2.23/getting-started/
- https://nodejs.org/api/worker_threads.html
