Мониторинг файла, пока не найдена строка

Я использую 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"s PIPESTATUS массив). Это не имеет большого значения в этом случае, потому что 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, Задача состоит в том, чтобы достоверно знать две вещи в одном и том же месте кода: tailPID и ли grep вышел.

С конвейером вроде tail -f ... | grep ...легко изменить первый этап конвейера, чтобы сохранить tailPID в переменном фоне tail и чтение $!, Также легко изменить второй этап конвейера для запуска kill когда grep выходы. Проблема заключается в том, что два этапа конвейера работают в отдельных "средах выполнения" (в терминологии стандарта POSIX), поэтому второй этап конвейера не может считывать переменные, установленные первым этапом конвейера. Без использования переменных оболочки, либо второй этап должен как-то выяснить tailPID, чтобы он мог убить tail когда grep возвращается, или первый этап должен быть как-то уведомлен, когда grep возвращается.

Второй этап может использовать pgrep получить tailPID, но это было бы ненадежным (вы могли бы соответствовать неправильному процессу) и непереносимым (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

/questions/54323/rabotaet-dlya-tsikla-no-zhdat-stroki-slov-v-fajle-zhurnala-chtobyi-prodolzhit/54328#54328

Вам не нужен хвост, чтобы сделать это. Я думаю, что команда часов это то, что вы ищете. Команда 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. запускает подпроцесс, работающий в фоновом режиме
  2. Подпроцесс сна 1, чтобы дождаться следующего конвейерного процесса для чтения pid
  3. затем выполняет удар в хвост
  4. распечатать PID фонового процесса
  5. мы добавляем ожидание завершения хвоста в фоновом режиме
  6. Конвейерный подпроцесс установит ловушку для уничтожения PID.
  7. прочитайте PID из первой строки
  8. 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 $$;

echoLINE как «строку» и отправьте вывод в 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) ]; затем сломаться; фи; сделанный

Другие вопросы по тегам