Централизованное хранение логов с помощью Loki и Fluent-bit

Введение

Во всех моих проектах действует принцип “логи не должны содержать ошибок”. Это значит, что как только появляется ошибка в логах, я должен с ней разобраться. Это может показаться занудством, но на практике, разбирая даже самые очевидные и небольшие проблемы, я открывал для себя много нового. По мере увеличения количества устройств в сети, их становится всё сложнее и дольше проверять на наличие ошибок. Чтобы решить эту проблему, нужно скидывать логи в какое-то центральное место и посылать оповещения если появилась ошибка.

Выбор технологий

Централизация - это логично, но на практике всё не так просто. До последнего времени самым распространённым способом собрать логи в одном месте был стек технологий: Elasticsearch - Logstash - Kibana (ELK). И качество его кода было прямо таки сомнительным. Для начала, Elasticsearch не подходит для хранения логов. Эта распределённая система для полнотекстового поиска. Она разбивает предложения на лексемы, делает морфологический анализ и пр. Всё это занимает место на диске и требует дополнительной обработки на сервер. Для логов это просто не нужно. Для них достаточен поиск по уровню, например “ERROR”, и/или ключевому слову, например id=123234. Kibana - UI для поиска данных в Elasticsearch. Тяжеловесное приложение на javascript с 1207 (!) прямых зависимостей. Logstash - агент для отправки логов. Ещё один монстр бесконтрольного добавления количества фич. В архиве занимает 400+ Мб и при запуске съедает 700+ Мб памяти. Как будто плохого качества кода было недостаточно и Elasticsearch B.V. поменяло лицензию. Это породило довольно популярный форк Opensearch и Opensearch dashboards. После чего компания решила сделать агенты несовместимые с Opensearch, что привело теперь к ещё и путанице с различными версиями.

В результате я стал более пристальнее смотреть на альтернативы и открыл для себя следующий стек: fluent-bit - Loki - Grafana. Такое ощущение, что каждое из этих приложений - это ответ на безумство ELK.

  • fluent-bit. Написан на Си и занимает в памяти 35Мб. Не является частью других приложений, поэтому имеет хорошую интеграцию с Elasticsearch, Opensearch, Loki, Influx, Prometheus и пр.
  • Loki - база для хранения логов. Построена по тем же принципам, что и Prometheus, но для логов. Каждая строчка имет некоторое количество аттрибутов по которым можно фильтровать.
  • Grafana - универсальный UI для отображения метрик. С недавнего времени есть поддержка отображения логов. В отличие от Kibana может отображать не только логи, но и метрики. В отличие от Kibana может отображать логи из Elasticsearch, Opensearch и Loki. “Всего” 167 зависимостей.

fluent-bit

fluent-bit определяет три фазы обработки данных:

  • input. Источники данных
  • filter. Обработка каждой записи
  • output. Отправка данных

Источниками данных может быть как файл, так и различные системы. В частности, systemd. В systemd есть специальный процесс journald, который собирает логи со всей системы и сохраняет в бинарные файлы. Он пришёл на замену syslog и основное отличие в том, что логи стали структурированными и бинарными. Это позвоялет делать более быстрый поиск, поиск по тэгам и пр. Ещё одной особенностью systemd является то, что все запущенные сервисы отправляют свои логи в stdout/stderr, которые автоматически отправляются в journald. Так вот fluent-bit может подключаться к journald по API и получать логи. Это невероятно мощная комбинация:

  • Во-первых, не нужно парсить файлы. fluent-bit получает уже структурированные логи.
  • Во-вторых, fluent-bit получает вообще все логи в системе. Это невероятно круто, поскольку позволяет коррелировать события в приложении с общесистемными. Например, приложение не смогло отправить данные на сервер. В логах будет сначала потеря сигнала с WiFi (бывший dmesg), а потом уже логи приложения.

Конфигурируется input следующим образом:

[INPUT]
    Name            systemd
    Tag             host.*
    Lowercase On
    Strip_Underscores On
    Read_From_Tail On

По-умолчанию, плагин добавляет атрибуты сообщения в том же формате, что и journald, например _SYSLOG_IDENTIFIER. Это выглядит немного кричаще и подчёркивание первым символом обычно используется для каких-то зарезервированных переменных. Хорошо, что по-умолчанию плагин может преобразовать такие атрибуты в syslog_identifier.

journald добавляет множество атрибутов разной полезности. Например, _STREAM_ID=498daf9630ae40cebb79c5c1f475617e или _PID=39877. Хранить их в базе достаточно бессмысленно, поэтому можно отфильтровать:

[FILTER]
    Name record_modifier
    Match *
    Allowlist_key hostname
    Allowlist_key syslog_identifier
    Allowlist_key systemd_unit
    Allowlist_key message
    Allowlist_key priority

Далее необходимо преобразовать priority в более человеко-читабельный формат. priority - это число, которое соответствует уровню лога. Уровни бывают семи видов от 0 - fatal до 7 - trace. На практике важны только 4: ERROR, INFO, WARN, DEBUG. На каждое изменение атрибута нужна своя секция filter:

