- PVSM.RU - https://www.pvsm.ru -
journald умеет почти всё, что нужно для разбора проблемы на сервере: индексирует поля, фильтрует по юниту, процессу, пользователю и загрузке, ищет по времени и отдаёт записи по курсору. А смотрим мы его через journalctl, то есть одной командой за раз. Эта статья — о том, как оконный интерфейс из 1990 года оказался почти идеальной формой для этих возможностей, и о том, как устроен получившийся просмотрщик TurboJedi [1].
Мои демоны на C пишут журнал через ReportTools — небольшую библиотеку с подключаемыми обработчиками. Каждая запись уходит во все установленные обработчики: в консоль, в системный журнал, куда угодно ещё.
TurboReport начинался как ещё один такой обработчик. Он занимает место консольного и вместо потока строк поднимает внутри процесса экран на magiblot/tvision [2], современном порте Turbo Vision. Интерфейс живёт в собственном потоке, чтобы не мешать демону, а записи попадают к нему через тот же вызов, что и в консоль. Запускаешь демон с ключом — и получаешь окна с прокруткой, фильтром по уровням, паузой, выделением и копированием.
Когда этим начинаешь пользоваться, становится понятно, что ценность не в конкретном обработчике. Несколько окон с разными фильтрами, диалог уровней, меню, мышь и строка статуса оказались настолько удобными, что захотелось того же для всего системного журнала. Демонов, которые пишут в journald, на сервере гораздо больше, чем тех, что используют ReportTools.
Так появился TurboJedi — Turbo Journald Interface. Отдельная программа, которая ничего не знает про ReportTools и просто разговаривает с journald.
Сам journalctl хорош: -u, -t, -p, --since, --grep, FIELD=VALUE, курсоры. Неудобство в другом — в том, как устроена работа с ним при разборе инцидента:
Один запрос — один вид. Поменять фильтр значит выйти из less и набрать команду заново, потеряв место, где ты был.
Корреляция — это tmux. Посмотреть, что делали nginx и база в ту же секунду, можно либо одним потоком (-u nginx -u postgresql, и тогда строки перемешаны), либо в нескольких терминалах, каждый со своей прокруткой.
Следить и листать одновременно неудобно. -f следит за хвостом, но в нём нет навигации по истории; в пейджере есть навигация, но слежение там приходится включать вручную.
Значения фильтров надо знать заранее. Как точно называется юнит, какой PID был у упавшего процесса, какой boot ID у позавчерашней загрузки — всё это отдельные команды.
Ни одна из этих проблем не требует нового хранилища или нового языка запросов. Не хватает интерфейса, который держит несколько запросов открытыми одновременно.
Turbo Vision появился в 1990 году вместе с Turbo Pascal 6.0 и решал ту же задачу, что и сейчас: дать текстовому экрану полноценный оконный интерфейс. Окна с рамкой, номером и заголовком, диалоги с полями и галками, выпадающие меню с горячими клавишами, строка статуса с подсказками, мышь.
Для просмотра журнала это ложится один в один:
окно — один запрос к журналу со своим фильтром и своей прокруткой;
диалог — изменение фильтра без выхода из программы и повторного набора команды;
меню и строка статуса — все возможности видны, ничего не надо помнить;
плитка окон (F9) — несколько источников рядом, а не в разных терминалах.
magiblot/tvision [2] приносит в эту модель всё, что нужно сегодня: UTF-8, 24-битный цвет, мышь в современных терминалах, буфер обмена через OSC 52. Последнее важно на сервере: Ctrl‑Ins копирует строки в буфер обмена рабочей машины прямо через SSH.
При этом библиотека маленькая, собирается за минуту и не тянет за собой ничего, кроме ncurses. Программа целиком — около 6000 строк C++: 22 модуля, каждый со своим заголовком.
Главное решение: окно не фильтрует общий поток, а само открывает журнал (sd_journal_open) и задаёт фильтр запросом. Окно с ошибками nginx читает только ошибки nginx, сколько бы ни писали остальные.
Источник, уровни и PID превращаются в матчи journald (фрагмент упрощён):
// Matches of the same field are joined by OR, of different fields by AND
for (field = 0; field < JOURNAL_FIELD_COUNT; field ++)
if (!filter.values[field].empty())
{
snprintf(match, sizeof(match), "%s=%s", JournalFieldNames[field], value.c_str());
sd_journal_add_match(*journal, match, 0);
}
if (filter.levels != 0xff)
{
// No level at all leaves a match nothing can satisfy
sd_journal_add_match(*journal, "PRIORITY=-", 0);
for (level = 0; level < 8; level ++)
if (filter.levels & (1 << level))
{
snprintf(match, sizeof(match), "PRIORITY=%d", level);
sd_journal_add_match(*journal, match, 0);
}
}
Совпадения одного поля journald объединяет через OR, разных полей — через AND. Это ровно та семантика, которая нужна диалогу источника: юнит и пользователь, а внутри уровней — ошибки или предупреждения.
Матчи libsystemd сравнивают поле целиком, поэтому текст сообщения в запрос не передать. journalctl --grep тоже не отправляет его в journald: он читает записи и проверяет MESSAGE у себя. TurboJedi делает то же самое. Текст, обычный или регулярное выражение PCRE2, на показ или на исключение, проверяется у прочитанных записей, которые уже прошли матчи.
Диалог источника (F7) содержит юнит, идентификатор, команду, транспорт, загрузку, пользователя и PID. У каждого поля есть кнопка «List…», которая спрашивает у журнала уникальные значения (sd_journal_query_unique) и показывает их списком со строкой фильтра. Загрузки подписаны временем первой записи, пользователи — именем и UID. В списке процессов по умолчанию стоит галка «Running only»: процесс считается живым, только если его PID есть в /proc и процесс стартовал не позже последней записи. Иначе PID уже достался кому‑то другому.
Время (Ctrl‑T) принимает то же, что --since: 2026-10-23 19:00, yesterday, -1h. Для выбора есть календарь.
Окно следит за хвостом через sd_journal_get_fd и sd_journal_process в цикле простоя приложения. Новые записи читаются пачками по 5000, чтобы длинный диапазон не замораживал экран.
Прокрутка выше первой строки подгружает историю. Делает это отдельный экземпляр журнала: он встаёт по курсору первой строки (sd_journal_seek_cursor) и идёт назад, а основной экземпляр в это время продолжает следить за хвостом. Строки нумеруются абсолютно: подгруженная история уменьшает номер первой строки, а вытесненные лимитом буфера строки его увеличивают. Поэтому выделение, поиск и возврат назад не ломаются ни от подгрузки, ни от обрезки.
Это функция, ради которой стоило всё затевать. Нашёл ошибку в одном окне — нажимаешь Alt‑G, и все остальные окна переходят к ближайшей по времени строке, сохраняя свои фильтры. Если строки в буфере нет, окно догружает историю, пока не дойдёт до нужного момента.
Реализация укладывается в несколько строк: окно рассылает широковещательное сообщение с временем выделенной строки, остальные ищут ближайшую строку по __REALTIME_TIMESTAMP. Ctrl‑G делает то же по введённому времени, а Alt‑← возвращает окно туда, где оно было до прыжка.
Как это выглядит в работе. Слева окно сервиса, справа окно ядра (-k или транспорт kernel в диалоге источника). В окне сервиса видна строка worker 3 exited unexpectedly with status 137. Код 137 означает, что процесс убит сигналом 9, но кем — из этой строки не понять. Выделяешь её, нажимаешь Alt‑G, и окно ядра встаёт на ту же секунду: если рядом окажется сообщение OOM killer о том же PID, причина найдена. Если нет — Alt‑← возвращает окно ядра туда, где оно было, и можно открыть следующий источник.
Enter на строке открывает все поля записи: курсор позволяет прочитать её целиком, даже если окно показывает три колонки. Enter на поле в карточке открывает новое окно с фильтром текущего плюс этим полем. Видишь _SYSTEMD_UNIT= или _PID= у подозрительной строки — одно нажатие, и рядом окно только с этим источником.
Все окна с положением, фильтрами, колонками и правилами подсветки сохраняются в INI, по секции на окно:
[window]
bounds = 0 0 120 28
unit = nginx.service
text = timeout
regex = yes
levels = EACRW---
highlight = conf.log
columns = date,time,level,identifier,pid,millisecond
При выходе раскладка сохраняется автоматически и открывается при следующем запуске. Удобно держать файл под конкретный сервис: turbojedi -c web.jedi, и сразу открыты фронтенд, бэкенд и база.
Подсвечивать IP‑адреса, UUID, пути и слова вроде failed хотелось, а придумывать для этого свой формат правил — нет. Готовый словарь уже есть: grc [3], generic colouriser. Его правила — это regexp=, colours= для совпадения и групп и count=once|more|stop. В Debian пакет grc содержит десятки готовых наборов.
TurboJedi читает эти файлы как есть. Регулярные выражения grc написаны для Python re, поэтому вместо std::regex пришлось взять PCRE2: в ECMAScript‑диалекте std::regex нет lookbehind, а PCRE2 вдобавок компилирует выражения JIT‑ом. Проверка показала, что все 722 выражения из пакета grc компилируются в PCRE2 без изменений.
Встроенный набор записан в том же формате и разбирается тем же кодом. Выбор набора — на окно (Ctrl‑L): встроенный, выключено или любой conf.*.
Несколько мест, где поведение оказалось не таким, как ждёшь.
seek_tail и next(). После sd_journal_seek_tail вызов sd_journal_next ничего не возвращает: курсор стоит после последней записи. Чтобы начать с последних N записей, нужен sd_journal_previous_skip(journal, N), и делать шаг назад нужно даже при N = 0.
Время жизни данных. Указатель из sd_journal_get_data действителен только до следующего обращения к журналу. Поэтому MESSAGE читается последним, после всех остальных полей.
Пользователи. journald хранит _UID, а не имя. Фильтр по имени приходится переводить в номер через getpwnam, а несуществующее имя — в матч, которому ничего не соответствует, иначе окно молча покажет всё.
Пустой набор уровней. Если снять все галки уровней, ни одного матча PRIORITY= не добавится, и окно покажет всё. Отсюда PRIORITY=- в коде выше: матч, которому не соответствует ни одна запись.
Окно, которого ещё нет. В Turbo Vision полоса прокрутки сообщает о новом положении широковещательным сообщением владельцу. Окно журнала читало первые записи прямо в конструкторе, ещё до вставки в окно, и сообщение до него не доходило. Полоса прокрутки показывала конец, а окно — начало. Лечится одним вызовом scrollDraw(), который забирает положение у полосы прокрутки.
Слишком быстрый ввод. tvision считает быстро пришедшие символы вставкой из буфера обмена. На живом человеке это не проявляется, а в автоматических тестах через pty клавиши пришлось отправлять с паузами.
git clone --recurse-submodules https://github.com/cyanide-burnout/TurboJedi.git
cd TurboJedi
make
sudo make install
Нужны libsystemd-dev, libncurses-dev, libpcre2-dev и cmake для tvision. make deb собирает deb‑пакет, а make deb DISTRIBUTION=arm64 — то же самое кросс‑компилятором. Версия берётся из git describe, а changelog пакета — из тегов и коммитов.
Читать весь журнал может root или участник группы systemd-journal. Остальные видят только свои записи.
Каждая из этих функций обсуждалась и была отброшена:
Закладки. Журнал — поток, а не документ. Несколько окон, синхронизация по времени и возврат назад закрывают тот же сценарий естественнее.
Экспорт в JSON. Карточка записи копируется целиком, а для машинной обработки есть journalctl -o json с теми же фильтрами.
Свой язык запросов. Фильтры journald уже выражают всё нужное, и диалоги просто дают к ним доступ.
Мерило, которого я старался держаться: инструмент должен оставаться маленьким, а всё, что он умеет, должно быть видно из меню.
Самое интересное в этой истории — не ностальгия по DOS. Модель «окно — запрос, диалог — фильтр, меню — все возможности на виду» оказалась удачной для задачи, которой в 1990 году не существовало: держать перед глазами несколько срезов системного журнала и переключаться между ними, не теряя места.
Оказалось, что для удобной работы с journald не нужно заново изобретать ни систему журналирования, ни оконный интерфейс. Обе уже существуют, оставалось только их соединить.
TurboJedi распространяется под MIT: github.com/cyanide‑burnout/TurboJedi [1].
Автор: performer
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/systemd/460846
Ссылки в тексте:
[1] TurboJedi: https://github.com/cyanide-burnout/TurboJedi
[2] magiblot/tvision: https://github.com/magiblot/tvision
[3] grc: https://github.com/garabik/grc
[4] Источник: https://habr.com/ru/articles/1098894/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1098894
Нажмите здесь для печати.