Архив рассылок сообщества снова открыт. Читать письма 2003-2009
Главная/Блог/Эксплуатация
Эксплуатация15 мин чтенияНиколай Ковалёв

Логирование в продакшене: journalctl, ELK и когда что использовать

journald и journalctl, ротация, JSON-логи с request_id, централизованный сбор и выбор между ELK, OpenSearch и Loki.

Серверная стойка, из которой потоки данных сходятся в центральный узел сбора

Коротко: на одном-двух серверах логам хватает journald: включите постоянное хранение, ограничьте объём и научитесь фильтровать journalctl по юниту, приоритету и времени. Когда серверов становится больше трёх или логи нужны разработчикам без SSH, переходите на централизованный сбор — Loki для компактной установки, ELK или OpenSearch для полнотекстового поиска и аналитики. Формат логов приложения (JSON, request_id) важнее выбора стека.

Логи вспоминают, когда что-то уже сломалось: сервис молча перезапускается, клиенты жалуются на ошибку 502, а в /var/log пусто или, наоборот, 40 ГБ мусора. Особенно больно это бьёт по API: один запрос проходит через балансировщик, приложение, очередь и базу, и без сквозного идентификатора связать эти записи невозможно. Если бэкенд вам пишет подрядчик — например, вы заказываете разработку API на заказ, — договоритесь о формате логов, уровнях и request_id до первого релиза, а не после первой аварии: переделывать логирование в живом сервисе в разы дороже.

Ниже — практическая схема от одного VPS до парка серверов: что умеет journald, как не забить диск, как писать логи приложения, чтобы их можно было собрать, и по каким признакам понять, что пора ставить ELK или Loki. Команды проверены на Debian 12 и Ubuntu 24.04 с systemd 252–255.

Как устроено логирование в современном Linux?

В дистрибутивах с systemd все логи сначала попадают в journald — демон systemd-journald. Он собирает:

  • сообщения ядра (то, что раньше читали через dmesg);
  • stdout и stderr всех сервисов, запущенных как юниты systemd;
  • сообщения, отправленные через классический syslog();
  • структурированные записи через нативный протокол journald.

Каждая запись хранится вместе с метаданными: имя юнита, PID, UID, приоритет, идентификатор загрузки, имя хоста. Благодаря этому фильтр «ошибки nginx за последний час» отрабатывает мгновенно, без grep по гигабайтам.

Параллельно на многих серверах работает rsyslog: он забирает записи у journald и раскладывает их по текстовым файлам — /var/log/syslog, /var/log/auth.log и так далее. В Debian 12 rsyslog по умолчанию уже не ставится, в Ubuntu 24.04 он есть. Третий источник — файлы, которые приложения пишут сами: /var/log/nginx/access.log, логи PostgreSQL, логи Java-приложений.

Отсюда первое правило продакшена: знайте, куда пишет каждый ваш сервис. Половина «пропавших» логов на деле лежит в файле, о котором никто не помнил.

journalctl: команды, которые нужны каждый день

Базовый набор покрывает 90 % диагностики. Запомните фильтры — они комбинируются между собой.

# логи конкретного сервиса, свежие внизу
journalctl -u nginx.service

# следить в реальном времени (аналог tail -f)
journalctl -u myapp -f

# только ошибки и хуже за последний час
journalctl -p err --since "1 hour ago"

# окно по времени
journalctl --since "2026-10-27 09:00" --until "2026-10-27 09:30"

# текущая и предыдущая загрузка
journalctl -b
journalctl -b -1

# сообщения ядра (OOM killer, ошибки дисков)
journalctl -k -p warning

# несколько сервисов сразу
journalctl -u myapp -u postgresql --since today

# последние 200 строк без пейджера, для скриптов
journalctl -u myapp -n 200 --no-pager

Уровни приоритета — те же, что в syslog. Фильтр -p показывает указанный уровень и всё, что серьёзнее.

КодИмяЧто означает на практике
0–2emerg, alert, critСистема или сервис неработоспособны, нужен человек
3errОшибка операции: запрос упал, соединение не установлено
4warningЧто-то идёт не так, но сервис работает
5–6notice, infoШтатные события: старт, стоп, обработанный запрос
7debugПодробности для разработчика, в проде обычно выключено

Поиск по полям и вывод в JSON

