Передача логов с виртуальной машины с помощью Fluent Bit и Lua-скрипта
Fluent Bit — кроссплатформенный инструмент с открытым исходным кодом. Он собирает, обрабатывает и фильтрует лог-сообщения из разных источников, а затем сохраняет их в хранилище. После этого лог-сообщения поступают в маршрутизатор, который определяет, куда они будут отправлены. Для работы с разными источниками и приемниками используются специализированные плагины.
Перед началом работы
Создайте сервисный аккаунт. В блоке Доступы и роли выберите роли:
в блоке Проект — «Пользователь сервисов»;
в блоке Сервисы — «logaas.writer».
Для сервисного аккаунта создайте API-ключ. В параметрах API-ключа укажите сервис «logging_as_a_service».
Срок действия API-ключа ограничен — когда он подойдет к концу, мы отправим вам уведомление. После этого необходимо обновить API-ключ.
Создайте виртуальную машину Ubuntu 22.04.
Шаг 1. Установка Fluent Bit
Возможно использование Fluent Bit версии 2.2 и выше. Рекомендуемая версия — 3.2.
Установите Fluent Bit одним из способов:
Установите приложение Fluent Bit из сборки дистрибутива для вашей операционной системы.
Чтобы проверить, что fluent-bit установлен корректно, нужно запустить его и убедиться, что он установлен как сервис. Для этого:
Запустите fluent-bit как сервис:
sudo systemctl start fluent-bitПроверьте статус сервиса fluent-bit — он должен быть активным:
systemctl status fluent-bitЕсли fluent-bit настроен верно, будет выведен статус в виде:
● fluent-bit.service - Fluent BitLoaded: loaded (/lib/systemd/system/fluent-bit.service; disabled; vendor preset: enabled)Active: active (running) since Tue 2025-03-11 15:48:23 UTC; 3s agoDocs: https://docs.fluentbit.io/manual/Main PID: 34596 (fluent-bit)Tasks: 8 (limit: 2323)Memory: 9.4MCPU: 70msCGroup: /system.slice/fluent-bit.service└─34596 /opt/fluent-bit/bin/fluent-bit -c //etc/fluent-bit/fluent-bit.confПосле проверки сервиса fluent-bit остановите его, чтобы далее настроить на совместную работу с logaas:
sudo systemctl stop fluent-bit
Шаг 2. Настройка Fluent Bit
Создайте файл logaas_format.lua для форматирования логов в формат сервиса «Клиентское логирование»:
sudo touch /etc/fluent-bit/logaas_format.luaОткройте созданный файл с помощью редактора nano:
sudo nano /etc/fluent-bit/logaas_format.luaВ файл добавьте:
-- Fluent Bit lua client script-- Version: 1.0.1-- Copyright 2025 Cloud.ru-- Licensed under the Apache License, Version 2.0 (the "License");-- you may not use this file except in compliance with the License.-- You may obtain a copy of the License at-- http://www.apache.org/licenses/LICENSE-2.0-- Unless required by applicable law or agreed to in writing, software-- distributed under the License is distributed on an "AS IS" BASIS,-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.-- See the License for the specific language governing permissions and-- limitations under the License.local whitelist = {"timestamp","level","project_id","log_group_id","default_labels","labels","message","json_message","trace_id","service_name","instance_id"}local whitelist_hash = {}for _, key in ipairs(whitelist) dowhitelist_hash[key] = trueendlocal json = (function()local escape = function(str)return str:gsub('[\\"/]', function(c)return '\\' .. cend)endlocal function encode_value(val)local t = type(val)if t == 'string' thenreturn '"' .. escape(val) .. '"'elseif t == 'number' or t == 'boolean' thenreturn tostring(val)elseif t == 'table' thenlocal result = {}for k, v in pairs(val) dolocal key = type(k) == 'number' and '[' .. k .. ']' or '"' .. k .. '"'table.insert(result, key .. ':' .. encode_value(v))endreturn '{' .. table.concat(result, ',') .. '}'elsereturn 'null'endendreturn {encode = function(tbl) return encode_value(tbl) end}end)()function extra_fields(record)local message_ = {}local keys_to_remove = {}local json_message = {}local message_data = {}for key, value in pairs(record) doif not whitelist_hash[key] thenmessage_data[key] = valuetable.insert(keys_to_remove, key)endendif next(message_data) ~= nil thenjson_message = json.encode(message_data)return json_messageelsereturn nilendendfunction ensure_string(var)if type(var) == "string" thenreturn var, trueendif type(var) == "number" or type(var) == "boolean" thenreturn tostring(var), trueendreturn nil, falseendfunction format_log(tag, timestamp, record)-- 1. Project ID & Log Group IDlocal default_project_id = record.default_project_idlocal default_group_id = record.default_group_idlocal project_id = record.project_idif not project_id or project_id == "" thenproject_id = default_project_idendlocal group_id = record.group_idif not group_id or group_id == "" thengroup_id = default_group_idendrecord.default_project_id = nilrecord.default_group_id = nil-- 2. Timestamplocal system_timezone = os.date("%z")local timezone_formatted = string.sub(system_timezone, 1, 3) .. ":" .. string.sub(system_timezone, 4, 5)local sec = timestamp.seclocal nsec = timestamp.nseclocal iso_timestamp = os.date("%Y-%m-%dT%H:%M:%S", sec)local milliseconds = string.format("%03d", math.floor(nsec / 1000000))local formatted_ts = iso_timestamp .. "." .. milliseconds .. timezone_formatted-- 3. Messagelocal message = record.messageif type(message) == "table" thenmessage = json.encode(message)end-- 4. Label merginglocal default_labels = {}local keys_to_remove = {}local merged_labels = {}for key, value in pairs(record) doif key:find("^default_labels.") thenlocal subkey = key:sub(16)default_labels[subkey] = valuetable.insert(keys_to_remove, key)endendfor _, key in ipairs(keys_to_remove) dorecord[key] = nilendfor k, v in pairs(default_labels) domerged_labels[k] = vendif record.labels thenfor k, v in pairs(record.labels) doval, ok = ensure_string(v)if ok thenmerged_labels[k] = valelseprint("skip unsupported type value: "..k.." => "..v)endendend-- 5. Build loglocal log_entry = {timestamp = formatted_ts,level = record.level or "INFO",project_id = project_id,log_group_id = group_id,labels = merged_labels,message = message,json_message = extra_fields(record)}if record.trace_id and record.trace_id ~= "" thenlog_entry.trace_id = record.trace_idendif record.service_name and record.service_name ~= "" thenlog_entry.service_name = record.service_nameendif record.instance_id and record.instance_id ~= "" thenlog_entry.instance_id = record.instance_idend-- 6. Completereturn 1, timestamp, log_entryendОткройте файл /etc/fluent-bit/fluent-bit.conf:
sudo nano /etc/fluent-bit/fluent-bit.confДобавьте в него данные в виде:
[SERVICE]Daemon OffFlush 1Log_Level infoParsers_File parsers.confstorage.sync full[INPUT]Name tailPath <path-to-log/logfile.log>Parser docker[FILTER]Name parserMatch *Key_Name logParser jsonReserve_Data true# target section[FILTER]name modifymatch *Set default_project_id REPLACE_TO_PROJECT_IDSet default_group_id REPLACE_TO_LOG_GROUP_ID#default labels section[FILTER]name modifymatch *Set default_labels.<label_name_1> value_ASet default_labels.<label_name_2> value_B[FILTER]Name luaMatch *Script logaas_format.luaCall format_logtime_as_table true[OUTPUT]Name httpMatch *Host logging.api.cloud.ruPort 443tls onURI /api/v1/logs-ingestjson_date_key falseHeader Authorization Api-Key REPLACE_TO_SA_API_KEYСекция [INPUT] указывает на источник логов, а [OUTPUT] — на сервис, в который отправятся логи.
В режиме tail сбор логов в fluent-bit работает по принципу отслеживания новых записей в логах. При перезапуске сервиса данные, обработанные ранее, не отправляются в систему повторно.
Измените файл, подставив в него свои данные:
<path-to-log/logfile.log> — путь к файлу-источнику логов: fluent-bit будет сканировать этот файл и отслеживать в нем новые строки.
REPLACE_TO_PROJECT_ID и REPLACE_TO_LOG_GROUP_ID — ID проекта и ID лог-группы, в которую будут отправлены логи. REPLACE_TO_LOG_GROUP_ID — необязательная строка. Если ее не добавить, логи отправятся в группу проекта по умолчанию (default-группа).
REPLACE_TO_SA_API_KEY — API-ключ сервисного аккаунта. Проверьте, что у вас есть доступ к проекту, а для вашего сервисного аккаунта выбраны роли «Пользователь сервисов» и «logaas.writer».
Секция default labels section — опциональная. В ней вы можете указать метки, которые будут добавлены ко всем логам. Это удобно для последующей фильтрации логов с помощью языка фильтрующих выражений. Метки указываются в виде default_labels.<label_name>, где <label_name> — имя метки, которая добавится к логам.
В следующем шаге инструкции настраивается тестовая отправка данных с помощью генератора логов, который записывает логи в лог-файл. Для тестирования с помощью генератора вместо <path-to-log/logfile.log> укажите путь к лог-файлу: /usr/local/bin/log_producer/error_log.log.
Пример изменений в файле /etc/fluent-bit/fluent-bit.conf:
[SERVICE]Daemon OffFlush 1Log_Level infoParsers_File parsers.confstorage.sync full[INPUT]Name tailPath /usr/local/bin/log_producer/error_log.logParser docker[FILTER]Name parserMatch *Key_Name logParser jsonReserve_Data true# target section[FILTER]name modifymatch *Set default_project_id 00000000-1111-2222-3333-444444444444Set default_group_id aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee#default labels section[FILTER]name modifymatch *Set default_labels.some_field_A value_ASet default_labels.some_field_B value_B[FILTER]Name luaMatch *Script logaas_format.luaCall format_logtime_as_table true[OUTPUT]Name httpMatch *Host logging.api.cloud.ruPort 443tls onURI /api/v1/logs-ingestjson_date_key falseHeader Authorization Api-Key ZDVkNmVlY2EtxxxxxxxxxxxxxxxxhYTJhNGJl.xxxxxxxxxxxxx
Шаг 3. Проверка отправки логов
На этом этапе вы сможете настроить тестовую отправку логов с помощью bash-скрипта — генератора логов. Он будет записывать логи в лог-файл.
Чтобы создать генератор:
Создайте директорию, в которой будет находиться скрипт:
sudo mkdir /usr/local/bin/log_producer/Создайте пустой файл log_producer.sh:
sudo touch /usr/local/bin/log_producer/log_producer.shОткройте созданный файл с помощью редактора nano:
sudo nano /usr/local/bin/log_producer/log_producer.shВ файл добавьте:
#!/bin/bashLOG_FILE=${1:-./error_log.log}generate_log() {# Generate @timestamp in UTC with 8 fractional secondsnanoseconds=$(date +"%N")trimmed_ns=${nanoseconds:0:8}timestamp=$(date -u "+%Y-%m-%dT%H:%M:%S.${trimmed_ns}Z")# Random log level selectionlevels=("TRACE" "DEBUG" "INFO" "NOTICE" "WARN" "ERROR" "CRITICAL" "ALERT" "EMERGENCY" "FATAL")level=${levels[$RANDOM % ${#levels[@]}]}# Thread selectionthreads=("rest-query-pool-1" "rest-query-pool-2" "worker-thread-3" "io-thread-4")thread=${threads[$RANDOM % ${#threads[@]}]}# Logger name (fixed)logger="ru.rtlabs.einfahrt.query.server.http.request.rquery.RQueryCaExecutorImpl"request_id=$(uuidgen)message="Результат исполнения запроса $request_id получен полностью"context="default"created_time=$(TZ="Europe/Moscow" date "+%Y-%m-%dT%H:%M:%S.%3N%:z")# Build MDC JSONmdc_json="\"mdc\":{"mdc_json+="\"requestId\":\"$request_id\","mdc_json+="\"created\":\"$created_time\""mdc_json+="}"# Construct single-line JSONprintf '{"@timestamp":"%s","level":"%s","thread":"%s","logger":"%s","message":"%s","context":"%s",%s}\n' \"$timestamp" \"$level" \"$thread" \"$logger" \"$message" \"$context" \"$mdc_json"}# Handle Ctrl+Ctrap 'echo -e "\nLogging stopped. Output: $LOG_FILE"; exit' SIGINTecho "Logging to $LOG_FILE - Press CTRL+C to stop"while true; dogenerate_log >> "$LOG_FILE"sleep 1doneПоследние строки кода запускают генератор логов в бесконечном цикле — чтобы остановить генератор, нажмите CTRL + C. Вы можете изменить это поведение генератора — например, чтобы задать генерацию логов в течение 1 минуты, замените строки:
while true; dogenerate_log >> "$LOG_FILE"sleep 1doneна строки:
count=0while [ $count -lt 60 ]; dogenerate_log >> "$LOG_FILE"((count++))sleep 1doneНазначьте файл log_producer.sh исполняемым:
sudo chmod +x /usr/local/bin/log_producer/log_producer.shЗапустите генератор логов:
sudo /usr/local/bin/log_producer/log_producer.shГенератор можно запустить в фоновом режиме, добавив к команде знак & — так вы сможете продолжать работать в этой же консоли, не открывая новую для последующих процессов.
sudo /usr/local/bin/log_producer/log_producer.sh &
После запуска генератор начнет создавать лог-файл /usr/local/bin/log_producer/error_log.log.
Чтобы остановить работу log_producer.sh, нажмите CTRL + C.
Шаг 4. Запуск Fluent Bit для сбора логов
Перед первым запуском fluent-bit в режиме сервиса нужно проверить, нет ли ошибок доступа и корректно ли заполнены файлы настроек. Для этого проверьте работу fluent-bit в следующем порядке:
Запустите fluent-bit в консольном режиме.
Запустите fluent-bit в режиме сервиса.
В дальнейшем вы сможете использовать любой из этих способов.
Запуск в консольном режиме
Запустите fluent-bit в консоли:
sudo /opt/fluent-bit/bin/fluent-bit -c /etc/fluent-bit/fluent-bit.conf
Чтобы завершить работу fluent-bit, нажмите CTRL + C.
Запуск в режиме сервиса
Запустите fluent-bit для сбора логов как сервис:
sudo systemctl start fluent-bit
Если сервис был запущен ранее, его можно перезапустить, чтобы применились изменения конфигурации:
sudo systemctl restart fluent-bit
Шаг 5. Просмотр логов
Через несколько секунд после отправки логи появятся в сервисе Клиентского логирования.
Вы можете посмотреть логи в лог-группах. Логи можно отфильтровать с помощью языка фильтрующих выражений и выгрузить как файл.
В режиме tail сбор логов в fluent-bit работает по принципу отслеживания новых записей в логах. При перезапуске сервиса данные, обработанные ранее, не отправляются в систему повторно. Чтобы данные непрерывно поступали в сервис, выберите подходящий сценарий:
запустите генератор логов в бесконечном цикле, чтобы поддерживать постоянное поступление данных;
выполняйте генерацию логов пакетами — запускайте скрипт многократно с необходимым интервалом.
Это позволяет исключить дублирование записей и поддерживать актуальность передаваемых данных.
После окончания работы
Если виртуальная машина и ее логи стали неактуальными, вы можете удалить их:
- Перед началом работы
- Шаг 1. Установка Fluent Bit
- Шаг 2. Настройка Fluent Bit
- Шаг 3. Проверка отправки логов
- Шаг 4. Запуск Fluent Bit для сбора логов
- Шаг 5. Просмотр логов
- После окончания работы