> Полный список статей есть в [llms.txt](https://docs.f3cloud.com/llms.txt). Markdown-версия есть у каждой страницы, достаточно добавить .md к её адресу. Эта страница в Markdown: [https://docs.f3cloud.com/ru/docker/opentelemetry-collector.md](https://docs.f3cloud.com/ru/docker/opentelemetry-collector.md).

# OpenTelemetry на VPS – сбор трейсов и метрик через OpenTelemetry Collector

Установка OpenTelemetry Collector на VPS через Docker Compose. Что такое трассы, метрики и логи, протокол OTLP, приёмники, процессоры и экспортёры, отправка тестовых данных утилитой telemetrygen, подключение приложения на Python без правки кода, просмотр трассы в Jaeger через SSH-туннель и метрики сервера через приёмник host_metrics. Проверено на Debian 12 с OpenTelemetry Collector 0.163.1.

**OpenTelemetry** помогает увидеть, что происходит внутри приложения и на сервере. Программа отправляет в одно место трассы запросов, метрики и записи журнала, а оттуда их можно передать в любое хранилище и рассмотреть на временных шкалах и графиках. Главный посредник в этой схеме называется **OpenTelemetry Collector**, и именно его мы поднимем на VPS. В этой статье мы запустим Collector и Jaeger через Docker Compose, отправим тестовые данные, подключим приложение на Python без правки его кода, откроем трассу в браузере и добавим метрики самого сервера. Все шаги мы повторили на тестовом сервере с Debian 12, версия Collector на момент проверки **0.163.1**, версия Jaeger **2.22.0**.

***

## Как устроен OpenTelemetry

Пока приложение состоит из одного процесса, разобраться в его поведении обычно помогает журнал. Но когда запрос проходит через несколько сервисов, например через веб-приложение, очередь и базу данных, журнала перестаёт хватать. Хочется увидеть путь конкретного запроса и узнать, сколько времени он провёл на каждом шаге, а заодно понять, не упирается ли сам сервер в память или диск. Данные, которые программы и серверы выдают о собственной работе, называют **телеметрией**. OpenTelemetry, или сокращённо OTel, – это открытый набор спецификаций, программных интерфейсов и инструментов, который описывает, как телеметрию создавать, передавать и собирать. Он не привязан к конкретному поставщику, поэтому одни и те же данные можно отправить и в Jaeger, и в Prometheus, и в коммерческий сервис. При этом стоит сразу увидеть границу. По определению из официальной документации, OpenTelemetry не является системой наблюдаемости, то есть сам он данные не хранит и графики не рисует, а хранение и показ оставляет другим программам.

Телеметрия делится на **сигналы** трёх основных видов. **Трасса** (по-английски trace, в разговоре её часто называют трейсом) описывает путь одного запроса через приложение. Она состоит из **спанов**, и каждый спан представляет отдельную единицу работы внутри запроса, например обработку HTTP-запроса или обращение к базе данных. У спана есть имя, время начала и конца, атрибуты и идентификаторы. Все спаны одной трассы несут общий идентификатор трассы, а у вложенного спана указан идентификатор родителя, и именно по этим связям Jaeger потом рисует дерево вызовов, которое мы увидим в шаге 5. **Метрика** – это измерение, снятое во время работы, например занятая память или число запросов. В отличие от трассы она описывает поведение системы в целом, а не одного запроса, и поэтому хорошо подходит для графиков и оповещений. **Лог** – это запись о событии. Чтобы не заставлять отказываться от привычных библиотек журналирования, OpenTelemetry подключается к ним, и когда в приложении включён SDK или автоматическое инструментирование, к записям журнала сами добавляются идентификаторы активной трассы и спана. Благодаря этому строку журнала можно связать с запросом, в ходе которого она появилась.

Теперь посмотрим, как данные покидают приложение. В самом коде используется **API**, который задаёт, как создавать телеметрию, а за его реализацию на конкретном языке отвечает **SDK**. SDK собирает созданные спаны и метрики, обрабатывает их и экспортирует. Писать трассировку вручную для каждого фреймворка не приходится, потому что существуют библиотеки инструментирования, которые сами создают спаны для запросов популярных библиотек, а в языках, где это поддерживается, их подключают вообще без правки кода. Именно так мы поступим с приложением на Flask. Данные SDK отправляет по протоколу **OTLP**, и у него два варианта передачи. **OTLP/gRPC** по умолчанию использует порт 4317, а **OTLP/HTTP** порт 4318, причём по HTTP данные уходят на адреса `/v1/traces`, `/v1/metrics` и `/v1/logs`. Это два разных протокола на двух разных портах, и перепутать их очень легко. В разделе с частыми вопросами мы воспроизвели, какие ошибки при этом появляются.

Приложение можно настроить так, чтобы оно отправляло данные прямо в хранилище, и для знакомства с OpenTelemetry или небольшого стенда это допустимо. Однако в большинстве случаев рядом с сервисами запускают **Collector**, отдельную программу-посредник, которая принимает телеметрию, обрабатывает её и пересылает дальше. Приложению так проще, ведь оно быстро отдаёт данные и забывает о них, а повторные попытки отправки, группировку в пачки, шифрование и фильтрацию чувствительных данных берёт на себя Collector. Внутри он устроен как набор конвейеров, по одному для каждого сигнала. Конвейер начинается с **приёмников** (receivers), которые получают данные, например по OTLP или из метрик сервера. Затем идут **процессоры** (processors), которые меняют данные по пути, например складывают их в пачки. Заканчивается конвейер **экспортёрами** (exporters), которые отправляют результат в одно или несколько хранилищ. Есть тонкость, о которую спотыкаются новички. Компонент недостаточно описать в своём разделе конфигурации, его нужно ещё перечислить в разделе `service`, иначе он не будет включён. Мы увидим это в конфигурации шага 2. Collector выпускают в нескольких сборках, и мы возьмём **contrib**, в которой приёмников намного больше, чем в базовой. В образах версии 0.163.1 приёмников 108 против 7.

> **Роль Jaeger в схеме.**
>
> Jaeger – это открытая система для хранения и просмотра трасс. В нашей схеме он играет роль хранилища и веб-интерфейса, а к самому OpenTelemetry не относится. Jaeger 2.22.0 принимает данные по OTLP, в его логе видны строки Starting GRPC server и Starting HTTP server, поэтому Collector будет передавать ему трассы тем же протоколом.

***

## Что понадобится

* **Сервер** с Debian 12 и **не меньше 2 ГБ памяти**, например тариф **DE-RZ9-1**. На нашем сервере Collector занял от 45 до 70 МБ памяти, Jaeger от 20 до 60 МБ, а демонстрационное приложение на Flask около 120 МБ. Образы Docker заняли на диске около 750 МБ, виртуальное окружение Python ещё 67 МБ. Если сервера ещё нет, закажите его по [инструкции](https://docs.f3cloud.com/ru/client-area/order-vps.md).
* **Подключение по SSH**, например через [Termius](https://docs.f3cloud.com/ru/vps/termius-connection.md). Команды выполняются от имени `root`.
* **Docker и Docker Compose**. Как их поставить, рассказано в [отдельной статье](https://docs.f3cloud.com/ru/docker/docker-installation.md). У нас стояли Docker 29.9.2 и Compose 5.6.1.
* **Python 3** для демонстрационного приложения. В Debian 12 он уже есть, наша версия 3.11.2.
* **Терминал и браузер на вашем компьютере**, чтобы открыть Jaeger через SSH-туннель.

***

## Шаг 1. Подготовьте папку и файл Compose

Создайте папку проекта и перейдите в неё.

```bash
mkdir -p /opt/otel
cd /opt/otel
```

Теперь создайте файл `compose.yaml`. Скопируйте блок ниже целиком, вместе со строкой `cat` в начале и `EOF` в конце, и вставьте в окно терминала.

```bash
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
```

В файле описаны два контейнера.

* **jaeger** хранит трассы и показывает их в веб-интерфейсе. Его порт 16686 опубликован только на внутреннем адресе сервера `127.0.0.1`.
* **collector** принимает телеметрию. Его порты 4317 (OTLP/gRPC) и 4318 (OTLP/HTTP) тоже опубликованы только на `127.0.0.1`, поэтому приложения на самом сервере до него дотянутся, а из интернета он недоступен.

Файл настроек Collector подключается внутрь контейнера только на чтение, мы создадим его на следующем шаге. Версии образов указаны явно, чтобы после перезапуска вы получили ту же версию, а не новую.

> **Почему порты только на 127.0.0.1.**
>
> У Collector из этой статьи нет паролей, и данные от него принимает любой, кто смог подключиться к порту. Официальная документация OpenTelemetry советует публиковать порт Collector в Docker только на `127.0.0.1`, если им пользуются программы с того же сервера. Кроме того, брандмауэр UFW порты Docker сам не закрывает, об этом подробно рассказано в [статье про Docker](https://docs.f3cloud.com/ru/docker/docker-installation.md). Мы проверили с домашнего компьютера, что соединения с портами 4317, 4318 и 16686 на внешнем адресе сервера не устанавливаются и завершаются по тайм-ауту.

***

## Шаг 2. Напишите конфигурацию Collector

Создайте файл `otel-config.yaml` тем же способом, скопировав блок целиком.

```bash
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 (gRPC) и 4318 (HTTP). Адрес `0.0.0.0` означает, что приём идёт на всех адресах внутри контейнера, а доступ снаружи ограничен тем, как мы опубликовали порты в `compose.yaml`.
* **processors** описывает процессоры. Процессор `batch` складывает данные в пачки перед отправкой.
* **exporters** описывает экспортёры. Имя `otlp_grpc/jaeger` состоит из типа и собственного названия после косой черты, так можно держать несколько экспортёров одного типа. Адрес `jaeger:4317` это имя сервиса из `compose.yaml`, Docker сам находит контейнер по такому имени. Строка `insecure: true` отключает шифрование, потому что обмен идёт внутри сети Docker на одном сервере. Экспортёр `debug` печатает всё полученное в лог Collector, режим `basic` выводит по одной строке на пачку данных.
* **service** собирает конвейеры. Для каждого сигнала указано, откуда брать данные, как их обработать и куда отправить. Трассы идут и в Jaeger, и в лог, а метрики и логи пока только в лог.

> **Не убирайте адрес 0.0.0.0.**
>
> Если у приёмника не указать `endpoint`, Collector слушает только внутренний адрес контейнера `127.0.0.1`, и опубликованные порты перестают работать. Мы проверили это на тестовом сервере, подробности в разделе с частыми вопросами.

> **Названия компонентов в старых инструкциях.**
>
> В версии 0.163.1 экспортёр OTLP по gRPC называется `otlp_grpc`, а приёмник метрик сервера `host_metrics`. В старых инструкциях встречаются имена `otlp` и `hostmetrics`. Проверка конфигурации принимает и их, но мы используем новые названия.

***

## Шаг 3. Проверьте конфигурацию и запустите

Прежде чем запускать Collector, проверьте файл настроек. Команда ниже запускает одноразовый контейнер Collector с вашим файлом и просто сообщает, верна ли конфигурация.

```bash
docker compose run --rm --no-deps collector validate --config=/etc/otelcol-contrib/config.yaml
echo $?
```

Если всё в порядке, команда ничего не напечатает, а `echo $?` покажет `0`. При ошибке в файле вы увидите её текст, примеры есть в разделе с частыми вопросами. Теперь запустите контейнеры.

```bash
docker compose up -d
```

При первом запуске Docker сначала скачает образы. Через несколько секунд посмотрите последнюю строку лога Collector.

```bash
docker logs otel-collector 2>&1 | tail -n 1
```

Нужна строка **Everything is ready. Begin running and processing data.**, она означает, что приёмники запущены.

<figure><img src="https://docs.f3cloud.com/img/otel-compose-up.png" alt="Проверка конфигурации Collector, запуск docker compose up и строка Everything is ready в логе"><figcaption><p>Конфигурация верна, Collector запущен и готов принимать данные</p></figcaption></figure>

Строки лога Collector длинные, потому что в конце каждой стоит служебный блок с описанием самого Collector. Для нас важно начало строки и числа в конце.

***

## Шаг 4. Отправьте тестовые трассы

Чтобы проверить Collector до подключения настоящего приложения, возьмём утилиту **telemetrygen**. Она входит в проект OpenTelemetry и умеет генерировать тестовые трассы, метрики и логи. Запустим её в контейнере в сети сервера и отправим три трассы.

```bash
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` отключает шифрование. У нашего Collector его нет, и без этого параметра утилита попробует подключиться по TLS и получит ошибку.
* `--traces 3` задаёт число трасс. Утилита создаёт по одной трассе в секунду, поэтому три трассы уходят около 5 секунд.
* Фильтр `grep` оставляет только итоговую строку и строки с ошибками отправки. Утилита очень разговорчива.

Строка **traces generated** означает лишь то, что утилита создала данные. Дошли ли они до Collector, покажет его лог.

```bash
docker logs otel-collector 2>&1 | grep Traces | tail -n 2
```

<figure><img src="https://docs.f3cloud.com/img/otel-telemetrygen.png" alt="telemetrygen отправил три трассы, в логе Collector строки Traces по два спана"><figcaption><p>Collector принял трассы и записал их в лог</p></figcaption></figure>

В каждой строке **Traces** указано `"spans": 2`. Утилита создаёт трассу из двух спанов, а Collector получает трассы по мере появления и печатает по строке на каждую пачку.

Тот же тест можно выполнить и по HTTP. Для этого добавьте параметр `--otlp-http` и укажите порт 4318.

```bash
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-туннель

Веб-интерфейс Jaeger закрыт от интернета, поэтому подключаться к нему нужно через SSH-туннель.

1. На своём компьютере откройте новое окно терминала и создайте туннель. Замените `IP-адрес-сервера` на адрес вашего VPS.

   ```bash
   ssh -L 16686:127.0.0.1:16686 root@IP-адрес-сервера
   ```

2. Не закрывая это окно, откройте в браузере на своём компьютере адрес `http://127.0.0.1:16686`.
3. В списке **Service** выберите `telemetrygen` и нажмите <kbd>Find Traces</kbd>.

Откроется страница с найденными трассами. Мы отправили три трассы, и все три видны в таблице. У каждой по два спана, а длительность около 123 микросекунд, потому что telemetrygen по умолчанию создаёт очень короткие спаны.

<figure><img src="https://docs.f3cloud.com/img/otel-jaeger-telemetrygen.png" alt="Jaeger показывает три трассы сервиса telemetrygen по два спана каждая"><figcaption><p>Тестовые трассы из telemetrygen в Jaeger</p></figcaption></figure>

Нажмите на название любой трассы, и откроется её временная шкала со спанами внутри.

> **Jaeger хранит трассы только в памяти.**
>
> В режиме из нашего файла Compose Jaeger не пишет данные на диск. Согласно его документации, это временное хранилище в памяти. Мы проверили это, перезапустив контейнер, после чего прежние трассы исчезли. Постоянное хранение трасс в этой статье не настраивается, для знакомства и отладки хватает памяти.

***

## Шаг 6. Подключите приложение на Python

Тестовые данные хороши для проверки, но настоящая польза начинается, когда телеметрию присылает ваше приложение. Мы возьмём небольшое веб-приложение на Flask и подключим к нему OpenTelemetry без изменений в логике. За основу взят пример из официального руководства OpenTelemetry для Python, к которому мы добавили собственный спан и небольшую паузу, чтобы трасса получилась наглядной.

1. Установите пакет для виртуальных окружений Python. В Debian 12 он не входит в стандартную поставку.

   ```bash
   apt install -y python3-venv
   ```

2. Создайте папку приложения и виртуальное окружение, а затем активируйте его.

   ```bash
   mkdir -p /opt/otel/app
   cd /opt/otel/app
   python3 -m venv venv
   source venv/bin/activate
   ```

   В начале строки терминала появится пометка `(venv)`.

3. Установите Flask, OpenTelemetry и экспортёр OTLP. Затем запустите `opentelemetry-bootstrap`, он находит в окружении уже установленные библиотеки, например Flask, и ставит для них пакеты инструментирования.

   ```bash
   pip install flask opentelemetry-distro opentelemetry-exporter-otlp
   opentelemetry-bootstrap -a install
   ```

   На нашем сервере установились Flask 3.1.3, OpenTelemetry SDK 1.45.1 и opentelemetry-distro 0.66b1.

4. Создайте файл приложения `app.py`.

   ```bash
   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
   ```

   Приложение отвечает на адрес `/rolldice` случайным числом от 1 до 6. Единственное обращение к OpenTelemetry в нём это спан `roll`, который мы создали сами, чтобы увидеть вложенный участок. Всё остальное сделает автоматическое инструментирование.

5. Запустите приложение через `opentelemetry-instrument`.

   ```bash
   opentelemetry-instrument --service_name dice-server --logs_exporter otlp flask run -p 8000
   ```

   * `--service_name dice-server` задаёт имя сервиса в Jaeger.
   * `--logs_exporter otlp` включает отправку логов. Трассы и метрики по документации уходят по OTLP/gRPC на `localhost:4317` и без этого параметра.
   * `flask run -p 8000` обычная команда запуска приложения. Если порт 8000 у вас занят, выберите другой.

   Окно останется почти пустым, и это нормально. Записи журнала приложения теперь собирает OpenTelemetry и отправляет в Collector, а в терминал они не выводятся.

6. Откройте второе окно SSH и обратитесь к приложению несколько раз.

   ```bash
   for p in Anna Boris Clara; do curl "http://127.0.0.1:8000/rolldice?player=$p"; echo; done
   ```

   Каждый ответ задерживается примерно на треть секунды из-за паузы в коде. Через несколько секунд проверьте лог Collector.

   ```bash
   docker logs otel-collector 2>&1 | grep -E "Traces|Logs" | tail -n 2
   ```

<figure><img src="https://docs.f3cloud.com/img/otel-python-requests.png" alt="Три запроса curl к приложению и строки Logs и Traces в логе Collector, шесть спанов"><figcaption><p>Три запроса дали шесть спанов и записи журнала</p></figcaption></figure>

Три запроса дали **шесть спанов**, то есть по два на каждый, и записи журнала приложения. Collector принял все три сигнала.

Теперь вернитесь в Jaeger, обновите страницу, в списке **Service** выберите `dice-server` и нажмите <kbd>Find Traces</kbd>. Откройте любую трассу `GET /rolldice` и нажмите на вложенный спан `roll`.

<figure><img src="https://docs.f3cloud.com/img/otel-jaeger-dice-trace.png" alt="Трасса GET /rolldice в Jaeger со вложенным спаном roll, атрибут roll.value равен 5"><figcaption><p>Запрос и вложенный спан roll, внизу его атрибуты</p></figcaption></figure>

Верхняя полоса это весь запрос `GET /rolldice`, его создала библиотека инструментирования Flask. Под ней вложенный спан `roll`, который мы создали вручную. Он занимает почти всё время запроса, около 300 из 301 миллисекунды, потому что именно там находится наша пауза. В атрибутах спана видно `roll.value` с выпавшим числом. Так на трассе сразу видно, в какой части запроса уходит время.

Остановите приложение сочетанием <kbd>Ctrl</kbd> + <kbd>C</kbd> в первом окне.

> **Отправка по HTTP вместо gRPC.**
>
> Приложение можно переключить на OTLP/HTTP переменной окружения. Добавьте перед командой запуска `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`, и данные уйдут на порт 4318. Если нужен другой адрес Collector, его задаёт переменная `OTEL_EXPORTER_OTLP_ENDPOINT`. Мы проверили оба способа, при HTTP сервис появился в Jaeger так же, как и при gRPC.

***

## Шаг 7. Добавьте метрики сервера

Пока Collector принимает данные от приложений, но сам ничего не измеряет. Научим его собирать метрики сервера с помощью приёмника **host_metrics**. Он умеет снимать загрузку процессора, память, диски и сеть.

Вернитесь в папку проекта.

```bash
cd /opt/otel
```

1. Откройте файл настроек Collector.

   ```bash
   nano otel-config.yaml
   ```

   Сохраните изменения сочетанием <kbd>Ctrl</kbd> + <kbd>O</kbd>, затем <kbd>Enter</kbd>, а выйдите из редактора сочетанием <kbd>Ctrl</kbd> + <kbd>X</kbd>.

2. Добавьте в раздел `receivers` новый приёмник сразу после `otlp`, с таким же отступом в два пробела.

   ```yaml
     # Метрики самого сервера
     host_metrics:
       root_path: /hostfs
       collection_interval: 10s
       scrapers:
         cpu:
         memory:
         load:
         filesystem:
         network:
   ```

   * `scrapers` перечисляет, что именно измерять. Мы взяли процессор, память, среднюю нагрузку, файловую систему и сеть. В документации приёмника есть и другие, например диски и процессы.
   * `collection_interval` задаёт частоту измерений. По умолчанию это одна минута, мы поставили 10 секунд, чтобы быстрее увидеть результат.
   * `root_path` нужен, потому что Collector работает в контейнере. Чтобы он видел сервер, а не собственный контейнер, корень сервера подключают в папку `/hostfs`, а в `root_path` указывают её.

3. В том же файле добавьте новый приёмник в конвейер метрик.

   ```yaml
       metrics:
         receivers: [otlp, host_metrics]
   ```

4. Измените режим экспортёра `debug`, чтобы в логе были не только счётчики, но и сами значения. Замените в разделе `exporters` строку `verbosity: basic` на `verbosity: detailed` и сохраните файл.

5. Откройте файл `compose.yaml` и подключите корень сервера на чтение в раздел `volumes` сервиса `collector`.

   ```bash
   nano compose.yaml
   ```

   ```yaml
       volumes:
         - ./otel-config.yaml:/etc/otelcol-contrib/config.yaml:ro
         - /:/hostfs:ro                 # файловая система сервера для метрик хоста
   ```

6. Проверьте конфигурацию и пересоздайте контейнер Collector.

   ```bash
   docker compose run --rm --no-deps collector validate --config=/etc/otelcol-contrib/config.yaml
   docker compose up -d
   ```

Подождите около 20 секунд, пока Collector сделает первые измерения, и посмотрите, какие метрики он собрал.

```bash
docker logs otel-collector --since 12s 2>&1 | grep "Name: system" | sort -u
```

Чтобы убедиться, что цифры настоящие, возьмём значение занятой памяти из лога и сравним его с командой `free`.

```bash
docker logs otel-collector --since 12s 2>&1 | grep -A12 system.memory.usage | grep -E "state|Value" | head -2
free -b | head -2
```

<figure><img src="https://docs.f3cloud.com/img/otel-hostmetrics.png" alt="Метрики system.cpu, system.memory, system.filesystem и system.network в логе Collector и вывод free для сравнения"><figcaption><p>Collector собирает метрики сервера, а занятая память совпадает с выводом free</p></figcaption></figure>

Collector выдал 13 метрик с именами, начинающимися на `system.`. Например, `system.cpu.load_average.1m` это средняя нагрузка за минуту, `system.memory.usage` это память в байтах по состояниям, `system.filesystem.usage` это занятое место на дисках, а `system.network.io` это объём переданных данных. Занятая память из метрики близка к столбцу `used` в выводе `free`, небольшая разница получилась из-за того, что значения сняты в разные моменты.

> **Верните режим basic.**
>
> Подробный режим выводит в лог около 90 КБ каждую минуту, а это примерно 130 МБ в сутки. Когда насмотритесь, верните в `otel-config.yaml` строку `verbosity: basic` и перезапустите Collector командой `docker compose restart collector`.

Пока метрики попадают только в лог. Чтобы хранить их и рисовать графики, нужно отправить их в хранилище метрик, и в Collector для этого есть экспортёры, например `prometheus`. Это тема отдельной статьи.

***

## Полезные команды

```bash
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                      # остановить и удалить контейнеры
```

После остановки командой `docker compose down` трассы в Jaeger пропадут, так как они хранятся только в памяти.

***

## Частые вопросы

<details>

<summary>Нужно ли открывать порты 4317, 4318 и 16686 в UFW</summary>

Нет. Приложения на этом же сервере подключаются к `127.0.0.1`, а Jaeger мы открываем через SSH-туннель. Порты в Compose опубликованы только на `127.0.0.1`, и мы проверили с другого компьютера, что на внешнем адресе сервера соединение с ними не устанавливается. Не заменяйте `127.0.0.1:4317:4317` на `4317:4317` без необходимости. Брандмауэр UFW порты Docker не закрывает, и Collector без паролей окажется открытым всему интернету.

</details>

<details>

<summary>Приложение работает, но в Collector и Jaeger ничего нет, а в логе Failed to auto initialize OpenTelemetry</summary>

Не установлен экспортёр OTLP. Мы запускали приложение с одним только `opentelemetry-distro` и получили такие строки.

```text
RuntimeError: Requested component 'otlp_proto_grpc' not found in entry point 'opentelemetry_traces_exporter'
Failed to auto initialize OpenTelemetry
```

Само приложение при этом продолжает отвечать на запросы, поэтому ошибка легко остаётся незамеченной. Остановите приложение, выполните `pip install opentelemetry-exporter-otlp` и запустите его снова.

</details>

<details>

<summary>В Jaeger пусто, а в терминале приложения нет ни одной ошибки</summary>

Ошибки отправки в терминал не выводятся, потому что записи журнала, в том числе сообщения самого OpenTelemetry, собирает автоматическое инструментирование. Мы проверили, что с параметром `--logs_exporter console` те же записи вместе с ошибками экспорта появляются на экране. Запустите приложение так.

```bash
opentelemetry-instrument --service_name dice-server --logs_exporter console flask run -p 8000
```

В консоль начнут выводиться и записи самого приложения, и сообщения об ошибках экспорта, примеры которых разобраны в следующих двух вопросах.

</details>

<details>

<summary>Failed to export traces to localhost:4318 … Trying to connect an http1.x server</summary>

Приложение отправляет данные по gRPC, а адрес указывает на порт HTTP. Мы воспроизвели ошибку, запустив приложение с адресом Collector `http://localhost:4318` через переменную `OTEL_EXPORTER_OTLP_ENDPOINT`.

```text
Failed to export traces to localhost:4318, error code: StatusCode.UNAVAILABLE, error details: failed to connect to all addresses; last error: INTERNAL: ipv4:127.0.0.1:4318: Failed parsing HTTP/2 (Expected SETTINGS frame as the first frame, got frame type 80) (Trying to connect an http1.x server (HTTP status 400))
```

Есть два выхода. Либо верните порт gRPC `4317`, либо переключите приложение на HTTP переменной `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf`. У утилиты telemetrygen та же ошибка выглядит как `http2: frame too large, note that the frame header looked like an HTTP/1.1 header`.

</details>

<details>

<summary>BadStatusLine … encountered while exporting spans batch, malformed HTTP response</summary>

Обратная ситуация. Приложение или утилита отправляют данные по HTTP, а адрес указывает на порт gRPC. Мы запустили приложение с переменными `OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf` и `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317`.

```text
Transient error ('Connection aborted.', BadStatusLine('\x00\x00\x06\x04\x00\x00\x00\x00\x00\x00\x05\x00\x00@\x00')) encountered while exporting spans batch, retrying in 0.82s.
Failed to export spans batch due to timeout, max retries or shutdown.
```

Запомните соответствие. Порт 4317 принимает только gRPC, порт 4318 только HTTP. Для HTTP не забывайте и про `--otlp-http` у telemetrygen.

</details>

<details>

<summary>transport: Error while dialing … connect: connection refused</summary>

Collector не запущен или адрес указан неверно. Мы получили эту ошибку, отправив трассы на порт 4319, на котором никто не слушает.

```text
traces export: exporter export timeout: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp 127.0.0.1:4319: connect: connection refused"
```

Проверьте, что контейнер работает, командой `docker compose ps`, и что порты слушаются на сервере.

```bash
ss -tln | grep -E ":(4317|4318|16686) "
```

В выводе должны быть три строки с адресом `127.0.0.1`.

</details>

<details>

<summary>tls: first record does not look like a TLS handshake</summary>

Утилита telemetrygen по умолчанию подключается по TLS, а наш Collector принимает данные без шифрования. Мы получили эту ошибку, запустив утилиту без параметра `--otlp-insecure`.

```text
transport: authentication handshake failed: tls: first record does not look like a TLS handshake
```

Добавьте параметр `--otlp-insecure`.

</details>

<details>

<summary>error reading server preface: connection reset by peer</summary>

Порт опубликован, а Collector внутри контейнера слушает не тот адрес. Так бывает, если в приёмнике `otlp` не указан `endpoint`. Мы проверили конфигурацию с пустыми `grpc:` и `http:`, и в логе Collector появились такие адреса.

```text
"endpoint": "127.0.0.1:4317"
"endpoint": "127.0.0.1:4318"
```

Telemetrygen при этом отвечает ошибкой.

```text
traces export: exporter export timeout: rpc error: code = Unavailable desc = connection error: desc = "error reading server preface: connection reset by peer"
```

Укажите в конфигурации `endpoint: 0.0.0.0:4317` для gRPC и `endpoint: 0.0.0.0:4318` для HTTP, как в шаге 2, и перезапустите Collector.

</details>

<details>

<summary>references exporter "jaeger" which is not configured</summary>

В конвейере указан компонент, которого нет в своём разделе конфигурации. Мы получили эту ошибку, написав в конвейере трасс `jaeger` вместо `otlp_grpc/jaeger`.

```text
Error: service::pipelines::traces: references exporter "jaeger" which is not configured
```

Имя в разделе `service` должно совпадать с именем в `exporters`, включая часть после косой черты. Так же Collector ругается и на неописанные приёмники и процессоры.

</details>

<details>

<summary>receivers::host_metrics: invalid root_path: stat /hostfs: no such file or directory</summary>

В `compose.yaml` не подключён корень сервера в папку `/hostfs`, а в настройках приёмника `host_metrics` она указана.

```text
Error: receivers::host_metrics: invalid root_path: stat /hostfs: no such file or directory
```

Добавьте в раздел `volumes` строку `- /:/hostfs:ro`, как в шаге 7.

</details>

***

## Наши ресурсы

**Telegram-канал:** [https://t.me/f3cloud_news](https://t.me/f3cloud_news)\
**Официальный сайт:** [https://f3cloud.com](https://f3cloud.com/ru)