Любое поле записи можно использовать как фильтр. Посмотреть все поля одной записи помогает -o verbose, а -o json отдаёт поток, который удобно разбирать jq:

# все записи процесса с конкретным PID
journalctl _PID=12345

# записи, отправленные конкретным бинарником
journalctl _EXE=/usr/sbin/sshd

# поиск по тексту сообщения (systemd 237+)
journalctl -u myapp -g "timeout|connection refused"

# JSON-поток для разбора
journalctl -u myapp -o json --since today | jq -r '.MESSAGE' | sort | uniq -c | sort -rn | head

Последняя команда — быстрый способ найти самое частое сообщение за день. На одном сервере это заменяет половину функций Kibana.

Как настроить journald, чтобы логи не терялись и не забивали диск?

Две типовые проблемы журнала противоположны: он исчезает после перезагрузки или съедает весь диск. Обе решаются в /etc/systemd/journald.conf (полный список опций — в man journald.conf) (лучше — отдельным файлом в /etc/systemd/journald.conf.d/, чтобы пакетный менеджер не перезаписал правку):

# /etc/systemd/journald.conf.d/prod.conf
[Journal]
Storage=persistent
SystemMaxUse=1G
SystemKeepFree=2G
MaxRetentionSec=1month
Compress=yes
RateLimitIntervalSec=30s
RateLimitBurst=10000
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
journalctl --disk-usage
  • Storage=persistent — журнал пишется в /var/log/journal и переживает перезагрузку. Без этого после падения сервера вы не увидите, что было перед ним.
  • SystemMaxUse и SystemKeepFree — верхний предел журнала и минимум свободного места, который journald обязан оставить. Срабатывает то ограничение, которое строже.
  • MaxRetentionSec — максимальный срок хранения. Удобно, когда есть требование «хранить месяц и не дольше».
  • RateLimit* — защита от сервиса, который в цикле пишет тысячи строк в секунду. Если видите в журнале «Suppressed N messages», лимит сработал: чините сервис, а не поднимайте лимит бесконечно.

Файлы в /var/log, которые пишет rsyslog или само приложение, journald не касаются — их ротирует logrotate. Проверьте, что для логов вашего приложения есть конфиг в /etc/logrotate.d/, иначе через полгода один app.log займёт весь раздел:

/var/log/myapp/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}

Ещё проще — не писать в файлы вовсе. Приложение, запущенное как юнит systemd или в контейнере, должно писать в stdout и stderr; ротацию и хранение берёт на себя journald или Docker.

Как писать логи приложения, чтобы их можно было собрать?

Хорошие логи начинаются в коде, а не в стеке хранения. Даже лучший ELK бесполезен, если приложение пишет «Error!» без контекста. Пять правил, которые стоит закрепить в команде:

  1. Структурированный формат. Одна запись — одна строка JSON с постоянным набором полей: ts, level, service, msg, плюс контекст. Сборщику не придётся разбирать текст регулярками, которые ломаются от каждой правки сообщения.
  2. Сквозной request_id. Балансировщик или первый сервис генерирует идентификатор запроса и передаёт его заголовком дальше; каждый сервис пишет его в каждую запись. Тогда один запрос ищется одним фильтром по всем системам.
  3. Осмысленные уровни. error — только то, на что нужно реагировать. Если в проде тысячи «ошибок» в час, настоящую никто не заметит.
  4. Никаких секретов и персональных данных. Токены, пароли, полные номера карт и телефонов маскируются до вывода.
  5. Время в UTC и ISO 8601. Иначе при сопоставлении логов из разных часовых поясов полчаса уходят на арифметику.

Так выглядит запись, с которой удобно работать и глазами, и машиной:

{"ts":"2026-10-27T06:41:12.381Z","level":"error","service":"orders-api","request_id":"9f2c1a7e","route":"POST /v1/orders","status":502,"duration_ms":3004,"msg":"payment gateway timeout"}

Для nginx аналогичный JSON-формат access-лога задаётся директивой log_format с параметром escape=json, а request_id берётся из встроенной переменной $request_id и передаётся бэкенду через proxy_set_header X-Request-ID $request_id;.

Когда journalctl уже недостаточно?

