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, остальные может подделать приложение.

Чек-лист

  1. Приложение пишет одну JSON-строку на событие.
  2. Указаны уровень, время, компонент и идентификатор запроса.
  3. В журнале настроены лимиты и постоянное хранение.
  4. Запросы проверены на пустом и невалидном потоке.
  5. Секреты и персональные данные не попадают в логи.
  6. Есть резервный путь: если JSON сломан, текст всё равно читается.

Итог

Связка journald и jq даёт быстрый способ превратить логи в данные без установки тяжёлой платформы. Начните с -o json и одного фильтра, затем добавьте статистику и проценты. Следите за объёмом, ограничивайте окно времени и не забывайте, что структурированный лог — это ещё и ответственность за безопасность содержимого.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте