Мониторинг файла, пока не найдена строка
Я использую tail -f для мониторинга файла журнала, в который ведется активная запись. Когда в файл журнала записывается определенная строка, я хочу выйти из режима мониторинга и продолжить работу с остальным сценарием.
В настоящее время я использую:
tail -f logfile.log | grep -m 1 "Server Started"
Когда строка найдена, grep завершает работу, как и ожидалось, но мне нужно найти способ заставить команду tail выйти так, чтобы скрипт мог продолжаться.
22 ответа
Простой POSIX с одним вкладышем
Вот простая однострочная. Для этого не нужны специфичные для bash или не POSIX трюки, или даже именованный канал. Все, что вам действительно нужно, это отделить окончание tail
от grep
, Таким образом, однажды grep
заканчивается, сценарий может продолжаться, даже если tail
еще не закончился. Итак, этот простой метод доставит вас туда:
( tail -f -n0 logfile.log & ) | grep -q "Server Started"
grep
будет блокировать, пока не найдет строку, после чего он выйдет. Делая tail
запустить из своей собственной под-оболочки, мы можем поместить его в фоновом режиме, чтобы он работал независимо. Между тем, основная оболочка может продолжить выполнение скрипта, как только grep
выходы. tail
будет задерживаться в своей вложенной оболочке до тех пор, пока в лог-файл не будет записана следующая строка, а затем завершится (возможно, даже после завершения основного сценария). Суть в том, что конвейер больше не ждет tail
чтобы завершить, так что трубопровод выходит, как только grep
выходы.
Некоторые незначительные изменения:
- Опция -n0 для
tail
заставляет его начинать чтение с текущей последней строки файла журнала, если строка существует ранее в файле журнала. - Вы можете дать
tail
-F, а не -f. Это не POSIX, но это позволяетtail
работать, даже если журнал вращается во время ожидания. - Опция -q вместо -m1 делает
grep
выйти после первого появления, но без распечатки строки триггера. Также это POSIX, а не -m1.
Принятый ответ не работает для меня, плюс он сбивает с толку и меняет файл журнала.
Я использую что-то вроде этого:
tail -f logfile.log | while read LOGLINE
do
[[ "${LOGLINE}" == *"Server Started"* ]] && pkill -P $$ tail
done
Если строка журнала соответствует шаблону, убить tail
начатый этим сценарием.
Примечание: если вы также хотите просмотреть вывод на экране, либо | tee /dev/tty
или повторить строку перед тестированием в цикле while.
Если вы используете Bash (по крайней мере, но кажется, что он не определен POSIX, поэтому он может отсутствовать в некоторых оболочках), вы можете использовать синтаксис
grep -m 1 "Server Started" <(tail -f logfile.log)
Он работает почти так же, как уже упоминавшиеся решения FIFO, но гораздо проще в написании.
Есть несколько способов получить tail
выходить:
Плохой подход: сила tail
написать еще одну строку
Вы можете заставить tail
написать еще одну строку вывода сразу после grep
нашел совпадение и вышел. Это приведет к tail
чтобы получить SIGPIPE
, заставляя это выйти. Один из способов сделать это - изменить файл, который отслеживается tail
после grep
выходы.
Вот пример кода:
tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }
В этом примере cat
не выйдет до grep
закрыл свой стандартный вывод, так tail
вряд ли сможет писать в трубу раньше grep
был шанс закрыть свой стандартный ввод. cat
используется для распространения стандартного вывода grep
неизмененной.
Этот подход относительно прост, но есть несколько недостатков:
- Если
grep
закрывает стандартный вывод перед закрытием стандартного, всегда будет условие гонки:grep
закрывает стандартный вывод, вызываяcat
выйти, вызвавecho
, вызываяtail
вывести строку. Если эта строка отправленаgrep
доgrep
был шанс закрыть стандартный ввод,tail
не получитSIGPIPE
пока не пишет другую строку. - Требуется доступ для записи в файл журнала.
- Вы должны быть в порядке с изменением файла журнала.
- Вы можете повредить файл журнала, если произойдет запись одновременно с другим процессом (записи могут чередоваться, что приводит к появлению новой строки в середине сообщения журнала).
- Этот подход специфичен для
tail
- он не будет работать с другими программами. - Третий этап конвейера затрудняет получение доступа к коду возврата второго этапа конвейера (если только вы не используете расширение POSIX, такое как
bash
"sPIPESTATUS
массив). Это не имеет большого значения в этом случае, потому чтоgrep
всегда будет возвращать 0, но в целом средняя стадия может быть заменена другой командой, код возврата которой вам небезразличен (например, что-то, что возвращает 0 при обнаружении "сервер запущен", 1 при обнаружении "сервер не удалось запустить"),
Следующие подходы позволяют избежать этих ограничений.
Лучший подход: избегайте трубопроводов
Вы можете использовать FIFO, чтобы полностью избежать конвейера, позволяя продолжить выполнение grep
возвращается. Например:
fifo=/tmp/tmpfifo.$$
mkfifo "${fifo}" || exit 1
tail -f logfile.log >${fifo} &
tailpid=$! # optional
grep -m 1 "Server Started" "${fifo}"
kill "${tailpid}" # optional
rm "${fifo}"
Строки, помеченные комментарием # optional
можно удалить и программа все равно будет работать; tail
будет просто задерживаться, пока не прочитает другую строку ввода или не будет уничтожен каким-либо другим процессом.
Преимущества этого подхода:
- вам не нужно изменять файл журнала
- подход работает для других утилит, кроме
tail
- не страдает от состояния гонки
- вы можете легко получить возвращаемое значение
grep
(или любую другую альтернативную команду, которую вы используете)
Недостатком этого подхода является сложность, особенно управление FIFO: вам нужно будет безопасно сгенерировать временное имя файла, и вам нужно будет убедиться, что временный FIFO удален, даже если пользователь нажимает Ctrl-C в середине сценарий. Это можно сделать с помощью ловушки.
Альтернативный подход: отправить сообщение в Kill tail
Вы можете получить tail
выходить из этапа конвейера, посылая ему сигнал типа SIGTERM
, Задача состоит в том, чтобы достоверно знать две вещи в одном и том же месте кода: tail
PID и ли grep
вышел.
С конвейером вроде tail -f ... | grep ...
легко изменить первый этап конвейера, чтобы сохранить tail
PID в переменном фоне tail
и чтение $!
, Также легко изменить второй этап конвейера для запуска kill
когда grep
выходы. Проблема заключается в том, что два этапа конвейера работают в отдельных "средах выполнения" (в терминологии стандарта POSIX), поэтому второй этап конвейера не может считывать переменные, установленные первым этапом конвейера. Без использования переменных оболочки, либо второй этап должен как-то выяснить tail
PID, чтобы он мог убить tail
когда grep
возвращается, или первый этап должен быть как-то уведомлен, когда grep
возвращается.
Второй этап может использовать pgrep
получить tail
PID, но это было бы ненадежным (вы могли бы соответствовать неправильному процессу) и непереносимым (pgrep
не указано стандартом POSIX).
Первый этап может отправить PID на второй этап по каналу echo
в PID, но эта строка будет смешана с tail
выходной. Демультиплексирование двух может потребовать сложной схемы экранирования, в зависимости от вывода tail
,
Вы можете использовать FIFO, чтобы второй этап конвейера уведомлял первый этап конвейера, когда grep
выходы. Тогда на первом этапе можно убить tail
, Вот пример кода:
fifo=/tmp/notifyfifo.$$
mkfifo "${fifo}" || exit 1
{
# run tail in the background so that the shell can
# kill tail when notified that grep has exited
tail -f logfile.log &
# remember tail's PID
tailpid=$!
# wait for notification that grep has exited
read foo <${fifo}
# grep has exited, time to go
kill "${tailpid}"
} | {
grep -m 1 "Server Started"
# notify the first pipeline stage that grep is done
echo >${fifo}
}
# clean up
rm "${fifo}"
Этот подход имеет все плюсы и минусы предыдущего подхода, за исключением того, что он более сложный.
Предупреждение о буферизации
POSIX позволяет полностью буферизовать потоки stdin и stdout, что означает, что tail
вывод не может быть обработан grep
на сколь угодно долго. В системах GNU не должно быть проблем: GNU grep
использования read()
, что позволяет избежать всей буферизации и GNU tail -f
регулярно звонит fflush()
при записи на стандартный вывод. В системах без GNU может потребоваться сделать что-то особенное, чтобы отключить или регулярно очищать буферы.
Позвольте мне расширить ответ @00promeheus (который является лучшим).
Может быть, вы должны использовать тайм-аут, а не ждать бесконечно.
Приведенная ниже функция bash будет блокироваться до тех пор, пока не появится заданное условие поиска или не истечет заданное время ожидания.
Статус выхода будет 0, если строка найдена в течение времени ожидания.
wait_str() {
local file="$1"; shift
local search_term="$1"; shift
local wait_time="${1:-5m}"; shift # 5 minutes as default timeout
(timeout $wait_time tail -F -n0 "$file" &) | grep -q "$search_term" && return 0
echo "Timeout of $wait_time reached. Unable to find '$search_term' in '$file'"
return 1
}
Возможно, файл журнала еще не существует сразу после запуска вашего сервера. В этом случае вам следует подождать, пока он появится, прежде чем искать строку:
wait_server() {
echo "Waiting for server..."
local server_log="$1"; shift
local wait_time="$1"; shift
wait_file "$server_log" 10 || { echo "Server log file missing: '$server_log'"; return 1; }
wait_str "$server_log" "Server Started" "$wait_time"
}
wait_file() {
local file="$1"; shift
local wait_seconds="${1:-10}"; shift # 10 seconds as default timeout
until test $((wait_seconds--)) -eq 0 -o -f "$file" ; do sleep 1; done
((++wait_seconds))
}
Вот как вы можете использовать это:
wait_server "/var/log/server.log" 5m && \
echo -e "\n-------------------------- Server READY --------------------------\n"
В настоящее время, как дано, все tail -f
Решения здесь рискуют получить ранее зарегистрированную строку "Server Started" (которая может или не может быть проблемой в вашем конкретном случае, в зависимости от количества зарегистрированных строк и ротации / усечения файла журнала).
Вместо того, чтобы чрезмерно усложнять вещи, просто используйте умнее tail
, как показал bmike с фрагментом perl. Самое простое решение - это retail
который имеет встроенную поддержку регулярных выражений с шаблонами условийзапуска и остановки:
retail -f -u "Server Started" server.log > /dev/null
Это будет следовать за файлом, как обычныйtail -f
пока не появится первыйновый экземпляр этой строки, затем выйдите. (The -u
опция не срабатывает на существующие строки в последних 10 строках файла в обычном режиме "follow".)
Если вы используете GNUtail
(изcoreutils), следующий самый простой вариант - использовать --pid
и FIFO (именованный канал):
mkfifo ${FIFO:=serverlog.fifo.$$}
grep -q -m 1 "Server Started" ${FIFO} &
tail -n 0 -f server.log --pid $! >> ${FIFO}
rm ${FIFO}
FIFO используется, потому что процессы должны запускаться отдельно, чтобы получить и передать PID. FIFO все еще страдает от той же проблемы зависания для своевременной записи, чтобы вызватьtail
чтобы получить SIGPIPE, используйте --pid
вариант, чтобыtail
выходит, когда замечает, что grep
прекращено (обычно используется для мониторинга процесса записи, а не читателя, но tail
на самом деле все равно). вариант-n 0
используется с tail
так что старые строки не вызывают совпадения.
Наконец, вы могли бы использовать хвост с сохранением состояния, это сохранит текущее смещение файла, поэтому последующие вызовы будут показывать только новые строки (это также обрабатывает вращение файла). Этот пример использует старый FWTK retail
*:
retail "${LOGFILE:=server.log}" > /dev/null # skip over current content
while true; do
[ "${LOGFILE}" -nt ".${LOGFILE}.off" ] &&
retail "${LOGFILE}" | grep -q "Server Started" && break
sleep 2
done
* Примечание, то же имя, программа отличается от предыдущей опции.
Вместо того, чтобы зацикливаться на процессоре, сравните временную метку файла с файлом состояния (.${LOGFILE}.off
), и спать. Используйте "-T
"чтобы указать расположение файла состояния, если требуется, в приведенном выше предполагается, что текущий каталог. Не стесняйтесь пропустить это условие, или в Linux вы можете использовать более эффективные inotifywait
вместо:
retail "${LOGFILE:=server.log}" > /dev/null
while true; do
inotifywait -qq "${LOGFILE}" &&
retail "${LOGFILE}" | grep -q "Server Started" && break
done
Итак, после некоторого тестирования, я нашел быстрый способ с 1 строкой сделать эту работу. Похоже, tail -f выйдет, когда выйдет grep, но есть одна загвоздка. Кажется, он срабатывает только в том случае, если файл открыт и закрыт. Я сделал это, добавив пустую строку в файл, когда grep найдет совпадение.
tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> logfile \;
Я не уверен, почему открытие / закрытие файла вызывает хвост, чтобы понять, что канал закрыт, поэтому я бы не стал полагаться на это поведение. но, похоже, сейчас работает.
Причина, по которой он закрывается, посмотрите на флаг -F, а не на флаг -f.
Прочитайте их все. tldr: отделить окончание хвоста от grep.
Наиболее удобными являются две формы
( tail -f logfile.log & ) | grep -q "Server Started"
и если у вас есть Баш
grep -m 1 "Server Started" <(tail -f logfile.log)
Но если этот хвост, сидящий на заднем плане, беспокоит вас, есть более хороший способ, чем пятерка или любой другой ответ здесь. Требуется Баш.
coproc grep -m 1 "Server Started"
tail -F /tmp/x --pid $COPROC_PID >&${COPROC[1]}
Or if it isn't tail which is outputing things,
coproc command that outputs
grep -m 1 "Sever Started" ${COPROC[0]}
kill $COPROC_PID
Это будет немного сложнее, так как вам придется войти в процесс управления и сигнализации. Больше kludgey было бы решением с двумя сценариями, использующим отслеживание PID. Лучше бы использовать именованные каналы, как это.
Какой сценарий оболочки вы используете?
Для быстрого и грязного, одно решение сценария - я сделал бы сценарий perl, используя File: Tail
use File::Tail;
$file=File::Tail->new(name=>$name, maxinterval=>300, adjustafter=>7);
while (defined($line=$file->read)) {
last if $line =~ /Server started/;
}
Поэтому вместо того, чтобы печатать внутри цикла while, вы можете отфильтровать совпадения строк и выйти из цикла while, чтобы ваш сценарий продолжался.
Любой из них должен включать в себя лишь небольшое обучение для реализации контроля потока наблюдения, который вы ищете.
tail
команда может быть фоновой, и его пид эхом grep
подоболочка. в grep
subhell обработчик ловушек на EXIT может убить tail
команда.
( (sleep 1; exec tail -f logfile.log) & echo $! ; wait ) |
(trap 'kill "$pid"' EXIT; pid="$(head -1)"; grep -m 1 "Server Started")
Дождитесь появления файла
while [ ! -f /path/to/the.file ]
do sleep 2; done
дождитесь появления строки в файле
while ! grep "the line you're searching for" /path/to/the.file
do sleep 10; done
Вам не нужен хвост, чтобы сделать это. Я думаю, что команда часов это то, что вы ищете. Команда watch контролирует вывод файла и может быть прервана с помощью опции -g при изменении вывода.
watch -g grep -m 1 "Server Started" logfile.log && Yournextaction
Я не могу представить себе более чистого решения, чем это:
#!/usr/bin/env bash
# file : untail.sh
# usage: untail.sh logfile.log "Server Started"
(echo $BASHPID; tail -f $1) | while read LINE ; do
if [ -z $TPID ]; then
TPID=$LINE # the first line is used to store the previous subshell PID
else
echo "$LINE"; [[ "$LINE" == *"${*:2}"* ]] && kill -3 $TPID && break
fi
done
хорошо, возможно имя может быть улучшено ...
Преимущества:
- он не использует никаких специальных утилит
- он не записывает на диск
- он грациозно покидает хвост и закрывает трубу
- это довольно коротко и легко понять
Алекс, я думаю, что это поможет тебе.
tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> /dev/null ;
эта команда никогда не даст запись в лог-файл, но будет молча grep...
Другие решения здесь имеют несколько проблем:
- если процесс регистрации уже остановлен или остановлен во время цикла, они будут работать бесконечно
- редактирование журнала, который должен быть просмотрен только
- излишняя запись дополнительного файла
- не учитывая дополнительную логику
Вот то, что я придумал, используя tomcat в качестве примера (удалите хэши, если вы хотите видеть журнал во время его запуска):
function startTomcat {
loggingProcessStartCommand="${CATALINA_HOME}/bin/startup.sh"
loggingProcessOwner="root"
loggingProcessCommandLinePattern="${JAVA_HOME}"
logSearchString="org.apache.catalina.startup.Catalina.start Server startup"
logFile="${CATALINA_BASE}/log/catalina.out"
lineNumber="$(( $(wc -l "${logFile}" | awk '{print $1}') + 1 ))"
${loggingProcessStartCommand}
while [[ -z "$(sed -n "${lineNumber}p" "${logFile}" | grep "${logSearchString}")" ]]; do
[[ -z "$(ps -ef | grep "^${loggingProcessOwner} .* ${loggingProcessCommandLinePattern}" | grep -v grep)" ]] && { echo "[ERROR] Tomcat failed to start"; return 1; }
[[ $(wc -l "${logFile}" | awk '{print $1}') -lt ${lineNumber} ]] && continue
#sed -n "${lineNumber}p" "${logFile}"
let lineNumber++
done
#sed -n "${lineNumber}p" "${logFile}"
echo "[INFO] Tomcat has started"
}
Вот гораздо лучшее решение, которое не требует записи в файл журнала, что в некоторых случаях очень опасно или даже невозможно.
sh -c 'tail -n +0 -f /tmp/foo | { sed "/EOF/ q" && kill $$ ;}'
В настоящее время он имеет только один побочный эффект, tail
процесс будет оставаться в фоновом режиме, пока следующая запись не будет записана в журнал.
Вы хотите уйти, как только строка написана, но вы также хотите уйти после тайм-аута:
if (timeout 15s tail -F -n0 "stdout.log" &) | grep -q "The string that says the startup is successful" ; then
echo "Application started with success."
else
echo "Startup failed."
tail stderr.log stdout.log
exit 1
fi
Попробуйте использовать inotify (inotifywait)
Вы настраиваете inotifywait для любого изменения файла, затем проверяете файл с помощью grep, если он не найден, просто перезапустите inotifywait, если он найден, выйдите из цикла ...
Вдохновленный ответом Фио /questions/933302/monitoring-fajla-poka-ne-najdena-stroka/933334#933334, я написал эту версию для функции, которая работает как в Linux, так и в MacOSX.
wait_for_pattern() {
local file="$1"
local pattern="$2"
# Two parts:
# 1. tail follow the file in background, printing the PID and waiting for it
# 2. add a trap to kill the tail PID on exit, read PID from first line, sed to match the first occurrence
(
(sleep 1; exec tail -n 1000 -f "${file}") & echo $! ; wait
) | (
trap 'kill "$pid"' EXIT; pid="$(head -1)";
sed -n "/${pattern}/{p;q;}"
)
}
wait_for_pattern /tmp/foo Lib
Объяснение:
- запускает подпроцесс, работающий в фоновом режиме
- Подпроцесс сна 1, чтобы дождаться следующего конвейерного процесса для чтения pid
- затем выполняет удар в хвост
- распечатать PID фонового процесса
- мы добавляем ожидание завершения хвоста в фоновом режиме
- Конвейерный подпроцесс установит ловушку для уничтожения PID.
- прочитайте PID из первой строки
- sed будет соответствовать шаблону и завершит работу при первом появлении. Этот синтаксис sed совместим с macosx.
Мне нужно было записать строку в файл журнала после того, как строка появилась в auth.log
вдохновился ответом выше
tail -f logfile.log | while read LOGLINE
do
[[ "${LOGLINE}" == *"Server Started"* ]] && pkill -P $$ tail
done
для меня работал дальше
tail -f -n0 /var/log/auth.log | while read LOGLINE;
do [[ "${LOGLINE}" == *"Removed session"* ]] && pkill -P $$ tail;
echo "$(date "+%d-%m-%Y_%H:%M:%S") $(whoami) Screen_Locked poweroff" >>
/var/log/lock-unlock-user.log;
done
Один лайнер
tail -f myfile.log | while read LINE; do echo $LINE; echo "$LINE" | grep -Fq 'Listening on 0.0.0.0' && pkill -P $$; done
Примечания
- На основе отличного ответа Роба /questions/933302/monitoring-fajla-poka-ne-najdena-stroka/933337#933337
- Не использует bash только двойные скобки
[[]]
- Это использует
GNU grep
чтобы выполнить сопоставление строк. - я не видел ни одного
tail
процессы, запускающиеся после этого, заканчиваются, но я мог это пропустить, поправьте меня, если я ошибаюсь.
Объяснение
tail -f myfile.log | while read LINE;
будет зацикливаться построчно отmyfile.log
и отправить его вwhile
который сохранит его в переменной$LINE
.
echo $LINE;
Будет распечатана/отражена строка, чтобы мы могли ее отслеживать, я полагаю, что при этом будет потеряна вся информация о цвете.
echo "$LINE" | grep -Fq 'Listening on 0.0.0.0' && pkill -P $$;
echo
LINE как «строку» и отправьте вывод в grep.
Затем grep будет искать строку «Прослушивание 0.0.0.0» и, когда она будет найдена, завершит текущий процесс.
-
$$
— специальная переменная, возвращающая текущий идентификатор процесса. - grep
-F
означает «выполнить сопоставление открытого текста», это пропускает выполнение регулярного выражения и делает его быстрее. - grep
-q
заглушает вывод и завершает работу, как только обнаруживается одно совпадение, возвращаяtrue
илиfalse
с которым ты можешь связать&&
После этого оболочка продолжит выполнять команды как обычно.
Самое замечательное в этом подходе то, что мы можем использовать его для чего угодно, даже для мониторинга докер-контейнера, пока не будет найдена строка.
docker logs container_name -f | while read LINE; do echo ${LINE}; echo "${LINE}" | grep -Fq 'Listening on 0.0.0.0' && pkill -P $$; done; echo "hello"
как насчет этого:
пока правда; делать, если [! -z $(grep "myRegEx" myLog.log) ]; затем сломаться; фи; сделанный