Логи пишутся не туда, где вы их ищете: находим реальные пути записи
Вы открываете /var/log/app/app.log на только что доставшемся сервере, а там — тишина: последняя строка датирована прошлой неделей, хотя приложение прямо сейчас отвечает на запросы и явно что-то логирует. Первая мысль — «логирование сломано». В девяти случаях из десяти оно работает штатно, просто пишет не в тот файл, который вы смотрите: путь переопределён в конфиге, вывод уходит в journald вместо файла, или между приложением и диском стоит симлинк, о котором никто не вспомнил. На унаследованном сервере документации на этот счёт обычно нет — путь к логам нужно спрашивать у самой системы. Разберём, как это сделать по шагам, от самого надёжного источника истины до самого обманчивого.
Содержание
- lsof и /proc: спросите не документацию, а сам процесс
- Конфиг приложения: ищем директиву логирования, а не угадываем путь
- systemd journal против файла: может, лога-файла и не должно быть
- Симлинки, bind-mounts и Docker-тома: путь короче, чем кажется
- Syslog вместо файла: приложение просто не пишет туда, куда вы смотрите
- Logrotate мог унести лог туда, куда вы не смотрите
- Собираем чек-лист: пошаговый алгоритм поиска реального пути
lsof и /proc: спросите не документацию, а сам процесс
Самый надёжный источник правды о том, куда пишет процесс, — не конфиг и не документация, а сам работающий процесс: ядро точно знает, какие файловые дескрипторы у него открыты прямо сейчас.
Найдите PID процесса и посмотрите на его открытые файлы:
pgrep -f имя-приложения
lsof -p <PID> | grep -E 'REG|log'
lsof покажет все обычные файлы (REG в колонке TYPE), которые процесс держит открытыми на запись, — среди них обычно и настоящий файл лога, даже если он лежит в неожиданном месте вроде /opt/app/data/runtime.log или /home/deploy/logs/out.log, а не в привычном /var/log. Колонка FD со значением вроде 3w — дескриптор №3, открытый на запись, это и есть кандидат.
Тот же результат без lsof (он не всегда установлен по умолчанию) даёт прямое чтение /proc:
ls -la /proc/<PID>/fd/ | grep -v socket
Символические ссылки в /proc/<PID>/fd/ указывают на реальные пути файлов, которые процесс держит открытыми, — включая случай, когда файл на диске уже переименован или удалён ((deleted) в конце ссылки, отдельный частый признак, разберём его ниже).
Если процесс запущен под systemd, сразу проверьте, куда утроен его стандартный вывод — это может объяснить, почему файлового лога нет вовсе:
systemctl show имя-сервиса -p StandardOutput -p StandardError
Если оба значения — journal (поведение по умолчанию для systemd-юнитов без явной директивы), файла в привычном смысле может не быть в принципе: весь вывод уходит в journald, и это не поломка, а нормальная конфигурация, которую просто не задокументировали. Подробнее — в разделе ниже.
Конфиг приложения: ищем директиву логирования, а не угадываем путь
Если lsof показал файл в неожиданном месте, следующий шаг — найти в конфиге приложения строку, которая его туда направляет, а не полагаться на память о том, «куда обычно пишет вот эта CMS». На унаследованном сервере дефолты почти никогда не действуют без изменений — иначе не пришлось бы искать.
Практичнее искать не файл конфига по имени (их может быть несколько, и не факт, что читается именно найденный первым), а саму директиву логирования по содержимому:
grep -riE 'log_?(file|path|dir|output)' /etc/имя-приложения/ 2>/dev/null
grep -riE 'access_log|error_log' /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* 2>/dev/null
grep -riE '"log"|logging' /opt/app/config*.json /opt/app/*.yml 2>/dev/null
Три вещи чаще всего сбивают с толку при поиске конфига на чужом сервере:
- Переменные окружения переопределяют файл конфига. Многие приложения (особенно на Node.js, Go, Python) читают путь к логу из
LOG_FILE/LOG_PATH/LOG_DIRв окружении, и это значение выигрывает у того, что написано в конфиге. Проверьте окружение реально запущенного процесса:
cat /proc/<PID>/environ | tr '\0' '\n' | grep -i log
- Несколько конфигов, и грузится не тот, что вы правите. Конфиг лежит и в
/etc/app/config.yml, и в домашнем каталоге пользователя сервиса, и в каталоге, откуда приложение реально запущено (systemctl cat имя-сервисапокажетWorkingDirectory=и точную команду запуска, включая флаг--config, если он перекрывает всё остальное). - Логирование настроено на уровне фреймворка, а не приложения. У Java (Logback/Log4j2), Python (
logging.conf/dictConfigв коде) путь к логу может быть зашит не в основной конфиг сервиса, а в отдельный файл конфигурации логирования:find / -iname 'log4j2*.xml' -o -iname 'logback*.xml' -o -iname 'logging.conf' 2>/dev/null.
Если путь в конфиге совпадает с тем, что показал lsof, — вопрос закрыт. Если не совпадает — доверяйте lsof: он показывает, что происходит на самом деле, а не что написано в файле, который приложение, возможно, вообще не читает.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверsystemd journal против файла: может, лога-файла и не должно быть
Отдельная и очень частая причина «логов не там, где ожидается» — их вообще нет как файла, потому что вывод приложения идёт в journald, а не на диск в виде текстового файла.
Если сервис запущен через systemd-юнит без явных директив StandardOutput=/StandardError=, по умолчанию весь его stdout/stderr отправляется в journal. Проверить и прочитать такой лог:
journalctl -u имя-сервиса --since "2 hours ago"
journalctl -u имя-сервиса -f
Если команда возвращает содержательный вывод — вот и ответ: логи не пропали, они просто никогда не были файлом. Обратная ситуация тоже встречается: приложение пишет в файл напрямую и одновременно дублирует часть вывода в journal через stdout — тогда часть сообщений видна в обоих местах, а часть только в одном.
Отдельно стоит проверить: даже при Storage=persistent в /etc/systemd/journald.conf journal может сознательно отбрасывать часть строк под нагрузкой из-за встроенного ограничения скорости приёма — не находя сообщение ни в journal, ни в файле, не спешите с выводом «логирование сломано». Диагностика и настройка этих лимитов разобраны отдельно: как journald хранит логи и почему под нагрузкой он теряет строки.
Если сервис в контейнере, добавляется ещё один слой: контейнерный движок пишет вывод через собственный log driver (json-file, journald, syslog и другие), и путь на хосте определяется настройками движка, а не приложением внутри контейнера — типичные грабли с драйверами разобраны в статье лучшие практики логирования в Docker.
Симлинки, bind-mounts и Docker-тома: путь короче, чем кажется
Даже когда и lsof, и конфиг сходятся на одном пути, это ещё не значит, что физический файл лежит там, где вы смотрите. Между «путём, который видит процесс» и «файлом на реальном разделе диска» может быть посредник.
Симлинки. Классика на унаследованном сервере: кто-то перенёс логи с переполненного корневого раздела на отдельный диск и оставил символическую ссылку на старом месте, чтобы не переписывать конфиги приложений:
ls -la /var/log/app
# lrwxrwxrwx 1 root root 24 ... /var/log/app -> /mnt/logs-disk/app
Если /mnt/logs-disk не смонтирован (диск отключён, fstab-запись протухла после миграции) — запись по симлинку либо падает с ошибкой, которую приложение может тихо проглотить, либо уходит в пустой каталог на корневом разделе, образовавшийся поверх точки монтирования. Проверка одной командой:
mount | grep -i logs
findmnt /mnt/logs-disk
Bind-mounts и тома контейнеров. Похожая история в Docker: внутри контейнера путь может выглядеть как /app/logs/app.log, но реальное содержимое лежит там, куда указывает -v или запись в volumes: из docker-compose.yml:
docker inspect имя-контейнера --format '{{ range .Mounts }}{{ .Source }} -> {{ .Destination }}{{ "\n" }}{{ end }}'
Если ожидаемый путь на хосте не совпадает с реальным Source из вывода — искали не там по объективной причине: путь внутри контейнера и путь на хосте это два разных пространства имён, совпадение их названий ничего не гарантирует.
Удалённый, но всё ещё открытый файл. Процесс продолжает писать в файл, уже удалённый с диска (кто-то вручную стёр старый лог, не тронув процесс). Файл физически не исчезает, пока хотя бы один процесс держит его открытым, но и не виден ни по какому пути. Признак — (deleted) в выводе lsof или ls -la /proc/<PID>/fd/:
lsof -p <PID> | grep deleted
Единственный способ вернуть доступ к содержимому такого файла, не перезапуская процесс, — читать его через сам дескриптор: cat /proc/<PID>/fd/3 > recovered.log (номер смотрите в выводе lsof/ls), после чего перезапустить сервис, чтобы он открыл файл заново.
Syslog вместо файла: приложение просто не пишет туда, куда вы смотрите
Ещё одна причина «логов нет на месте» — приложение изначально не пишет в файл напрямую, а вызывает системный syslog(), полагаясь на то, что локальный демон разберётся с записью. Дальше судьба сообщения полностью зависит от конфигурации rsyslog/syslog-ng, о которой у самого приложения нет никакого представления.
Проверить, использует ли приложение syslog-транспорт, можно по конфигу логгера (там обычно указан facility — local0–local7, daemon, user) или косвенно — если поиск файла по имени ничего не дал, а strace показывает вызовы записи в /dev/log:
strace -p <PID> -e trace=network,write 2>&1 | grep -i '/dev/log'
Если транспорт подтвердился, реальный путь записи ищите не у приложения, а в правилах rsyslog — именно они решают, в какой файл (или файлы) попадёт сообщение с конкретным facility и severity:
grep -rE 'local[0-7]|daemon\.' /etc/rsyslog.conf /etc/rsyslog.d/*.conf
На унаследованном сервере тут нередко находится правило, которое перенаправляет нужный facility в файл с неочевидным именем — например, local3.* уходит не в /var/log/app.log, а в /var/log/custom/billing-events.log, потому что кто-то так сделал ради разделения потоков и не оставил комментария. Другой частый вариант — правило вообще отсутствует, тогда сообщения попадают в общий /var/log/syslog//var/log/messages вперемешку со всем остальным, и найти их можно только фильтром по тегу процесса:
grep 'имя-приложения\[' /var/log/syslog
Если ни явного правила, ни совпадений в общем syslog нет, а rsyslog вовсе не запущен — проверьте, не слушает ли /dev/log сам systemd-journald вместо отдельного демона: тогда искать нужно через journalctl, как описано выше.
Logrotate мог унести лог туда, куда вы не смотрите
Отдельная и обманчивая ситуация: путь записи не менялся никогда, но файл, который вы открываете, — уже не тот, куда только что писало приложение, потому что logrotate успел его повернуть между вашими проверками. Свежие строки лежат в новом активном файле, а то, что вы читаете, — вчерашняя ротированная копия, принятая за актуальный лог из-за похожего имени.
Сначала стоит понять, какая стратегия ротации настроена для этого лога — от неё зависит, где искать актуальные данные:
cat /etc/logrotate.d/имя-приложения
Ключевые директивы, которые меняют то, где реально лежат данные:
olddir /путь— ротированные копии уезжают в отдельный каталог, а не остаются рядом с активным файлом. Если вы искали архив логов там же, где активный файл, а конфиг указываетolddir, — архив просто в другом месте.copytruncateпротив обычной ротации без неё — приcopytruncateактивный файл не пересоздаётся, а обнуляется на месте после копирования, дескриптор у приложения остаётся тем же; без этой директивы logrotate переименовывает старый файл и создаёт новый, а приложению нужно либо самому переоткрыть файл по сигналу, либо оно продолжит писать в уже переименованный (и по факту скрытый из обычного листинга под новым именем) файл, пока не будет перезапущено.compress/delaycompress— вчерашняя ротированная копия может быть уже сжата в.gz, и обычныйtail/grepбезzgrep/zcatеё просто не прочитает и создаст ложное впечатление, что данных за этот период нет вовсе.
Проверить, когда последний раз реально срабатывала ротация для конкретного файла, и не разошлось ли расписание с ожиданиями:
cat /var/lib/logrotate/status | grep -i имя-приложения
ls -la /var/log/имя-приложения*
Если время в статусе logrotate заметно отличается от ожидаемого (ротация настроена ежедневно, а по факту не срабатывала неделю — или наоборот, срабатывает чаще из-за постороннего cron-задания), это частая находка на унаследованных серверах: два конфига logrotate управляют одним и тем же файлом с разными настройками, потому что предыдущий админ добавил новое правило, не удалив старое. Как устроена ротация и куда реально уходит место на диске после неё — в статье ротация логов, чтобы не забивался диск.
Собираем чек-лист: пошаговый алгоритм поиска реального пути
Когда логов не видно там, где ожидалось, порядок проверки удобнее держать фиксированным, а не скакать между гипотезами наугад — особенно там, где никто не подтвердит, как всё было настроено изначально.
# 1. Спросите сам процесс
pgrep -f имя-приложения
lsof -p <PID> | grep REG
# 2. Проверьте, не уходит ли вывод в journal
systemctl show имя-сервиса -p StandardOutput -p StandardError
journalctl -u имя-сервиса --since "1 hour ago" | head
# 3. Найдите директиву логирования в конфиге, а не угадывайте
grep -riE 'log_?(file|path|dir)' /etc/имя-приложения/
cat /proc/<PID>/environ | tr '\0' '\n' | grep -i log
# 4. Проверьте, что путь — не симлинк на несмонтированный диск
ls -la $(dirname путь-из-lsof)
mount | grep -i logs
# 5. Если это контейнер — сверьте путь внутри и снаружи
docker inspect имя-контейнера --format '{{ .Mounts }}'
# 6. Проверьте, не через syslog ли идёт запись
grep -rE 'local[0-7]' /etc/rsyslog.d/*.conf
# 7. Проверьте, не ротирован ли файл только что
cat /var/lib/logrotate/status | grep имя-приложения
Порядок не случаен: первые два пункта быстро отсекают целые классы причин (файла может не быть вовсе, если это journal), а дальше вы двигаетесь от самого надёжного источника (что процесс делает прямо сейчас) к самому косвенному (что могло произойти в прошлом при ротации). Такой порядок экономит часы по сравнению с угадыванием конфигурации по названию приложения или «как обычно бывает».
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Что делать, если lsof вообще не установлен на сервере?
Использовать /proc/<PID>/fd/ напрямую: ls -la /proc/<PID>/fd/ покажет символические ссылки на все открытые файлы процесса, включая помеченные (deleted). lsof можно установить (apt install lsof/dnf install lsof), но чтение /proc не требует установки ничего и работает даже в минимальных контейнерных образах.
Приложение пишет в несколько файлов сразу — как понять, какой из них основной?
Смотрите на активность через lsof несколько раз подряд с интервалом или через inotifywait -m /путь/к/каталогу — файл, в который реально идёт запись, будет менять размер или mtime на глазах. Часто один файл — активный лог, а рядом лежат ротированные архивы или отдельные потоки (access отдельно от error), и путаница возникает из-за похожих имён.
Почему в конфиге путь один, а lsof показывает другой?
Путь переопределён где-то с более высоким приоритетом: переменная окружения, флаг командной строки при запуске (смотрите точную команду через systemctl cat имя-сервиса), либо приложение читает не тот конфиг-файл, который вы правите — например, есть системный и пользовательский конфиг, и выигрывает второй.
Ротированный файл сжат в .gz, но данные за этот период всё равно нужны — как их прочитать без распаковки на диск?
zgrep, zcat, zless работают с .gz напрямую, без промежуточной распаковки: zgrep 'ошибка' /var/log/app.log.3.gz. Это особенно полезно, когда места на диске впритык.
Можно ли автоматизировать этот поиск, чтобы не проходить его вручную на каждом новом унаследованном сервере?
Да, чек-лист из предыдущего раздела легко оформить в один bash-скрипт, который принимает имя процесса и печатает результат каждого шага, — разумно завести его один раз для приёмки новых серверов. Если приёмка сервера в целом внушает опасения, стоит заодно свериться с более широким чек-листом: достался сервер от уволившегося админа: первый час.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →