Перейти к основному содержимому

Пишем логи из COI в Cloud Logging при помощи Fluentbit

· 7 мин. чтения

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

к сведению

UPD 2023-11-12: Я добавил в пример Terraform и расширил рассказ о конфигурации Fluentbit, примером про использование шаблонов для подстановки значений.

Шаг 1. Пишем логи

Ну что ж, опять начнем с того, что набросаем демо приложение на питоне, которое будет для нас генерировать записи логов.

import logging
import random
import sys
import time

# Писать логи контейнера будем в STDOUT. Разбивать по severity будем при помощи парсера в Fluent-bit
import uuid

logger = logging.getLogger(__name__)

# Зададим форматер чтобы позже по этому же шаблону парсить логи.
formatter = logging.Formatter(
'[req_id=%(req_id)s] [%(levelname)s] %(code)d %(message)s'
)

handler = logging.StreamHandler(stream=sys.stdout)
handler.setFormatter(formatter)

logger.addHandler(handler)

# Опционально можно настроить уровень логирования по умолчанию
logger.setLevel(logging.DEBUG)

# Мы могли бы обойтись и простым логированием случайных чисел, но я решил генерировать URL-подобные значения.
PATHS = [
'/',
'/admin',
'/hello',
'/docs',
]

PARAMS = [
'foo',
'bar',
'query',
'search',
None
]


def fake_url():
path = random.choice(PATHS)
param = random.choice(PARAMS)
if param:
val = random.randint(0, 100)
param += '=%s' % val
code = random.choices([200, 400, 404, 500], weights=[10, 2, 2, 1])[0]
return '?'.join(filter(None, [path, param])), code


if __name__ == '__main__':
while True:
req_id = uuid.uuid4()
# создаем пару код и значение URL
path, code = fake_url()
extra = {"code": code, "req_id": req_id}
# Если код 200, то пишем в лог с уровнем Info
if code == 200:
logger.info(
'Path: %s',
path,
extra=extra,
)
# Иначе с уровнем Error
else:
logger.error(
'Error: %s',
path,
extra=extra,
)
# Чтобы можно было погрепать несколько сообщение по одному request id в 30% случаев будем писать вторую запись
# в лог с уровнем Debug.
if random.random() > 0.7:
logger.debug("some additional debug log record %f", random.random(), extra=extra)

# Ждем 1 секунду, чтобы излишне не засорять журнал
time.sleep(1)

В принципе все просто и я постарался оставить комментарии, но я пройдусь еще раз по основным пунктам.

Логи docker-контейнера это просто вывод STDOUT и STDERR. В приложении я решил не делить вывод по двум этим потокам, так как далее в парсере Fluentbit у нас будет возможность распарсить строчку лога и вычленить оттуда уровень с которым была сделана запись.

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

Шаг 2. Настраиваем развертывание COI VM

Так как нам нужно развернуть как минимум два контейнера — контейнер с нашим приложением и логер-агент Fluentbit — воспользуемся возможностью COI работать со спецификацией docker compose. Опишем в файле наши контейнеры. Репозиторий с моим образом не публичный, вам нужно будет использовать свой.

docker-compose.yaml
version: '3.7'
services:
logs:
container_name: logs-app
image: cr.yandex/crpk28lsfu91rns28316/dockerlogtest:2021.10.17-6166ecb
restart: always
depends_on:
- fluentbit
logging:
# Fluent-bit понимает логи в этом формате
driver: fluentd
options:
# куда посылать лог-сообщения, необходимо чтобы адрес
# совпадал с настройками плагина forward
fluentd-address: localhost:24224
# теги используются для маршрутизации лог-сообщений, тема
# маршрутизации будет рассмотрена ниже
tag: app.logs

fluentbit:
container_name: fluentbit
# You can find the latest version of the image here:
# https://github.com/yandex-cloud/fluent-bit-plugin-yandex/releases
image: cr.yandex/yc/fluent-bit-plugin-yandex:v2.1.1-fluent-bit-2.1.7
ports:
- 24224:24224
- 24224:24224/udp
restart: always
environment:
YC_GROUP_ID: e23j5q1nhth94apeduuh
volumes:
- /etc/fluentbit/fluentbit.conf:/fluent-bit/etc/fluent-bit.conf
- /etc/fluentbit/parsers.conf:/fluent-bit/etc/parsers.conf

В контейнер Fluentbit дополнительно передаем переменную окружения YC_GROUP_ID. Она понадобится нам для настройки плагина yc-logging и скажет ему куда писать наши логи и содержит id группы логирования.

Далее нам понадобится принести на вм конфиги. Сделаем это как в инструкции, только в это раз все проще и KMS нам не понадобится.

Шаг 3. Настраиваем чтение логов из контейнера

cloud-init
#cloud-config
write_files:
- content: |
[SERVICE]
Flush 1
Log_File /var/log/fluentbit.log
Log_Level error
Daemon off
Parsers_File /fluent-bit/etc/parsers.conf
[FILTER]
Name parser
Match app.logs
Key_Name log
Parser app_log_parser
Reserve_Data On
[INPUT]
Name forward
Listen 0.0.0.0
Port 24224
Buffer_Chunk_Size 1M
Buffer_Max_Size 6M
[OUTPUT]
Name yc-logging
Match *
group_id ${YC_GROUP_ID}
message_key text
level_key severity
default_level WARN
authorization instance-service-account
path: /etc/fluentbit/fluentbit.conf
- content: |
[PARSER]
Name app_log_parser
Format regex
Regex ^\[req_id=(?<req_id>[0-9a-fA-F\-]+)\] \[(?<severity>.*)\] (?<code>\d+) (?<text>.*)$
Types code:integer
path: /etc/fluentbit/parsers.conf
users:
- name: username
groups: sudo
shell: /bin/bash
sudo: [ 'ALL=(ALL) NOPASSWD:ALL' ]
ssh-authorized-keys:
- ssh-rsa AAAA

Теперь подробнее зачем нам нужна каждая часть конфига. В секции [SERVICE] указаны настройки самого приложения Fluentbit, как например период с которым он отправляет логи. Подробнее можно прочитать тут.

[SERVICE]
Flush 1
Log_File /var/log/fluentbit.log
Log_Level error
Daemon off
Parsers_File /fluent-bit/etc/parsers.conf

В секции INPUT описано откуда и как нужно забирать логи.

[INPUT]
Name forward
Listen 0.0.0.0
Port 24224
Buffer_Chunk_Size 1M
Buffer_Max_Size 6M

Для работы с логами в формате Fluentd/Fluentbit нужно использовать инпут с типом forward. Про другие возможные типы инпутов можно прочитать в документации. А так же у меня есть инструкция как работать с systemd инпутом Fluentbit.

Тут вроде все понятно: Fluetnbit слушает логи на 24224 порту. Именно туда мы указали docker-compose отправлять логи нашего в соответствующем конфиге выше.

Также в том конфиге мы сказали драйверу помечать все записи тегом app.logs именно на него мы можем ориентироваться настраивая процессинг логов.

Для этого в файле parsers.conf мы опишем парсер.

[PARSER]
Name app_log_parser
Format regex
Regex ^\[req_id=(?<req_id>[0-9a-fA-F\-]+)\] \[(?<severity>.*)\] (?<code>\d+) (?<text>.*)$
Types code:integer

Мы воспользуемся regex парсером. Для его конфигурирования нужно задать регулярное выражение, при помощи которого мы будем разбирать строчки лога. Из каждой строки мы извлечем поля: req_id в которое клали уникальный id запроса, severity — уровень логирования, code — http код ответа и text — весь остальной текст.

Теперь этот парсер мы можем использовать, чтобы преобразовать наши логи. Для этого в основном конфиге добавим секцию FILTER.

[FILTER]
Name parser
Match app.logs
Key_Name log
Parser app_log_parser
Reserve_Data On

Тут мы говорим, что ищем только логи, тег которых совпадает с app.logs, берем из записи поле log, применяем к нему наш парсер app_log_parser, а все остальные поля лога сохраняем (Reserve_Data On).

Шаг 4. Отгружаем в Cloud logging

Ну вот мы и подошли к отправке логов в Облако. Так как мы разворачивали контейнер Fluentbit из образа куда уже добавлен плагин Yandex Cloud Logging, нужно лишь сконфигурировать секцию OUTPUT.

[OUTPUT]
Name yc-logging
Match *
group_id ${YC_GROUP_ID}
message_key text
level_key severity
default_level WARN
authorization instance-service-account

Для отправки логов нужна авторизация. Мы воспользуемся самым простым способом — привяжем к ВМ сервисный аккаунт, на который выдадим роль необходимую для записи логов logging.writer или выше.

к сведению

UPD: Теперь вы можете указывать в конфиге поля resource_type, resource_id и stream_name и тогда вы сможете фильтровать логи по этим полям. Самое интересное, что эти поля поддерживают синтаксис шаблонов подстановок, т.е. вы можете указать в одинарных фигурных скобках путь в json объекте лога, или в двойных фигурных скобках указать переменную окружения, которая будет взята из метаданных ВМ.

Например, так вы сможете фильтровать логи по имени контейнера и по хосту ВМ на которой запущен Fluentbit:

[OUTPUT]
Name yc-logging
Match *
group_id ${YC_GROUP_ID}
message_key text
level_key severity
default_level WARN
authorization instance-service-account
resource_type vm
resource_id {{instance/hostname}}
stream_name {container_name}

Отлично, теперь нужно лишь создать ВМ с этими конфигами.

Нам понадобится id image из которого мы будем разворачивать ВМ.

IMAGE_ID=$(yc compute image get-latest-from-family container-optimized-image --folder-id standard-images --format=json | jq -r .id)

Дальше его нам его надо будет подставить в команду создания ВМ.

yc compute instance create \
--name coi-vm \
--zone=ru-central1-a \
--network-interface subnet-name=default-ru-central1-a,nat-ip-version=ipv4 \
--metadata-from-file user-data=user-data.yaml,docker-compose=spec.yaml \
--create-boot-disk image-id=$IMAGE_ID \
--service-account-name service-account

Когда ВМ развернется и контейнеры на ней запустятся можно перейти в Cloud Logging и посмотреть логи.

Если кликнуть на иконку глаза, можно увидеть дополнительные залогированные параметры.

По ним можно пофильтровать. Например, запрос json_payload.code >= 400 найдет все строчки логов связанные с ответами содержавшими какие-либо ошибки.

Так же можно найти все сообщения относившиеся к обработке одного запроса, если пофильтровать по json_payload.req_id.

Подробнее про язык запросов можно прочитать в документации.

Весь код из этой статьи в одном месте.