Логирование в продакшене: 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–2 | emerg, alert, crit | Система или сервис неработоспособны, нужен человек |
| 3 | err | Ошибка операции: запрос упал, соединение не установлено |
| 4 | warning | Что-то идёт не так, но сервис работает |
| 5–6 | notice, info | Штатные события: старт, стоп, обработанный запрос |
| 7 | debug | Подробности для разработчика, в проде обычно выключено |
Поиск по полям и вывод в 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!» без контекста. Пять правил, которые стоит закрепить в команде:
- Структурированный формат. Одна запись — одна строка JSON с постоянным набором полей:
ts,level,service,msg, плюс контекст. Сборщику не придётся разбирать текст регулярками, которые ломаются от каждой правки сообщения. - Сквозной request_id. Балансировщик или первый сервис генерирует идентификатор запроса и передаёт его заголовком дальше; каждый сервис пишет его в каждую запись. Тогда один запрос ищется одним фильтром по всем системам.
- Осмысленные уровни.
error— только то, на что нужно реагировать. Если в проде тысячи «ошибок» в час, настоящую никто не заметит. - Никаких секретов и персональных данных. Токены, пароли, полные номера карт и телефонов маскируются до вывода.
- Время в 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 + rsyslog | Loki + Grafana | ELK / 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, наоборот, может занимать сопоставимо с исходными данными. Отсюда и разница в стоимости хранения.
Чек-лист логирования для продакшена
- journald в режиме
Storage=persistent, заданыSystemMaxUseи срок хранения. - Для каждого файла в
/var/logесть правило logrotate, или приложение пишет в stdout. - Логи приложения в JSON, с полями времени, уровня, сервиса и request_id.
- Уровень
errorозначает «нужна реакция», а не «что-то произошло». - Пароли, токены и персональные данные маскируются в коде.
- При трёх и более серверах логи собираются централизованно: journal-remote, Loki или ELK.
- Есть алерт хотя бы на всплеск ошибок и на заполнение диска.
- Команда знает, где смотреть логи и как найти запрос по 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-ФЗ.