web-dev-qa-db-fra.com

Surveiller un fichier jusqu'à ce qu'une chaîne soit trouvée

J'utilise tail -f pour surveiller un fichier journal en cours d'écriture. Lorsqu'une certaine chaîne est écrite dans le fichier journal, je souhaite quitter la surveillance et poursuivre le reste de mon script.

Actuellement, j'utilise:

tail -f logfile.log | grep -m 1 "Server Started"

Lorsque la chaîne est trouvée, grep se ferme comme prévu, mais je dois trouver un moyen de faire quitter également la commande tail afin que le script puisse continuer.

58
Alex Hofsteede

Une simple doublure POSIX

Voici un simple one-liner. Il n'a pas besoin d'astuces spécifiques à bash ou non-POSIX, ni même de pipe nommée. Tout ce dont vous avez besoin est de dissocier la terminaison de tail de grep. De cette façon, une fois que grep se termine, le script peut continuer même si tail n'est pas encore terminé. Donc, cette méthode simple vous y mènera:

( tail -f -n0 logfile.log & ) | grep -q "Server Started"

grep bloquera jusqu'à ce qu'il ait trouvé la chaîne, après quoi il se fermera. En exécutant tail à partir de son propre sous-shell, nous pouvons le placer en arrière-plan afin qu'il s'exécute indépendamment. Pendant ce temps, le shell principal est libre de poursuivre l'exécution du script dès que grep est terminé. tail restera dans son sous-shell jusqu'à ce que la ligne suivante soit écrite dans le fichier journal, puis se ferme (éventuellement même après la fin du script principal). Le point principal est que le pipeline n'attend plus la fin de tail. Il se ferme donc dès que grep est terminé.

Quelques modifications mineures:

  • L'option -n0 à tail permet de commencer à lire à partir de la dernière ligne actuelle du fichier journal, au cas où la chaîne existe plus tôt dans le fichier journal.
  • Vous voudrez peut-être donner tail -F plutôt que -f. Ce n'est pas POSIX, mais cela permet à tail de fonctionner même si le journal est pivoté pendant l'attente.
  • L'option -q plutôt que -m1 force grep à quitter après la première occurrence, mais sans afficher la ligne de déclenchement. En outre, il s'agit de POSIX, ce qui n'est pas le cas de -m1.
32
00prometheus

La réponse acceptée ne fonctionne pas pour moi. En plus, c'est déroutant et cela change le fichier journal.

J'utilise quelque chose comme ça:

tail -f logfile.log | while read LOGLINE
do
   [[ "${LOGLINE}" == *"Server Started"* ]] && pkill -P $$ tail
done

Si la ligne de journal correspond au modèle, supprimez la tail démarrée par ce script.

Remarque: si vous souhaitez également afficher la sortie à l'écran, utilisez | tee /dev/tty ou faites écho à la ligne avant de tester la boucle while.

57
Rob Whelan

Si vous utilisez Bash (du moins, mais il semble que ce ne soit pas défini par POSIX, il est peut-être manquant dans certains shells), vous pouvez utiliser la syntaxe

grep -m 1 "Server Started" <(tail -f logfile.log)

Cela fonctionne à peu près comme les solutions FIFO déjà mentionnées, mais beaucoup plus simple à écrire.

15
petch

Il y a plusieurs façons d'obtenir tail pour sortir:

Mauvaise approche: obliger tail à écrire une autre ligne

Vous pouvez forcer tail à écrire une autre ligne de sortie immédiatement après que grep ait trouvé une correspondance et quitté. Cela va donner à tail une SIGPIPE, ce qui la fera sortir. Une façon de procéder consiste à modifier le fichier surveillé par tail après la sortie de grep.

Voici un exemple de code:

tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }

Dans cet exemple, cat ne sortira pas tant que grep n'aura pas fermé sa sortie standard. Il est donc peu probable que tail puisse écrire dans le canal avant que grep ait pu fermer son stdin. cat est utilisé pour propager la sortie standard de grep non modifiée.

Cette approche est relativement simple, mais il y a plusieurs inconvénients:

  • Si grep ferme stdout avant stdin, il y aura toujours une condition de concurrence: grep ferme stdout, en déclenchant cat pour quitter, en déclenchant echo et en déclenchant tail en sortie. Si cette ligne est envoyée à grep avant que grep ne puisse fermer stdin, tail n'obtiendra pas la SIGPIPE tant qu'elle n'aura pas écrit une autre ligne.
  • Il nécessite un accès en écriture au fichier journal.
  • Vous devez être en mesure de modifier le fichier journal.
  • Vous pouvez corrompre le fichier journal si vous écrivez en même temps qu'un autre processus (les écritures peuvent être entrelacées, ce qui entraînera l'apparition d'une nouvelle ligne au milieu d'un message de journal).
  • Cette approche est spécifique à tail— elle ne fonctionnera pas avec d'autres programmes.
  • La troisième étape de pipeline rend difficile l'accès au code de retour de la deuxième étape de pipeline (sauf si vous utilisez une extension POSIX telle que le tableau bash's PIPESTATUS). Ce n’est pas un gros problème dans ce cas car grep retournera toujours 0, mais en général, l’étape intermédiaire peut être remplacée par une commande différente dont le code de retour vous tient à cœur (par exemple, quelque chose qui retourne 0 quand "serveur démarré" est détecté, 1 lorsque "échec du démarrage du serveur" est détecté).

Les approches suivantes évitent ces limitations.

Une meilleure approche: éviter les pipelines

Vous pouvez utiliser un FIFO pour éviter complètement le pipeline, permettant ainsi à l'exécution de continuer une fois que grep est retourné. Par exemple:

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}"

Les lignes marquées du commentaire # optional peuvent être supprimées et le programme fonctionnera toujours. tail s'attardera jusqu'à ce qu'il lise une autre ligne d'entrée ou soit tué par un autre processus.

Les avantages de cette approche sont les suivants:

  • vous n'avez pas besoin de modifier le fichier journal
  • l'approche fonctionne pour d'autres utilitaires que tail
  • il ne souffre pas d'une situation de concurrence
  • vous pouvez facilement obtenir la valeur de retour de grep (ou toute autre commande que vous utilisez)

L'inconvénient de cette approche est la complexité, en particulier la gestion de la FIFO: vous devez générer de manière sécurisée un nom de fichier temporaire et vous assurer que le FIFOtemporaire est supprimé même si l'utilisateur appuie sur la touche Ctrl -C au milieu du script. Cela peut être fait en utilisant un piège.

Approche alternative: Envoyer un message à Kill tail

Vous pouvez obtenir la fermeture de l’étape de pipeline tail en lui envoyant un signal du type SIGTERM. Le défi consiste à connaître de manière fiable deux choses au même endroit dans le code: le PID de tail et si grep est terminé.

Avec un pipeline comme tail -f ... | grep ..., il est facile de modifier la première étape du pipeline pour enregistrer le PID de tail dans une variable en mettant en arrière-plan tail et en lisant $!. Il est également facile de modifier la deuxième étape du pipeline pour qu'elle exécute kill lorsque grep se termine. Le problème est que les deux étapes du pipeline s'exécutent dans des "environnements d'exécution" distincts (dans la terminologie de la norme POSIX), de sorte que la deuxième étape de pipeline ne peut lire aucune variable définie par l'étape de pipeline précédente. Sans utiliser de variables Shell, la deuxième étape doit déterminer le PID de tail de façon à pouvoir tuer tail lorsque grep est renvoyé, ou la première étape doit être notifiée lorsque grep est renvoyée.

La deuxième étape pourrait utiliser pgrep pour obtenir le PID de tail, mais cela ne serait pas fiable (vous pourriez correspondre au processus incorrect) et non portable (pgrep n'est pas spécifié par la norme POSIX).

La première étape pourrait envoyer le PID à la deuxième étape via le tuyau en echoing le PID, mais cette chaîne sera mélangée avec la sortie de tail. Le démultiplexage des deux peut nécessiter un schéma d'échappement complexe, en fonction du résultat de tail.

Vous pouvez utiliser un FIFO pour que la deuxième étape de pipeline informe la première étape de pipeline lorsque grep se ferme. Alors la première étape peut tuer tail. Voici un exemple de code:

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}"

Cette approche présente tous les avantages et les inconvénients de l'approche précédente, à l'exception que ce soit plus compliqué.

Un avertissement sur la mise en mémoire tampon

POSIX autorise la mise en mémoire tampon complète des flux stdin et stdout, ce qui signifie que la sortie de tail peut ne pas être traitée par grep pendant un temps arbitrairement long. Il ne devrait y avoir aucun problème sur les systèmes GNU: GNU grep utilise read(), ce qui évite toute mise en mémoire tampon, et GNU tail -f appelle régulièrement fflush() lors de l'écriture sur stdout. Les systèmes non-GNU peuvent avoir à faire quelque chose de spécial pour désactiver ou vider régulièrement les tampons.

14
Richard Hansen

Permettez-moi de développer la réponse @ 00prometheus (qui est la meilleure).

Peut-être devriez-vous utiliser un délai d'attente au lieu d'attendre indéfiniment.

La fonction bash ci-dessous sera bloquée jusqu'à ce que le terme de recherche donné apparaisse ou qu'un délai d'expiration donné soit atteint.

L'état de sortie sera 0 si la chaîne est trouvée dans le délai imparti.

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
}

Peut-être que le fichier journal n'existe pas encore juste après le lancement de votre serveur. Dans ce cas, vous devez attendre son apparition avant de rechercher la chaîne:

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))
}

Voici comment vous pouvez l'utiliser:

wait_server "/var/log/server.log" 5m && \
echo -e "\n-------------------------- Server READY --------------------------\n"
8
Elifarley

Donc, après avoir fait quelques tests, j'ai trouvé un moyen rapide, en une ligne, de faire en sorte que cela fonctionne. Il semble que tail -f se ferme quand grep se ferme, mais il y a un piège. Il semble ne se déclencher que si le fichier est ouvert et fermé. Pour ce faire, j'ai ajouté la chaîne vide au fichier lorsque grep a trouvé la correspondance.

tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> logfile \;

Je ne suis pas sûr de savoir pourquoi l'ouverture/la fermeture du fichier amène la queue à se rendre compte que le tuyau est fermé. Je ne compte donc pas sur ce comportement. mais cela semble fonctionner pour le moment.

Raison pour laquelle il ferme, regardez le drapeau -F, par rapport au drapeau -f.

6
Alex Hofsteede

Actuellement, comme indiqué, toutes les solutions tail -f ici risquent de capturer une ligne "Server Started" précédemment enregistrée (ce qui peut être un problème dans votre cas spécifique, en fonction du nombre de lignes enregistrées et de la rotation du fichier journal/troncature).

Plutôt que de trop compliquer les choses, utilisez simplement une tail plus intelligente, comme le montre bmike avec un extrait Perl. La solution la plus simple est cette retail qui intègre le support regex avec start et stop modèles de condition:

retail -f -u "Server Started" server.log > /dev/null

Cela suivra le fichier comme un tail -f normal jusqu'à ce que la première nouvelle nouvelle instance de cette chaîne apparaisse, puis se ferme. (L'option -u ne se déclenche pas sur les lignes existantes des 10 dernières lignes du fichier en mode "normal".)


Si vous utilisez GNU tail (from coreutils ), l'option suivante la plus simple consiste à utiliser --pid et un FIFO (canal nommé):

mkfifo ${FIFO:=serverlog.fifo.$$}
grep -q -m 1 "Server Started" ${FIFO}  &
tail -n 0 -f server.log  --pid $! >> ${FIFO}
rm ${FIFO}

Un FIFO est utilisé car les processus doivent être démarrés séparément pour obtenir et transmettre un PID. Un FIFO souffre toujours du même problème de traîner pendant une écriture rapide pour que tail reçoive un SIGPIPE , utilisez le --pid Option permettant à tail de se fermer lorsqu'il constate que grep est terminé (généralement utilisé pour surveiller le processus d'écriture plutôt que le lecteur , mais que tail ne le fait pas. soigne vraiment). L'option -n 0 est utilisée avec tail afin que les anciennes lignes ne déclenchent pas de correspondance.


Enfin, vous pouvez utiliser une queue d'état , qui stockera le décalage de fichier actuel afin que les invocations suivantes ne montrent que les nouvelles lignes (il gère également la rotation de fichier). Cet exemple utilise l'ancien 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

* Remarque, même nom, programme différent de l'option précédente.

Plutôt que d'avoir une boucle monopolisation d'UC, comparez l'horodatage du fichier avec le fichier d'état (.${LOGFILE}.off) et mettez en veille. Utilisez "-T" pour spécifier l'emplacement du fichier d'état, le cas échéant, le répertoire ci-dessus est supposé. N'hésitez pas à ignorer cette condition, ou sous Linux, vous pouvez utiliser la plus efficace inotifywait à la place:

retail "${LOGFILE:=server.log}" > /dev/null
while true; do
    inotifywait -qq "${LOGFILE}" && 
       retail "${LOGFILE}" | grep -q "Server Started" && break
done
6
mr.spuratic

Ce sera un peu délicat, car vous devrez entrer dans le contrôle et la signalisation du processus. Plus kludgey serait une solution à deux scripts utilisant le suivi PID. Mieux vaut utiliser des pipes nommés comme ceci.