Локальные логи перестают работать не из-за объёма, а из-за организации процесса. Признаки, что пора собирать логи централизованно:

  • серверов больше трёх, и поиск одной ошибки — это SSH по очереди на каждый;
  • сервис масштабируется горизонтально, и запрос мог попасть на любую из реплик;
  • логи нужны разработчикам или поддержке, которым не стоит давать доступ к серверам;
  • нужны алерты по содержимому логов: «больше 50 ошибок оплаты за 5 минут»;
  • хранить логи нужно дольше, чем позволяет диск сервера, или есть требование аудита;
  • контейнеры живут минуты, и их логи исчезают вместе с ними.

Промежуточный шаг, который часто забывают, — пересылка без отдельного стека. systemd-journal-upload и systemd-journal-remote собирают журналы нескольких машин на одном сервере, где их читает тот же journalctl через -D /var/log/journal/remote. Rsyslog умеет то же по протоколу syslog. Для двух-пяти серверов этого бывает достаточно.

ELK, OpenSearch или Loki: что выбрать?

ELK — это Elasticsearch (хранение и поиск), Logstash (обработка) и Kibana (интерфейс). Сегодня вместо Logstash чаще ставят лёгкие сборщики — Filebeat, Fluent Bit или Vector. Сильная сторона ELK — полнотекстовый поиск по любому слову в любом поле и мощная аналитика. Цена — память и диск: Elasticsearch индексирует весь текст, работает на JVM, и даже одноузловой установке для комфортной работы нужно несколько гигабайт оперативной памяти.

OpenSearch — форк Elasticsearch и Kibana, который развивает OpenSearch Software Foundation под Linux Foundation, лицензия Apache 2.0. По возможностям и требованиям к железу близок к ELK; его выбирают, когда важна полностью открытая лицензия.

Grafana Loki устроен иначе: он индексирует только метки (хост, сервис, уровень), а сам текст хранит сжатыми кусками. Поиск по тексту — это фильтр по уже отобранному потоку. Поэтому Loki требует в разы меньше ресурсов, легко хранит данные в объектном хранилище и показывает логи в той же Grafana, где у вас метрики. Сборщик — Grafana Alloy (Promtail переведён в режим поддержки) или Vector.

Критерийjournald + rsyslogLoki + GrafanaELK / OpenSearch
Масштаб1–3 сервераОт нескольких серверов до сотенДесятки и сотни серверов, большие объёмы
ПоискФильтры по полям, grepМетки + фильтр по тексту (LogQL)Полнотекстовый по всем полям
РесурсыПочти нольНизкие: сотни МБ памяти на стартеВысокие: гигабайты памяти, быстрые диски
СопровождениеВходит в системуОдин бинарник или контейнерКластер, шардирование, обновления
Алерты по логамТолько скриптамиВстроенные правила + AlertmanagerВстроенные (часть функций платная у Elastic)
Когда братьСтарт, один VPSНужен общий поиск и алерты без отдельного админаСложная аналитика, аудит, SIEM-сценарии

Практическое правило: если у вас уже стоят Prometheus и Grafana (как в нашей статье про мониторинг Linux-сервера), начинайте с Loki — метрики и логи окажутся на одной панели, и от графика всплеска ошибок можно перейти к самим записям за то же время. ELK оправдан, когда без полнотекстового поиска и аналитики не обойтись и в команде есть человек, который будет сопровождать кластер.

Минимальный Loki на одном сервере

Для знакомства хватит Docker Compose: Loki хранит данные на локальном диске, Alloy читает journald и отправляет записи в Loki, Grafana показывает результат.

# docker-compose.yml (фрагмент)
services:
  loki:
    image: grafana/loki:3.5.0
    command: -config.file=/etc/loki/local-config.yaml
    ports: ["127.0.0.1:3100:3100"]
    volumes: ["loki-data:/loki"]
  alloy:
    image: grafana/alloy:latest
    volumes:
      - ./config.alloy:/etc/alloy/config.alloy
      - /var/log/journal:/var/log/journal:ro
      - /etc/machine-id:/etc/machine-id:ro
    command: run /etc/alloy/config.alloy
volumes:
  loki-data: {}
// config.alloy
loki.source.journal "system" {
  forward_to = [loki.write.local.receiver]
  labels     = {job = "systemd-journal", host = "web-1"}
}

loki.write "local" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

Порт Loki открыт только на 127.0.0.1 — не выставляйте его в интернет без аутентификации. Срок хранения задаётся параметром retention_period в конфиге Loki вместе с включённым compactor; без этого данные копятся бесконечно. Точные версии образов сверяйте с официальной документацией Loki.