[FILTER]
    Name modify
    Match host.*
    Condition Key_Value_Equals priority 6
    Remove priority
    Add level info

[FILTER]
    Name modify
    Match host.*
    Condition Key_Value_Equals priority 3
    Remove priority
    Add level error

[FILTER]
    Name modify
    Match host.*
    Condition Key_Value_Equals priority 2
    Remove priority
    Add level error

[FILTER]
    Name modify
    Match host.*
    Condition Key_Value_Equals priority 1
    Remove priority
    Add level error

[FILTER]
    Name modify
    Match host.*
    Condition Key_Value_Equals priority 0
    Remove priority
    Add level error

[FILTER]
    Name modify
    Match host.*
    Condition Key_Value_Equals priority 4
    Remove priority
    Add level warn

[FILTER]
    Name modify
    Match host.*
    Condition Key_Value_Equals priority 7
    Remove priority
    Add level debug

[FILTER]
    Name modify
    Match host.*
    Condition Key_Value_Equals priority 5
    Remove priority
    Add level debug

В результате в Loki будет хранится атрибут level с возможными значениями error, warn, debug, info. Текстовые значения особенно удобны в Grafana при конфигурации дашбордов.

Завершающим шагом нужно сконфигурировать output секцию:

[OUTPUT]
    name   loki
    host   <somehost>
    match  host.*
    labels    $hostname, $syslog_identifier, $level
    remove_keys $hostname, $syslog_identifier, $level
    drop_single_key        raw

Комбинация labels, remove_keys и drop_single_key позволяет преобразовать атрибуты сообщения в labels для отправки в Loki. Если этого не сделать, то в качестве сообщения будет отправлен json с множеством полей. Это удобно, если хочется передать чуть больше контекста, но в моём простом случае не нужно.

Influxdb

Иногда приложения плохо интегрированы с journald. И тогда в локах можно увидеть:

priority=6 message="ts=2025-02-27T15:33:23.222758Z lvl=error msg=\"some error goes here\" log_id=0uJYAO6l000 engine=tsm1 trace_id=0uz0VbaW000 op_name=tsm1_cache_snapshot op_event=st"

Во-первых, время дублируется внутри сообщения. Во-вторых, сообщение с priority=6 (INFO), но при этом внутри него lvl=error. В-третьих, сообщение содержит какие-то дополнительные бесполезные атрибуты. В таких случаях fluent-bit может распарсить текст.

[FILTER]
    Name parser
    Match host.influxdb.service
    Parser logfmt
    Key_Name message
    Reserve_Data On

[FILTER]
    Name modify
    Match host.influxdb.service
    Rename msg message
    Rename lvl level

В этой конфигурации есть два интересных момента: во-первых, фильтр можно настроить только на конкретное приложение. Это позволяет ускорить парсинг данных. Во-вторых, fluent-bit поддерживает множество стандартных парсеров, в том числе logfmt.

Java

Иногда приложения печатают в лог несколько строчек на одно событие. Самое распространённое - это java stacktrace в случае ошибки. При анализе таких событий неплохо было бы собрать все строчки вместе. Для этого есть поддержка multiline фильтра:

[FILTER]
    name                  multiline
    match                 host.r2cloud.service
    multiline.key_content message
    multiline.parser      custom-java

fluent-bit поддерживает несколько стандартных парсеров: go, java, python, docker. Однако, для java он ожидает следующий формат:

<some message> <exception>
	at
	at
	at

В то время как большинство библиотек логирования выводят в другом формате:

<some message>
<exception>
	at
	at
Caused by:
	at
	at

Но это не беда, потому что fluent-bit позволяет задавать собственный формат. Я сконфигурировал custom-java следующим образом:

[MULTILINE_PARSER]
    name          custom-java
    type          regex
    flush_timeout 1000
    # rules |   state name  | regex pattern                  | next state
    # ------|---------------|--------------------------------------------
    rule      "start_state"   "/(.+)/"                         "exception"
    rule      "exception"     "/(.+Exception.*)/"              "cont" 
    rule      "cont"          "/^(Caused by:.*)|(\s+at.*)/"    "cont"

Grafana

Grafana по-умолчанию поставляется с плагином для доступа к Loki, поэтому можно сразу переходить к конфигурации datasource:

Далее сделать дашборд. Самое простое - это список логов во всю ширину экрана с бесконечной прокруткой.

Я использовал визуализацию “Logs”, но можно и обычную таблицу. Разница лишь в том, что ошибки слева подсвечиваются красным и это удобно. Помимо этого сверху есть быстрые фильтры, которые можно сделать через параметры дашборда.

Единственный недостаток в том, что многострочные логи выводятся в одну строчку и это выглядит не очень:

Заключение

fluent-bit - это как глоток свежего воздуха. Он делает именно то, что нужно. Занимает памяти ровно столько, сколько не жалко выдать агенту для сбора метрик. Доступен для любых платформ и архитектур. Наверное, единственный его недостаток в том, что его собственные логи в странном формате, который нельзя поменять, поэтому для разбора его логов нужно писать отдельный парсер.

Следующим шагом будет настройка оповещений об ошибках.