journald умеет отдавать журнал не только в виде текста, но и в структурированном JSON. Это превращает просмотр логов в обработку данных: события можно фильтровать по полям, считать статистику, строить проценты и отправлять срез в другую систему. Если приложение пишет JSON в stdout, journald сохраняет строку в поле MESSAGE, а дальше её разбирает jq. Ниже — рабочие команды и важные ограничения.
Зачем логам JSON
Обычный текст удобен человеку, но плохо поддаётся автоматической обработке. Структурированная запись сразу содержит уровень, идентификатор запроса, маршрут и длительность — сводку можно получить без хрупких регулярных выражений.
- Фильтрация по уровню и компоненту без разбора строки.
- Подсчёт ошибок по маршрутам и типам исключений.
- Расчёт задержек и поиск выбросов.
- Передача среза в хранилище или систему алертов.
Формат journald
Флаг -o json печатает по одному объекту на строку. Служебные поля начинаются с подчёркивания: _PID, _COMM, _SYSTEMD_UNIT, _HOSTNAME. Пользовательские поля — MESSAGE, PRIORITY, SYSLOG_IDENTIFIER. Время хранится в микросекундах в __REALTIME_TIMESTAMP.
journalctl -o json -n 1 | jq .
journalctl -o json-pretty -u nginx -n 2
journalctl -o json --output-fields=MESSAGE,PRIORITY,SYSLOG_IDENTIFIER -n 5
json-pretty удобен для изучения структуры, но не для конвейера: объект занимает много строк. Для машинной обработки используйте компактный json и jq -c.
Как приложение пишет JSON
Ничего специального делать не нужно: достаточно выводить по одной JSON-строке в stdout. systemd захватывает поток и создаёт запись журнала. Важно, чтобы строка была валидным JSON и не содержала переводов строк внутри объекта.
import json, logging, sys
class JsonFormatter(logging.Formatter):
def format(self, record):
data = {
"level": record.levelname.lower(),
"logger": record.name,
"message": record.getMessage(),
}
data.update(getattr(record, "fields", {}))
return json.dumps(data, ensure_ascii=False)
log = logging.getLogger("billing")
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
log.addHandler(handler)
log.setLevel(logging.INFO)
log.info("invoice paid", extra={"fields": {"invoice_id": 17, "amount": 990}})
В журнале такая запись окажется в MESSAGE как строка. Проверить это можно командой journalctl -u billing -o json -n 1 | jq -r .MESSAGE. Настройте формат так, чтобы одно событие занимало ровно одну строку.
Базовые запросы jq
Сначала отфильтруйте записи по юниту и времени, затем извлеките поле. Конструкция fromjson? безопасно пропускает строки, которые не являются JSON.
journalctl -u billing --since "1 hour ago" -o json \
| jq -r 'select(.MESSAGE | startswith("{")) | .MESSAGE | fromjson | [.level, .logger, .message] | @tsv'
Результат удобно сразу отфильтровать по уровню. Учтите, что PRIORITY в journald — строка, а не число, поэтому сравнивать его нужно после tonumber.
journalctl -u billing --since today -o json \
| jq -r 'select((.PRIORITY | tonumber) <= 4) | [.SYSLOG_IDENTIFIER, (.MESSAGE | fromjson? | .message)] | @tsv'
Статистика по событиям
Когда записи разобраны, можно считать распределение. Например, сколько раз каждый уровень встречался за сутки и какие сообщения повторяются чаще всего.
journalctl -u billing --since today -o json \
| jq -r '.MESSAGE | fromjson? | .level' \
| sort | uniq -c | sort -rn
journalctl -u billing --since today -o json \
| jq -s 'map(.MESSAGE | fromjson?) | group_by(.route) | map({route: .[0].route, count: length}) | sort_by(-.count)'
Флаг -s собирает весь поток в массив. На больших объёмах это расходует память, поэтому сначала ограничьте окно времени или число записей.
Время и длительности
Метка __REALTIME_TIMESTAMP хранится в микросекундах. Делите на миллион и вызывайте todate, чтобы получить читаемое время. Для расчёта задержек извлекайте числовое поле и сортируйте массив.
journalctl -u billing -n 20 -o json \
| jq -r '[((.__REALTIME_TIMESTAMP | tonumber) / 1000000 | todate), .PRIORITY, (.MESSAGE | fromjson? | .message)] | @tsv'
journalctl -u billing --since "24 hours ago" -o json \
| jq -s '[.[] | .MESSAGE | fromjson? | select(.duration_ms != null) | .duration_ms] | sort as $v | {count: ($v | length), p50: $v[($v | length * 0.5 | floor)], p95: $v[($v | length * 0.95 | floor)]}'
Проценты считаются по позиции в отсортированном массиве. Оценка приблизительная, но для оперативной диагностики её достаточно. Если массив пуст, индексы вернут null.
Хранение и ротация журнала
По умолчанию journald может хранить журнал в памяти или на диске в зависимости от дистрибутива. Для сервера включите постоянное хранение и ограничьте размер, иначе диск закончится в самый неподходящий момент.
[Journal]
Storage=persistent
SystemMaxUse=2G
SystemMaxFileSize=200M
RateLimitIntervalSec=30s
RateLimitBurst=10000
ForwardToSyslog=no
После правки /etc/systemd/journald.conf перезапустите службу и проверьте занятое место. Очистка удаляет старые файлы, но не текущие записи.
sudo systemctl restart systemd-journald
journalctl --disk-usage
sudo journalctl --vacuum-size=1G
sudo journalctl --vacuum-time=14d
Живой поток и выгрузка
Для наблюдения в реальном времени используйте -f и небуферизованный вывод jq. Без --unbuffered результат может появляться рывками.
journalctl -u billing -f -o json \
| jq --unbuffered -r 'select((.PRIORITY | tonumber) <= 4) | [.SYSLOG_IDENTIFIER, (.MESSAGE | fromjson? | .message)] | @tsv'
journalctl -u billing --since "2026-01-01" -o json > billing.jsonl
jq -s 'length' billing.jsonl
Файл .jsonl содержит по объекту на строку. Его можно загрузить в аналитическую систему или разобрать повторно.
Ошибки и ограничения
- fromjson падает. Используйте
fromjson?и предварительныйselect. - Поле отсутствует. Разные записи имеют разный набор атрибутов; проверяйте его через
hasилиselect. - Секреты в логах. JSON не делает данные безопасными: пароли и токены нужно вырезать до записи.
- Огромные сообщения. Длинные строки замедляют обработку и могут обрезаться настройками journald.
- Доверие к полям. Поля с подчёркиванием заполняет journald, остальные может подделать приложение.
Чек-лист
- Приложение пишет одну JSON-строку на событие.
- Указаны уровень, время, компонент и идентификатор запроса.
- В журнале настроены лимиты и постоянное хранение.
- Запросы проверены на пустом и невалидном потоке.
- Секреты и персональные данные не попадают в логи.
- Есть резервный путь: если JSON сломан, текст всё равно читается.
Итог
Связка journald и jq даёт быстрый способ превратить логи в данные без установки тяжёлой платформы. Начните с -o json и одного фильтра, затем добавьте статистику и проценты. Следите за объёмом, ограничивайте окно времени и не забывайте, что структурированный лог — это ещё и ответственность за безопасность содержимого.