Quel script shell utilisez-vous?

Pour une solution de script rapide et sale, je créerais un script Perl en utilisant Fichier: Tail

use File::Tail;
$file=File::Tail->new(name=>$name, maxinterval=>300, adjustafter=>7);
while (defined($line=$file->read)) {
    last if $line =~ /Server started/;
}

Ainsi, plutôt que d'imprimer à l'intérieur de la boucle while, vous pouvez filtrer la correspondance de chaîne et sortir de la boucle while pour laisser votre script se poursuivre.

L'une ou l'autre de ces opérations devrait impliquer un peu d'apprentissage pour mettre en œuvre le contrôle de flux d'observation que vous recherchez.

4
bmike

Je ne peux pas imaginer une solution plus propre que celle-ci:

#!/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

ok, peut-être que le nom peut être sujet à des améliorations ...

Avantages:

  • il n'utilise aucun utilitaire spécial
  • il n'écrit pas sur le disque
  • il quitte gracieusement la queue et ferme le tuyau
  • c'est assez court et facile à comprendre
2
Giancarlo Sportelli

attendre que le fichier apparaisse

while [ ! -f /path/to/the.file ] 
do sleep 2; done

attendre que la chaîne apparaisse dans le fichier

while ! grep "the line you're searching for" /path/to/the.file  
do sleep 10; done

https://superuser.com/a/743693/129669

2
Mykhaylo Adamovych

Vous n'avez pas nécessairement besoin de queue pour le faire. Je pense que la commande watch est ce que vous recherchez. La commande watch surveille la sortie d'un fichier et peut être terminée avec l'option - g lorsque la sortie est modifiée.

watch -g grep -m 1 "Server Started" logfile.log && Yournextaction
2
l1zard

Alex, je pense que celui-ci vous aidera beaucoup.

tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> /dev/null ;

cette commande ne donnera jamais une entrée sur le fichier journal mais grep silencieusement ...

1
Md. Mohsin Ali

La commande tail peut être mise en arrière-plan et son pid renvoyé au sous-shell grep. Dans le sous-shell grep, un gestionnaire d'interruption sur EXIT peut tuer la commande tail.

( (sleep 1; exec tail -f logfile.log) & echo $! ; wait ) | 
     (trap 'kill "$pid"' EXIT; pid="$(head -1)"; grep -m 1 "Server Started")
1
phio

Voici une solution bien meilleure qui ne vous oblige pas à écrire dans le fichier journal, ce qui est très dangereux, voire impossible dans certains cas.

sh -c 'tail -n +0 -f /tmp/foo | { sed "/EOF/ q" && kill $$ ;}'

Actuellement, il n'a qu'un seul effet secondaire: le processus tail restera en arrière-plan jusqu'à ce que la ligne suivante soit écrite dans le journal.

1
sorin

Les autres solutions ici ont plusieurs problèmes:

  • si le processus de journalisation est déjà en panne ou tombe en panne pendant la boucle, ils seront exécutés indéfiniment
  • éditer un journal qui ne devrait être consulté
  • écrire inutilement un fichier supplémentaire
  • ne permettant pas de logique supplémentaire

Voici ce que j'ai proposé d'utiliser l'exemple de Tomcat (supprimez les hachages si vous voulez voir le journal pendant son démarrage):

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"
}
1
user503391

Lisez-les tous. tldr: découpler la fin de tail de grep.

Les deux formes les plus pratiques sont

( tail -f logfile.log & ) | grep -q "Server Started"

et si vous avez bash

grep -m 1 "Server Started" <(tail -f logfile.log)

Mais si cette queue assise à l'arrière-plan vous dérange, il y a une meilleure façon de parler ici ou une autre réponse. Nécessite bash.

coproc grep -m 1 "Server Started"
tail -F /tmp/x --pid $COPROC_PID >&${COPROC[1]}

Ou si ce n'est pas la queue qui produit des choses,

coproc command that outputs
grep -m 1 "Sever Started" ${COPROC[0]}
kill $COPROC_PID
1
Ian Kelling

Vous voulez partir dès que la ligne est écrite, mais vous voulez aussi partir après un délai d'attente:

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
0
Adrien

Essayez d'utiliser inotify (inotifywait)

Vous configurez inotifywait pour tout changement de fichier, puis vérifiez le fichier avec grep. S'il ne le trouve pas, relancez-le inotifywait. S'il le trouve, quittez la boucle ... Smth like that

0
Evengard