OpenTelemetry Collector на VPS: трейсы и метрики в Jaeger через Docker Compose

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

22 октября 2026 г.·7 мин чтения·opentelemetry, jaeger, docker, monitoring

Пока приложение — один процесс, достаточно журнала. Когда запрос проходит через веб-приложение, очередь и базу, журнала перестаёт хватать: хочется увидеть путь конкретного запроса и сколько времени он провёл на каждом шаге. Это и даёт 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/.