ADG Оставить заявку
Блог DevOps 6 мин чтения

OTel Logs GA: мигрируем последние сервисы с Fluent Bit на OpenTelemetry Collector

OpenTelemetry Logs API достиг GA. Разбираем миграцию с Fluent Bit на OTel Collector и настройку tail sampling без потери сигнала об ошибках в тяжёлых микросервисах.

Контекст момента

OpenTelemetry Logs API достиг GA - стабильный контракт для унифицированного сбора логов, метрик и трейсов через единый Collector

OTel Logs API наконец-то вышел в GA. Это означает, что все три сигнала - трейсы, метрики и логи - теперь стабильны в рамках одного стека. Мы давно ждали этого момента: последние полтора года у нас на нескольких проектах был такой зоопарк, где трейсы шли через OTel Collector, метрики через Prometheus Exporter, а логи - по-прежнему через Fluent Bit. Работало, но поддерживать конфигурации двух агентов на каждом узле надоело.

Как только Logs API вышел в GA, мы начали плановую миграцию. Рассказываем, как это выглядит и почему tail sampling оказался самым нетривиальным куском.

Почему не меняли раньше

Fluent Bit - зрелый, быстрый, надёжный. У нас не было повода его трогать, пока OTel Logs был в экспериментальном статусе. Нестабильный API означал риск: конфигурация сломается при апгрейде Collector, придётся переписывать.

Теперь статус изменился. GA - это стабильный контракт на атрибуты, семантику и формат. Мы можем строить пайплайны, не боясь, что через три месяца имена полей переименуют.

Второй момент: когда все три сигнала идут через один Collector, появляются реальные возможности для корреляции на уровне пайплайна. Пока логи жили в Fluent Bit, а трейсы - в OTel Collector, мы не могли, например, обогатить лог атрибутами текущего трейса без дополнительных телодвижений. Теперь это делается прямо в Collector через k8sattributes процессор и propagation context.

Как выглядит миграция

Схема у нас такая: Kubernetes-кластер, на каждом узле раньше жил DaemonSet с Fluent Bit, который читал логи контейнеров и гнал их в Loki. Параллельно - OTel Collector как sidecar или отдельный DaemonSet для трейсов и метрик.

После миграции: один OTel Collector DaemonSet, который принимает всё. Fluent Bit уходит.

Для сбора логов контейнеров в Collector используем filelog receiver - он читает файлы из /var/log/pods/ так же, как это делал Fluent Bit:

receivers:
  filelog:
    include:
      - /var/log/pods/*/*/*.log
    start_at: beginning
    include_file_path: true
    include_file_name: false
    operators:
      - type: container
        id: container-parser

Оператор container автоматически парсит формат Docker/containerd - timestamp, stream, log body. После этого добавляем k8sattributes процессор, который обогащает каждую запись метаданными pod-а: namespace, deployment, node.

Экспорт идёт в Loki через loki exporter - тот же backend, что был с Fluent Bit, ничего менять не нужно.

Tail sampling: зачем и как

Здесь начинается самое интересное с инженерной точки зрения. У нас есть несколько высоконагруженных сервисов - условно, API-gateway и сервис обработки платежей. Они генерируют огромное количество трейсов и связанных логов. Если писать всё в Loki/Tempo без фильтрации, хранилище растёт неприлично быстро.

Раньше с Fluent Bit мы резали объём через head-based sampling - просто брали каждый N-й лог. Проблема очевидная: ошибки тоже попадают в выборку случайно, и если в 1% запросов что-то сломано, при сэмплинге 10% мы видим это ошибку лишь в 10% случаев, а иногда вообще её теряем.

OTel Collector решает эту задачу через tail-based sampling - решение о сохранении трейса принимается после того, как весь трейс собран. Это принципиально: мы видим итоговый статус и можем сохранить ровно то, что важно.

Конфиг для tailsampling процессора:

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 50000
    policies:
      - name: errors-policy
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow-requests-policy
        type: latency
        latency: { threshold_ms: 2000 }
      - name: probabilistic-policy
        type: probabilistic
        probabilistic: { sampling_percentage: 5 }

Логика здесь такая: сначала применяются политики по порядку. Трейс с ошибкой - сохраняем всегда. Трейс медленнее 2 секунд - сохраняем всегда. Всё остальное - 5% вероятность. Это даёт нам полный сигнал о проблемах при значительно меньшем объёме данных.

Параметр decision_wait: 10s означает, что Collector ждёт 10 секунд, чтобы собрать все спаны трейса, прежде чем принять решение. Для большинства наших запросов этого достаточно. Для длинных batch-операций пришлось увеличить до 30 секунд - иначе трейс обрезался посередине.

num_traces: 50000 - это лимит трейсов, которые Collector держит в памяти в ожидании решения. На хосте с тяжёлым сервисом это означает заметное потребление памяти. Мы выставили limits в pod spec и несколько раз наловили OOMKill на этапе тестирования, пока не подобрали правильные значения. Ориентир: один трейс в памяти весит примерно 1-5 KB в зависимости от количества спанов. Считайте сами под свою нагрузку.

Что пошло не так

Несколько моментов, на которых потеряли время.

Порядок процессоров. OTel Collector применяет процессоры в том порядке, в котором они указаны в pipeline. Мы сначала поставили tail_sampling перед k8sattributes, и атрибуты Kubernetes не попадали в решение о сэмплинге. Поправили порядок - сначала k8sattributes, потом tail_sampling.

Tail sampling работает только с трейсами, не с логами. Это важно: tail_sampling процессор обрабатывает только сигнал traces. Для логов отдельной политики «сохранить лог, если в трейсе была ошибка» из коробки нет. Мы решили это через фильтрацию по severity: filter процессор режет логи ниже WARN на уровне Collector, а ERROR и выше сохраняем все.

Fluent Bit и OTel Collector одновременно. На период миграции мы несколько дней запускали оба агента параллельно - сравнивали, всё ли попадает. Дублирование логов в Loki дало красивые артефакты на дашбордах. Следите за тем, чтобы убрать Fluent Bit только после того, как убедились в корректности нового пайплайна.

Итог на сегодня

Миграция завершена на двух из трёх кластеров. Третий - legacy-окружение с нестандартным container runtime - ждёт отдельного подхода, там filelog receiver не отработал из коробки и нужно разбираться.

На мигрированных кластерах объём логов в Loki снизился примерно вдвое - за счёт фильтрации DEBUG/INFO на уровне Collector. При этом ни одного пропущенного алерта об ошибке с момента запуска не было. Это пока слишком короткий срок для выводов, но первое впечатление позитивное.

DaemonSet стал один вместо двух. Это сами по себе не драматическое изменение, но конфигурация теперь в одном месте, и это ощутимо.

Самый полезный результат - не технический. Теперь, когда инцидент-агент из нашего стека на Zabbix + Loki получает контекст для анализа, логи и трейсы несут одинаковые атрибуты: тот же service.name, тот же k8s.pod.name. Корреляция в Grafana стала работать без ручного маппинга. Это и есть основная ценность единого стека - не «один агент вместо двух», а согласованные данные.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.