Проблемы с которыми я столкнулся при написании рекурсивного парсера journal в Linux
- вторник, 23 декабря 2025 г. в 00:00:17
Добрый день, всем читающим данную статью. При анализе артефактов с Linux систем journal являются источником огромного количества полезной информации.
Из журналов можно достать:
название машины;
идентификатор события;
тип события;
критичность события;
сообщение;
объект породивший событие;
и многое другое.
Существует множество утилит которые позволяют парсить журналы:
встроенный во многие дистрибутивы linux journalctl;
кроссплатформенный go-journalctl от Velocidex;
библиотека go-systemd;
и т.д.
Однако, у всех этих утилит есть свои минусы. Так go-systemd и journalctl не имеют встроенной кроссплатформенности, а go-journalctl не может парсить все файлы находящиеся в директории рекурсивно.
По этой причине, у меня возникла идея написать свой кроссплатформенный парсер файлов journal, который мог бы обрабатывать не только файлы, но и директории, с возможностью экспорта в различные форматы и сортировки по временным меткам.
В процессе написания данного инструмента я столкнулся с несколькими проблемами и хочу поделиться способами, которыми я их решал.
Одна из самых простых проблем с которыми пришлось столкнуться - это рекурсивный обход директорий. Тут я видел 2 пути решения. Первый путь - рекурсивно вызывать функцию для директории. Второй путь (который я и выбрал) - очередь, а именно, 2 очереди - одна для директорий, вторая для файлов. Работает следующим образом:
на вход с консоли подаются изначальные директории и записываются в очередь директорий;
цикл берёт первую запись из очереди директорий и просматривает её содержимое;
если объект - файл, то он записывается в очередь для файлов, если объект - директория, то он записывается в конец очереди для директорий;
цикл повторяется пока очередь не опустеет.
Второй проблемой для меня стало нахождение кроссплатформенной библиотеки для парсинга файлов-журналов. Сначала я выбрал go-systemd, но в конце концов меня ждал неприятный сюрприз - она оказалась не кроссплатформенной. После этого была выбрана библиотека velocidex/go-journalctl.
Следующим шагом было решить как именно будет парситься файл. Для ускорения я сделал многопоточный парсинг файлов с выводом в канал. Как он работает:
открывается буфферизованный канал на запись;
запускается горутина для функции-читателя из канала;
цикл итерируется по очереди из файлов;
запускаются горутины под каждый файл, которые читают записи journal и пишут в канал;
цикл повторяется пока очередь не опустеет;
ожидается пока не закончатся все горутины-писатели;
закрывается канал;
ожидается пока закончится горутина-читатель.
Реализация сортировки зачастую является сложной задачей по оперативной памяти. Недолго думая, я принял решение воспользоваться объектом, в котором этот функционал уже встроен. Этот объект - реляционная база данных. Следующим шагом было - определиться какой именно базой данных пользоваться ведь их огромное множество:
PostgreSQL;
MySQL;
SQLite;
и многие другие.
Выбор был довольно очевиден, ведь написание docker-compose или чего-то подобного для простой консольной утилиты даже звучит комично, и этот выбор - файловая база данных SQLite.
В данной базе данных при запуске программы создаётся таблица записей и индекс по временной метке записи. А дальше дело техники функция-читатель из проблемы 2 пишет в базу данных, получая информацию из канала, однако, чтобы не писать по одной записи (потому что это довольно тяжело при большом количестве записей), делается не просто INSERT в базу данных, а batch INSERT (вставка сразу большого количества данных одним запросом).
Журналов в Linux огромное количество. Даже у меня на хостовой машине с недавно переустановленной ОС их уже набежало около 250тыс. Что уж говорить о количестве, возникающем при анализе нескольких старых рабочих серверов.
И тут возникает потребность не только оптимизации экспорта в работе программы, но и разбиения записей по файлам, ведь не все компьютеры смогут нормально работать с файлами на миллионы строк. Для этого в моей программе есть флаг partition, который как раз отвечает за разбиение. Если флаг не указан или равен 0, то из базы данных сформированной ранее, берутся сразу все записи отсортированные по времени и экспортируются в необходимый формат. Однако, если флаг больше 0, например 10тыс., то записи экспортируются через цикл, который итерируется по всем ранее добавленным записям порционно. Порции в данном случае равны partition. Экспорт в необходимый формат происходит по тем же порциям по которым происходит итерация.
В данном материале, я поделился своим кейсом решения некоторых довольно часто возникающих проблем.
Надеюсь мои решения некоторых представленных проблем показались вам довольно интересными. Также, надеюсь, что написанная мной утилита для рекурсивного парсинга журналов поможет вам более удобно анализировать артефакты в Linux.
Всем спасибо за прочтение!
P.S. Написал легковесный EDR-клиент для linux-серверов с полной интеграцией с Телеграм (@light_defender_bot, сайт ), в данный момент проходит тестирование. Буду рад фидбеку.