77 votes

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

J'utilise tail -f pour surveiller un fichier journal dans lequel on écrit activement. Lorsqu'une certaine chaîne est écrite dans le fichier journal, je veux arrêter la surveillance et continuer avec 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 s'arrête comme prévu, mais je dois trouver un moyen de faire en sorte que les tail quitter aussi pour que le script puisse continuer.

67voto

Rob Whelan Points 789

La réponse acceptée ne fonctionne pas pour moi, de plus elle est confuse et elle modifie 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 du journal correspond au modèle, tuer la commande tail lancé par ce script.

Note : si vous souhaitez également visualiser la sortie sur l'écran, soit | tee /dev/tty ou faire un écho de la ligne avant le test dans la boucle while.

21voto

Matt Eberts Points 21

Si vous utilisez Bash (du moins, mais il semble qu'il ne soit pas défini par POSIX, donc il peut manquer dans certains shells), vous pouvez utiliser la syntaxe suivante

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

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

17voto

thabubble Points 745

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

Mauvaise approche : Force tail pour écrire une autre ligne

Vous pouvez forcer tail pour écrire une autre ligne de sortie immédiatement après grep a trouvé une correspondance et est sorti. Cela entraînera tail pour obtenir un SIGPIPE ce qui entraîne sa sortie. Une façon d'y parvenir est de modifier le fichier surveillé par le programme tail après grep sortent.

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 avant que grep a fermé son stdout, donc tail ne sera probablement pas en mesure d'écrire sur le tuyau avant que grep a eu l'occasion de fermer son stdin. cat est utilisé pour propager la sortie standard de grep non modifié.

Cette approche est relativement simple, mais elle présente plusieurs inconvénients :

  • Si grep ferme stdout avant de fermer stdin, il y aura toujours une condition de course : grep ferme le stdout, déclenchant cat pour sortir, ce qui déclenche echo , déclenchement tail pour sortir une ligne. Si cette ligne est envoyée à grep avant grep a eu l'occasion de fermer stdin, tail n'obtiendra pas le SIGPIPE jusqu'à ce qu'il écrive une autre ligne.
  • Il nécessite un accès en écriture au fichier journal.
  • Vous devez être d'accord pour 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 fait apparaître une nouvelle ligne au milieu d'un message du journal).
  • Cette approche est spécifique à tail -il ne fonctionnera pas avec d'autres programmes.
  • Le troisième étage du pipeline rend difficile l'accès au code de retour du deuxième étage du pipeline (à moins que vous n'utilisiez une extension POSIX telle que bash 's PIPESTATUS réseau). 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 intéresse (par exemple, quelque chose qui retourne 0 lorsque "le serveur a démarré" est détecté, 1 lorsque "le serveur n'a pas réussi à démarrer" 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 à l'exécution de continuer une fois que grep retours. 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 peut être supprimé 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 services publics que tail
  • il ne souffre pas d'une condition de course
  • vous pouvez facilement obtenir la valeur de retour de grep (ou toute autre commande alternative que vous utilisez)

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

Approche alternative : Envoyer un message pour tuer tail

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

Avec un pipeline comme tail -f ... | grep ... il est facile de modifier la première étape du pipeline pour économiser de l'argent. tail dans une variable en arrière-plan. tail et la lecture $! . Il est également facile de modifier la deuxième étape du pipeline pour exécuter kill cuando grep sorties. Le problème est que les deux étapes du pipeline s'exécutent dans des "environnements d'exécution" distincts (selon la terminologie de la norme POSIX), de sorte que la deuxième étape du pipeline ne peut lire aucune des variables définies par la première étape du pipeline. Sans utiliser les variables Shell, soit la deuxième étape doit d'une manière ou d'une autre comprendre tail pour qu'il puisse tuer tail cuando grep ou la première étape doit d'une manière ou d'une autre être notifiée quand grep retours.

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

Le premier étage pourrait envoyer le PID au deuxième étage via le tuyau en echo dans le PID, mais cette chaîne de caractères sera mélangée avec tail de la sortie de l'entreprise. Démultiplexer les deux peut nécessiter un schéma d'échappement complexe, en fonction de la sortie de tail .

Vous pouvez utiliser un FIFO pour que le deuxième étage de pipeline notifie le premier étage de pipeline lorsque grep sortent. 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 inconvénients de l'approche précédente, sauf qu'elle est plus compliquée.

Avertissement sur la mise en mémoire tampon

POSIX permet aux flux stdin et stdout d'être entièrement mis en mémoire tampon, ce qui signifie que tail peut ne pas être traitée par grep pendant un temps arbitrairement long. Il ne devrait pas y avoir de problèmes sur les systèmes GNU : GNU grep utilise read() qui évite toute mise en mémoire tampon, et GNU tail -f fait des appels réguliers à 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.

10voto

Elifarley Points 578

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

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

La fonction bash ci-dessous bloquera jusqu'à ce que le terme de recherche donné apparaisse ou qu'un délai 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 qu'il apparaisse avant de rechercher la chaîne de caractères :

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"

7voto

Robin Menetrey Points 21

Actuellement, comme indiqué, tous les tail -f Les solutions de ce type courent le risque de récupérer une ligne "Server Started" précédemment enregistrée (ce qui peut ou non être un problème dans votre cas spécifique, selon le nombre de lignes enregistrées et la rotation/troncature du fichier journal).

Plutôt que de compliquer les choses à l'excès, il suffit d'utiliser une méthode plus intelligente. tail comme bmike montré avec un snippit perl. La solution la plus simple est la suivante retail qui a intégré le support des regex avec commencer y arrêter les modèles de condition :

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

Cela suivra le fichier comme une tail -f jusqu'à ce que le premier nouveau instance de cette chaîne apparaît, puis quittez. (La -u ne se déclenche pas sur les lignes existantes dans les 10 dernières lignes du fichier en mode normal "follow").


Si vous utilisez GNU tail (de coreutils ), l'option suivante la plus simple est d'utiliser --pid et un FIFO (nommé pipe) :

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 afin d'obtenir et de transmettre un PID. Une FIFO souffre toujours du même problème d'attente d'une écriture opportune pour causer des pertes de données. tail pour recevoir un SIGPIPE utilisez le --pid de sorte que tail sort lorsqu'il remarque que grep s'est terminé (conventionnellement utilisé pour surveiller la écrivain plutôt que le lecteur mais tail ne s'en soucie pas vraiment). Option -n 0 est utilisé avec tail afin que les anciennes lignes ne déclenchent pas de correspondance.


Enfin, vous pouvez utiliser un queue stateful Cette fonction stocke le décalage actuel du fichier de sorte que les invocations suivantes ne montrent que les nouvelles lignes (elle gère également la rotation du 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

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

Plutôt que d'avoir une boucle qui accapare le CPU, comparez l'horodatage du fichier avec le fichier d'état ( .${LOGFILE}.off ), et le sommeil. Utilisez " -T "pour spécifier l'emplacement du fichier d'état si nécessaire, ce qui précède suppose le répertoire courant. N'hésitez pas à sauter cette condition, ou sous Linux, vous pouvez utiliser la méthode 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

SistemesEz.com

SystemesEZ est une communauté de sysadmins où vous pouvez résoudre vos problèmes et vos doutes. Vous pouvez consulter les questions des autres sysadmins, poser vos propres questions ou résoudre celles des autres.

Powered by:

X