PunchUNILORIN student wins NCDMB essay contest, gets N1mESPNWhat's next for the Indiana Fever after front office departures?ESPN DeportesGurú de las Diagonales: Eagles están oficialmente en alertaInquirerMilitary war games lunched in Dinagat IslandsRTP DesportoRui Borges e Debast. Treinador confirma que vai falhar próximos dois jogosThe Jerusalem PostMerz warns that Germany faces pressure from extremist parties, 'massive threat' from RussiaBollywood HungamaEXCLUSIVE: Jennifer Winget, Sapna Pabbi and Vipin Sharma join Arjun Rampal in Hansal Mehta–Robbie Grewal’s BillionaireZDF heuteAktuelle Pressemitteilungen des ZDFFrance 24Paris: Protest starts at Porte d'Italie, after last-minute change to the routeThe Sydney Morning HeraldMultiple people shot in drive-by shooting south of BrisbanePremium TimesUnilever Nigeria posts 20% jump in nine-month profit, raises interim dividend fourfoldSRF News– Hitlers «Teehaus» am Obersalzberg niedergebrannt
The Daily Newsstand · Free, Always
Saturday, October 24, 2026

Turbo Vision спустя 36 лет: нормальный интерфейс для journald

Translate

journald умеет почти всё, что нужно для разбора проблемы на сервере: индексирует поля, фильтрует по юниту, процессу, пользователю и загрузке, ищет по времени и отдаёт записи по курсору. А смотрим мы его через journalctl, то есть одной командой за раз. Эта статья — о том, как оконный интерфейс из 1990 года оказался почти идеальной формой для этих возможностей, и о том, как устроен получившийся просмотрщик TurboJedi.

Откуда взялось

Мои демоны на C пишут журнал через ReportTools — небольшую библиотеку с подключаемыми обработчиками. Каждая запись уходит во все установленные обработчики: в консоль, в системный журнал, куда угодно ещё.

TurboReport начинался как ещё один такой обработчик. Он занимает место консольного и вместо потока строк поднимает внутри процесса экран на magiblot/tvision, современном порте Turbo Vision. Интерфейс живёт в собственном потоке, чтобы не мешать демону, а записи попадают к нему через тот же вызов, что и в консоль. Запускаешь демон с ключом — и получаешь окна с прокруткой, фильтром по уровням, паузой, выделением и копированием.

Когда этим начинаешь пользоваться, становится понятно, что ценность не в конкретном обработчике. Несколько окон с разными фильтрами, диалог уровней, меню, мышь и строка статуса оказались настолько удобными, что захотелось того же для всего системного журнала. Демонов, которые пишут в journald, на сервере гораздо больше, чем тех, что используют ReportTools.

Так появился TurboJedi — Turbo Journald Interface. Отдельная программа, которая ничего не знает про ReportTools и просто разговаривает с journald.

Чем неудобен journalctl

Сам journalctl хорош: -u, -t, -p, --since, --grep, FIELD=VALUE, курсоры. Неудобство в другом — в том, как устроена работа с ним при разборе инцидента:

  • Один запрос — один вид. Поменять фильтр значит выйти из less и набрать команду заново, потеряв место, где ты был.

  • Корреляция — это tmux. Посмотреть, что делали nginx и база в ту же секунду, можно либо одним потоком (-u nginx -u postgresql, и тогда строки перемешаны), либо в нескольких терминалах, каждый со своей прокруткой.

  • Следить и листать одновременно неудобно. -f следит за хвостом, но в нём нет навигации по истории; в пейджере есть навигация, но слежение там приходится включать вручную.

  • Значения фильтров надо знать заранее. Как точно называется юнит, какой PID был у упавшего процесса, какой boot ID у позавчерашней загрузки — всё это отдельные команды.

Ни одна из этих проблем не требует нового хранилища или нового языка запросов. Не хватает интерфейса, который держит несколько запросов открытыми одновременно.

Почему Turbo Vision

Turbo Vision появился в 1990 году вместе с Turbo Pascal 6.0 и решал ту же задачу, что и сейчас: дать текстовому экрану полноценный оконный интерфейс. Окна с рамкой, номером и заголовком, диалоги с полями и галками, выпадающие меню с горячими клавишами, строка статуса с подсказками, мышь.

Для просмотра журнала это ложится один в один:

  • окно — один запрос к журналу со своим фильтром и своей прокруткой;

  • диалог — изменение фильтра без выхода из программы и повторного набора команды;

  • меню и строка статуса — все возможности видны, ничего не надо помнить;

  • плитка окон (F9) — несколько источников рядом, а не в разных терминалах.

magiblot/tvision приносит в эту модель всё, что нужно сегодня: 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, 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 package собирает deb-пакет, а make package DISTRIBUTION=arm64 — то же самое кросс-компилятором. Версия берётся из git describe, а changelog пакета — из тегов и коммитов.

Читать весь журнал может root или участник группы systemd-journal. Остальные видят только свои записи.

Чего в TurboJedi нет

Каждая из этих функций обсуждалась и была отброшена:

  • Закладки. Журнал — поток, а не документ. Несколько окон, синхронизация по времени и возврат назад закрывают тот же сценарий естественнее.

  • Экспорт в JSON. Карточка записи копируется целиком, а для машинной обработки есть journalctl -o json с теми же фильтрами.

  • Свой язык запросов. Фильтры journald уже выражают всё нужное, и диалоги просто дают к ним доступ.

Мерило, которого я старался держаться: инструмент должен оставаться маленьким, а всё, что он умеет, должно быть видно из меню.

Итог

Самое интересное в этой истории — не ностальгия по DOS. Модель «окно — запрос, диалог — фильтр, меню — все возможности на виду» оказалась удачной для задачи, которой в 1990 году не существовало: держать перед глазами несколько срезов системного журнала и переключаться между ними, не теряя места.

Оказалось, что для удобной работы с journald не нужно заново изобретать ни систему журналирования, ни оконный интерфейс. Обе уже существуют, оставалось только их соединить.

TurboJedi распространяется под MIT: github.com/cyanide-burnout/TurboJedi.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.