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 стала работать без ручного маппинга. Это и есть основная ценность единого стека - не «один агент вместо двух», а согласованные данные.