OpenTelemetry Collector на VPS: трейсы и метрики в Jaeger через Docker Compose
Запуск OpenTelemetry Collector и Jaeger в Docker, проверка конфигурации, тестовые трассы telemetrygen, подключение Flask-приложения без правки кода, метрики сервера host_metrics и разбор типичных ошибок. Debian 12 и Ubuntu 24.04.

Пока приложение — один процесс, достаточно журнала. Когда запрос проходит через веб-приложение, очередь и базу, журнала перестаёт хватать: хочется увидеть путь конкретного запроса и сколько времени он провёл на каждом шаге. Это и даёт OpenTelemetry (OTel) — открытый стандарт создания, передачи и сбора телеметрии, не привязанный к поставщику. Сам OTel ничего не хранит и графиков не рисует; этим занимаются Jaeger, Prometheus или коммерческие сервисы.
Как это устроено
Три вида сигналов:
| Сигнал | Что описывает |
|---|---|
| Трасса (trace) | путь одного запроса; состоит из спанов — отдельных единиц работы с именем, временем начала и конца, атрибутами. По общему идентификатору трассы и ссылкам на родителя Jaeger рисует дерево вызовов |
| Метрика | измерение во времени: память, число запросов. Описывает систему в целом, подходит для графиков и алертов |
| Лог | запись о событии; при включённом SDK к ней сами добавляются идентификаторы трассы и спана |
Приложение отправляет данные по протоколу OTLP. Вариантов два: OTLP/gRPC на порту 4317 и OTLP/HTTP на 4318 (пути /v1/traces, /v1/metrics, /v1/logs). Перепутать порты очень легко — ошибки разобраны в конце.
Можно слать данные прямо в хранилище, но обычно рядом ставят Collector — посредника, который принимает телеметрию, обрабатывает и пересылает дальше. Приложение быстро отдаёт данные и забывает о них, а повторы, пачки, шифрование и фильтрацию берёт на себя Collector. Внутри он — набор конвейеров по одному на сигнал: приёмники (receivers) → процессоры (processors) → экспортёры (exporters). Главная ловушка новичков: компонент мало описать в своём разделе, его надо ещё перечислить в service, иначе он не включится. Берём сборку contrib — в ней 108 приёмников против 7 в базовой.
Jaeger в этой схеме — хранилище и веб-интерфейс для трасс. К OpenTelemetry он не относится, но принимает OTLP.
Что нужно
- Сервер на tanaka.host с Debian 12 или Ubuntu 24.04, от 2 ГБ памяти. Collector занимает 45–70 МБ, Jaeger 20–60 МБ, образы на диске около 750 МБ.
- Docker и Compose — установка Docker.
- Python 3 для демонстрационного приложения (в Debian 12 и Ubuntu 24.04 уже есть).
- Браузер на своём компьютере — Jaeger будем открывать через SSH-туннель.
1. Папка и файл Compose
mkdir -p /opt/otel
cd /opt/otel
Скопируйте блок целиком, вместе с cat и EOF:
cat > compose.yaml <<'EOF'
# Jaeger хранит и показывает трассы, Collector принимает телеметрию
services:
jaeger:
image: cr.jaegertracing.io/jaegertracing/jaeger:2.22.0
container_name: otel-jaeger
restart: unless-stopped
ports:
- "127.0.0.1:16686:16686" # веб-интерфейс Jaeger, только с самого сервера
collector:
image: otel/opentelemetry-collector-contrib:0.163.1
container_name: otel-collector
restart: unless-stopped
volumes:
- ./otel-config.yaml:/etc/otelcol-contrib/config.yaml:ro
ports:
- "127.0.0.1:4317:4317" # OTLP gRPC
- "127.0.0.1:4318:4318" # OTLP HTTP
depends_on:
- jaeger
EOF
Все порты опубликованы только на 127.0.0.1: приложения с этого же сервера до Collector дотянутся, а из интернета он недоступен. Версии образов закреплены, чтобы после перезапуска получить ту же версию.
Почему только 127.0.0.1. У Collector из статьи нет паролей — данные от него примет любой, кто подключится к порту. UFW порты Docker не закрывает (см. брандмауэр), поэтому единственная защита — не публиковать их наружу.
2. Конфигурация Collector
cat > otel-config.yaml <<'EOF'
receivers:
# Приём телеметрии от приложений по протоколу OTLP
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
# Собирает данные в пачки, так отправка эффективнее
batch:
exporters:
# Трассы уходят в Jaeger
otlp_grpc/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
# Всё, что принял Collector, печатается в его лог
debug:
verbosity: basic
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp_grpc/jaeger, debug]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [debug]
logs:
receivers: [otlp]
processors: [batch]
exporters: [debug]
EOF
receivers— приёмникotlpждёт данные на 4317 и 4318.0.0.0.0— все адреса внутри контейнера; снаружи доступ ограничен тем, как опубликованы порты в Compose.processors—batchскладывает данные в пачки.exporters— имяotlp_grpc/jaegerсостоит из типа и собственного названия после косой черты, так можно держать несколько экспортёров одного типа.jaeger:4317— имя сервиса из Compose.insecure: trueотключает TLS внутри сети Docker.debugпечатает принятое в лог,basic— по строке на пачку.service— конвейеры: трассы идут в Jaeger и в лог, метрики и логи пока только в лог.
Не убирайте
endpoint: 0.0.0.0. Без него Collector слушает127.0.0.1внутри контейнера, и опубликованные порты перестают работать.
В версии 0.163.1 экспортёр называется otlp_grpc, а приёмник метрик хоста — host_metrics. Старые имена otlp и hostmetrics из прежних инструкций ещё принимаются.
3. Проверьте конфигурацию и запустите
docker compose run --rm --no-deps collector validate --config=/etc/otelcol-contrib/config.yaml
echo $?
Если всё верно, команда молчит, а echo $? показывает 0. Запуск:
docker compose up -d
docker logs otel-collector 2>&1 | tail -n 1
Нужна строка Everything is ready. Begin running and processing data.
4. Отправьте тестовые трассы
Утилита telemetrygen из проекта OpenTelemetry генерирует трассы, метрики и логи:
docker run --rm --network host \
ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest \
traces --otlp-insecure --traces 3 2>&1 | grep -E "generated|export:"
--network host даёт контейнеру сеть сервера, чтобы он видел 127.0.0.1:4317. --otlp-insecure отключает TLS — без него утилита получит ошибку handshake. --traces 3 — три трассы, по одной в секунду.
Строка traces generated означает только, что данные созданы. Дошли ли они — покажет лог Collector:
docker logs otel-collector 2>&1 | grep Traces | tail -n 2
В каждой строке "spans": 2 — telemetrygen делает трассу из двух спанов. Тот же тест по HTTP:
docker run --rm --network host \
ghcr.io/open-telemetry/opentelemetry-collector-contrib/telemetrygen:latest \
traces --otlp-http --otlp-insecure --otlp-endpoint localhost:4318 --service telemetrygen-http --traces 2
--service задаёт имя сервиса в Jaeger.
5. Откройте Jaeger через SSH-туннель
На своём компьютере:
ssh -L 16686:127.0.0.1:16686 root@IP-адрес-сервера
Не закрывая окно, откройте http://127.0.0.1:16686, в списке Service выберите telemetrygen и нажмите Find Traces. Три трассы по два спана, длительность около 123 микросекунд. Клик по трассе открывает временную шкалу.
Jaeger хранит трассы в памяти. В этой конфигурации данные на диск не пишутся — после перезапуска контейнера они исчезают. Для знакомства и отладки этого хватает.
6. Подключите приложение на Python
Возьмём маленькое Flask-приложение и подключим OTel без правки логики — через автоматическое инструментирование.
apt install -y python3-venv
mkdir -p /opt/otel/app
cd /opt/otel/app
python3 -m venv venv
source venv/bin/activate
pip install flask opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
opentelemetry-bootstrap находит установленные библиотеки (Flask) и ставит для них пакеты инструментирования.
cat > app.py <<'EOF'
import logging
import time
from random import randint
from flask import Flask, request
from opentelemetry import trace
app = Flask(__name__)
logger = logging.getLogger(__name__)
tracer = trace.get_tracer("diceroller.tracer")
@app.route("/rolldice")
def roll_dice():
player = request.args.get("player", default=None, type=str)
result = str(roll())
if player:
logger.warning("%s is rolling the dice: %s", player, result)
else:
logger.warning("Anonymous player is rolling the dice: %s", result)
return result
def roll():
# Свой участок трассы внутри запроса
with tracer.start_as_current_span("roll") as span:
time.sleep(0.3) # имитация работы
value = randint(1, 6)
span.set_attribute("roll.value", value)
return value
EOF
Единственное обращение к OTel в коде — спан roll, чтобы увидеть вложенный участок. Всё остальное сделает автоинструментирование:
opentelemetry-instrument --service_name dice-server --logs_exporter otlp flask run -p 8000
--logs_exporter otlp включает отправку логов; трассы и метрики уходят на localhost:4317 по умолчанию. Окно останется почти пустым — записи журнала теперь собирает OTel, а не терминал.
Во втором окне SSH:
for p in Anna Boris Clara; do curl "http://127.0.0.1:8000/rolldice?player=$p"; echo; done
docker logs otel-collector 2>&1 | grep -E "Traces|Logs" | tail -n 2
Три запроса дали шесть спанов и записи журнала. В Jaeger обновите страницу, выберите dice-server, откройте трассу GET /rolldice: верхняя полоса — весь запрос от инструментирования Flask, под ней спан roll почти на всё время запроса (там пауза), в атрибутах — roll.value. Остановите приложение по Ctrl+C.
Переключить приложение на HTTP можно переменной OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf перед командой запуска; другой адрес Collector задаёт OTEL_EXPORTER_OTLP_ENDPOINT.
7. Добавьте метрики сервера
Приёмник host_metrics снимает процессор, память, нагрузку, диски и сеть.
cd /opt/otel
nano otel-config.yaml
В раздел receivers сразу после otlp, с тем же отступом в два пробела:
# Метрики самого сервера
host_metrics:
root_path: /hostfs
collection_interval: 10s
scrapers:
cpu:
memory:
load:
filesystem:
network:
root_path нужен, потому что Collector в контейнере: корень сервера подключим в /hostfs. collection_interval по умолчанию минута, 10 секунд — чтобы быстрее увидеть результат. Добавьте приёмник в конвейер метрик:
metrics:
receivers: [otlp, host_metrics]
И замените verbosity: basic на verbosity: detailed, чтобы в логе были значения, а не только счётчики.
nano compose.yaml
В volumes сервиса collector:
volumes:
- ./otel-config.yaml:/etc/otelcol-contrib/config.yaml:ro
- /:/hostfs:ro # файловая система сервера для метрик хоста
docker compose run --rm --no-deps collector validate --config=/etc/otelcol-contrib/config.yaml
docker compose up -d
Через 20 секунд:
docker logs otel-collector --since 12s 2>&1 | grep "Name: system" | sort -u
Сверим занятую память с free:
docker logs otel-collector --since 12s 2>&1 | grep -A12 system.memory.usage | grep -E "state|Value" | head -2
free -b | head -2
Collector выдаёт 13 метрик system.*: system.cpu.load_average.1m, system.memory.usage, system.filesystem.usage, system.network.io и другие.
Верните
basic. Подробный режим пишет в лог около 90 КБ в минуту — порядка 130 МБ в сутки. Насмотрелись — вернитеverbosity: basicи выполнитеdocker compose restart collector.
Чтобы хранить метрики и рисовать графики, их нужно отправить в хранилище — экспортёр prometheus в Collector есть, но это тема отдельной статьи. Для простой проверки доступности сервисов хватит Uptime Kuma.
Полезные команды
docker compose ps # состояние контейнеров и порты
docker compose logs -f collector # лог Collector в реальном времени
docker compose restart collector # перечитать файл настроек
docker compose run --rm --no-deps collector validate --config=/etc/otelcol-contrib/config.yaml # проверить настройки
docker compose down # остановить и удалить контейнеры
Частые ошибки
Нужно ли открывать 4317/4318/16686 в UFW? Нет. Всё на 127.0.0.1, Jaeger — через туннель. Не меняйте 127.0.0.1:4317:4317 на 4317:4317: Collector без паролей окажется открыт всему интернету.
Failed to auto initialize OpenTelemetry / Requested component 'otlp_proto_grpc' not found. Не установлен экспортёр: pip install opentelemetry-exporter-otlp. Приложение при этом продолжает отвечать, так что ошибку легко не заметить.
В Jaeger пусто, а ошибок не видно. Ошибки экспорта уходят в OTel вместе с логами. Запустите с --logs_exporter console — всё появится на экране:
opentelemetry-instrument --service_name dice-server --logs_exporter console flask run -p 8000
Trying to connect an http1.x server (HTTP status 400). gRPC-клиент стучится на HTTP-порт 4318. Либо верните 4317, либо OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf. У telemetrygen то же выглядит как http2: frame too large.
BadStatusLine … malformed HTTP response. Обратное: HTTP-клиент на gRPC-порту 4317. Запомните: 4317 — только gRPC, 4318 — только HTTP, и для HTTP у telemetrygen нужен --otlp-http.
connect: connection refused. Collector не запущен или адрес неверный. docker compose ps и:
ss -tln | grep -E ":(4317|4318|16686) "
Должны быть три строки с 127.0.0.1.
tls: first record does not look like a TLS handshake. telemetrygen без --otlp-insecure.
error reading server preface: connection reset by peer. В приёмнике otlp нет endpoint, и Collector слушает 127.0.0.1 внутри контейнера. Верните 0.0.0.0:4317 и 0.0.0.0:4318.
references exporter "jaeger" which is not configured. Имя в service не совпадает с именем в exporters, включая часть после косой черты. Так же Collector ругается на неописанные приёмники и процессоры.
invalid root_path: stat /hostfs: no such file or directory. В Compose не подключён - /:/hostfs:ro.
Документация: https://opentelemetry.io/docs/collector/ и https://www.jaegertracing.io/docs/.