Сколько хранить логи и сколько это стоит по диску?

Срок хранения определяют три вещи: как далеко назад вы реально ищете при разборе инцидентов, требования закона или договора и бюджет на диск. Для большинства веб-проектов работает такая схема:

Тип логовТипичный срокКомментарий
Отладочные и info приложения7–14 днейНужны для разбора свежих инцидентов
Ошибки и предупреждения30–90 днейПозволяют увидеть повторяющиеся проблемы
Access-логи веб-сервера30–90 днейРазбор атак, аналитика нагрузки
Аудит: входы, sudo, изменения правОт 6 месяцев до года и большеРасследование инцидентов безопасности, требования аудиторов

Чтобы оценить объём, посмотрите, сколько журнал набирает за сутки: journalctl --disk-usage сегодня и завтра. Сжатие у journald, Loki и Elasticsearch разное, но порядок такой: текстовые логи сжимаются в 5–10 раз, а полнотекстовый индекс Elasticsearch, наоборот, может занимать сопоставимо с исходными данными. Отсюда и разница в стоимости хранения.

Чек-лист логирования для продакшена

  1. journald в режиме Storage=persistent, заданы SystemMaxUse и срок хранения.
  2. Для каждого файла в /var/log есть правило logrotate, или приложение пишет в stdout.
  3. Логи приложения в JSON, с полями времени, уровня, сервиса и request_id.
  4. Уровень error означает «нужна реакция», а не «что-то произошло».
  5. Пароли, токены и персональные данные маскируются в коде.
  6. При трёх и более серверах логи собираются централизованно: journal-remote, Loki или ELK.
  7. Есть алерт хотя бы на всплеск ошибок и на заполнение диска.
  8. Команда знает, где смотреть логи и как найти запрос по request_id, — это записано, а не живёт в голове одного админа.

Частые вопросы

Чем journalctl отличается от файлов в /var/log?

journald хранит записи в бинарном индексированном журнале с метаданными: юнит, PID, приоритет, загрузка. Поэтому фильтр по сервису и времени работает мгновенно. Файлы в /var/log пишет rsyslog или само приложение, их удобно читать grep и less. На многих дистрибутивах работают оба механизма одновременно, и одна запись попадает в оба места.

Почему после перезагрузки journalctl показывает только текущую загрузку?

Журнал хранится в /run/log/journal, то есть в оперативной памяти. Создайте каталог /var/log/journal или пропишите Storage=persistent в /etc/systemd/journald.conf и перезапустите systemd-journald — после этого журнал переживает перезагрузку и доступен через journalctl -b -1.

Сколько места выделять под журнал?

По умолчанию journald занимает до 10 % файловой системы, но не больше 4 ГБ. Для небольшого VPS разумно задать SystemMaxUse=1G и MaxRetentionSec=1month. Текущий объём показывает journalctl --disk-usage, очистка вручную — journalctl --vacuum-size=500M.

Когда пора переходить с journalctl на ELK или Loki?

Когда серверов больше двух-трёх и одну ошибку приходится искать, заходя по SSH на каждый; когда логи нужны разработчикам без доступа к серверам; когда требуются алерты по тексту логов и хранение дольше, чем позволяет диск сервера. До этого момента хватает journald и rsyslog.

ELK или Loki — что выбрать для небольшого проекта?

Для нескольких серверов чаще всего проще Loki: он индексирует только метки, а не весь текст, поэтому требует в разы меньше памяти и диска и встраивается в уже знакомую Grafana. ELK или OpenSearch оправданы, когда нужен полнотекстовый поиск по большим объёмам, сложная аналитика и есть человек, готовый сопровождать кластер.

Нужно ли писать логи приложения в JSON?

Да, если логи будут собираться централизованно. JSON с постоянными полями (время, уровень, сервис, request_id, сообщение) разбирается сборщиком без хрупких регулярных выражений. Для чтения глазами journalctl -o cat и jq делают такие строки вполне удобными.

Можно ли писать в логи пароли и персональные данные?

Нет. Логи читает больше людей, чем базу, они копируются в бэкапы и сторонние системы и хранятся дольше. Маскируйте токены, пароли, номера карт и телефоны ещё в приложении, до вывода в stdout. Для персональных данных в России это ещё и требование 152-ФЗ.

Вернуться в блог