web-dev-qa-db-ja.com

Bashスクリプトでローテーションするログ

私は次の問題を抱えています:

Stderrとstdoutへの出力を継続的に生成するアプリケーションがあります。このアプリケーションの出力はログファイルにキャプチャされます(アプリは次のようにリダイレクトされます:&> log.txt)。このためにファイルへの適切なログを生成するオプションがありません。

これで、1時間ごとに実行されるcronジョブがあり、他のことを行う以外に、log.txt.1にコピーして上記のログファイルをローテーションしようとし、空のファイルを作成してlog.txtにコピーします。

次のようになります。

cp log.txt log.txt.1
touch /tmp/empty
cp /tmp/empty log.txt

問題は、アプリケーションがまだ書き込みを行っていることです。このため、log.txt.1に非常に奇妙なものが含まれています。これは、多くのガベージ文字で始まり、実際のログファイルは最後のどこかにあります。 。

この特定の状況に合わせて正しいログをローテーションさせる方法について何か考えがありますか(私もcat log.txt > log.txt.1、 動作しません)?この特定のアプリケーションにオプションではなくlogrotateを使用すると、変更できない可能性のあるメカニズム全体が舞台裏にあります。

ありがとう。

18
Ferenc Deak

さて、ここにインスピレーションを得たアイデアがあります http://en.wikibooks.org/wiki/Bourne_Shell_Scripting/Files_and_streams

  1. 名前付きパイプを作成します。

    mkfifo /dev/mypipe
    
  2. stdoutとstderrを名前付きパイプにリダイレクトします。

    &> /dev/mypipe
    
  3. mypipeからファイルに読み込む:

    cat < /dev/mypipe > /var/log/log.txt &
    
  4. ログローテーションが必要な場合は、猫を殺し、ログを回転させて、猫を再起動します。

今、私はこれをテストしていません。どうなるか教えてください。

注:名前付きパイプには、/ var/tmp/pipe1、/ var/log/pipe、/ tmp/abracadabraなどの任意の名前を付けることができます。起動後、logging-scriptが実行される前にパイプを再作成してください。


または、catを使用せずに、単純なスクリプトファイルを使用します。

#!/bin/bash

while : ; do
  read line
  printf "%s\n" "$line"
done

このスクリプトは、改行が読み取られるたびに出力を保証します。 (catは、バッファがいっぱいになるか、EOFに遭遇するまで出力を開始しない場合があります)


最終-およびテスト済み-試行

重要な注意:以下の@ andrewからのコメントをお読みください。知っておく必要のある状況がいくつかあります。

よし!ついに私のLinuxボックスにアクセスできるようになりました。方法は次のとおりです。

ステップ1:このレコーダースクリプトを作成します:

#!/bin/bash

LOGFILE="/path/to/log/file"
SEMAPHORE="/path/to/log/file.semaphore"

while : ; do
  read line
  while [[ -f $SEMAPHORE ]]; do
    sleep 1s
  done
  printf "%s\n" "$line" >> $LOGFILE
done

ステップ2:レコーダーを稼働させます:

  1. 名前付きパイプを作成します。

    mkfifo $PIPENAME
    
  2. アプリケーションのSTDOUTとSTDERRを名前付きパイプにリダイレクトします。

    ...things... &> $PIPENAME
    
  3. レコーダを起動します。

    /path/to/recorder.sh < $PIPENAME &
    

    上記をNohupして、ログアウト後も存続させることができます。

  4. 完了!

ステップ3:ログローテーションが必要な場合は、レコーダーを一時停止します。

touch /path/to/log/file.semaphore
mv /path/to/log/file /path/to/archive/of/log/file
rm /path/to/log/file.semaphore

上記の手順を独自のスクリプトに組み込むことをお勧めします。 2行目を、使用するログローテーション方式に自由に変更してください。


注: Cプログラミングが便利な場合は、recorder.shの機能を実行するための短いCプログラムを作成することをお勧めします。コンパイルされたCプログラムは、Nohupでデタッチされたbashスクリプトよりも確かに軽量です。


注2: David Newcombはコメントで役立つ警告を提供しました:レコーダーが実行されていない間、パイプに書き込みますブロックし、プログラムが予期せず失敗する可能性があります。レコーダーができるだけ短時間ダウン(または回転)していることを確認してください。

したがって、回転が本当にすばやく発生することを確認できる場合は、sleep(整数値のみを受け入れる組み込みコマンド)を/bin/sleep(プログラム)に置き換えることができます。 float値を受け入れます)、スリープ期間を0.5以下に設定します。

10
pepoluan

まず第一に、ここで四角いホイールを再発明するべきではありません。あなたの仲間はおそらく反対しています /etc/logrotate.d/のすべてのスクリプトに自動的に適用される毎日のスケジュールでログをローテーションする -これはスクリプトを別の場所に配置することで回避できます。


さて、 ログローテーションへの標準的なアプローチlogrotateに実装されています)は、他のどの機能でも同様に実装できます。例えば。 bashのサンプル実装は次のとおりです。

MAXLOG=<maximum index of a log copy>
for i in `seq $((MAXLOG-1)) -1 1`; do
    mv "log."{$i,$((i+1))}    #will need to ignore file not found errors here
done 
mv log log.1    # since a file descriptor is linked to an inode rather than path,
                #if you move (or even remove) an open file, the program will continue
                #to write into it as if nothing happened
                #see https://stackoverflow.com/questions/5219896/how-do-the-unix-commands-mv-and-rm-work-with-open-files
<make the daemon reopen the log file with the old path>

最後の項目は、SIGHUPまたは(それほど頻繁ではありませんが)SIGUSR1を送信し、対応するファイル記述子または変数を置き換えるシグナルハンドラーをデーモンに含めることによって行われます。このように、スイッチはアトミックであるため、ロギングの可用性が中断されることはありません。 bashでは、これは次のようになります。

trap { exec &>"$LOGFILE"; } HUP

もう1つのアプローチは、書き込みプログラム自体が、書き込みプログラムとローテーションを実行するたびにログサイズを追跡するようにすることです。これにより、書き込み可能な場所とローテーションロジックのオプションが、プログラム自体がサポートするものに制限されます。ただし、自己完結型のソリューションであり、スケジュールではなく書き込みごとにログサイズをチェックするという利点があります。多くの言語の標準ライブラリにはそのような機能があります。ドロップインソリューションとして、これはApacheの rotatelogs に実装されています。

<your_program> 2>&1 | rotatelogs <opts> <logfile> <rotation_criteria>
2
ivan_pozdeev

rotatelogsここにドキュメント )を活用できます。このユーティリティは、スクリプトのstdoutをログファイルから切り離し、透過的な方法でローテーションを管理します。例えば:

your_script.sh | rotatelogs /var/log/your.log 100M

100Mに達すると、出力ファイルが自動的に回転します(時間間隔に基づいて回転するように構成できます)。

0
Cavaz

私は logrotee 今週末に書いた。 @JdeBPの multilogについての素晴らしい答え を以前に読んだことがあるなら、私はおそらく読まないでしょう。

私はそれが軽量で、次のように出力チャンクをbzip2できることに焦点を合わせました。

verbosecommand | logrotee  \
  --compress "bzip2 {}" --compress-suffix .bz2 \
  /var/log/verbosecommand.log

ただし、まだ実行してテストする必要のあることがたくさんあります。

0

Apacheのrotatelogsユーティリティを介して出力をパイプすることもできます。または次のスクリプト:

#!/bin/ksh
#rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]

numberOfFiles=10

while getopts "n:fltvecp:L:" opt; do
    case $opt in
  n) numberOfFiles="$OPTARG"
    if ! printf '%s\n' "$numberOfFiles" | grep '^[0-9][0-9]*$' >/dev/null; then
      printf 'Numeric numberOfFiles required %s. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$numberOfFiles" 1>&2
      exit 1
    Elif [ $numberOfFiles -lt 3 ]; then
      printf 'numberOfFiles < 3 %s. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$numberOfFiles" 1>&2
    fi
  ;;
  *) printf '-%s ignored. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$opt" 1>&2
  ;;
  esac
done
shift $(( $OPTIND - 1 ))

pathToLog="$1"
fileSize="$2"

if ! printf '%s\n' "$fileSize" | grep '^[0-9][0-9]*[BKMG]$' >/dev/null; then
  printf 'Numeric fileSize followed by B|K|M|G required %s. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$fileSize" 1>&2
  exit 1
fi

sizeQualifier=`printf "%s\n" "$fileSize" | sed "s%^[0-9][0-9]*\([BKMG]\)$%\1%"`

multip=1
case $sizeQualifier in
B) multip=1 ;;
K) multip=1024 ;;
M) multip=1048576 ;;
G) multip=1073741824 ;;
esac

fileSize=`printf "%s\n" "$fileSize" | sed "s%^\([0-9][0-9]*\)[BKMG]$%\1%"`
fileSize=$(( $fileSize * $multip ))
fileSize=$(( $fileSize / 1024 ))

if [ $fileSize -le 10 ]; then
  printf 'fileSize %sKB < 10KB. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$fileSize" 1>&2
  exit 1
fi

if ! touch "$pathToLog"; then
  printf 'Could not write to log file %s. rotatelogs.sh -n numberOfFiles pathToLog fileSize[B|K|M|G]\n' "$pathToLog" 1>&2
  exit 1
fi

lineCnt=0
while read line
do
  printf "%s\n" "$line" >>"$pathToLog"
  lineCnt=$(( $lineCnt + 1 ))
  if [ $lineCnt -gt 200 ]; then
    lineCnt=0
    curFileSize=`du -k "$pathToLog" | sed -e 's/^[  ][  ]*//' -e 's%[   ][  ]*$%%' -e 's/[  ][  ]*/[    ]/g' | cut -f1 -d" "`
    if [ $curFileSize -gt $fileSize ]; then
      DATE=`date +%Y%m%d_%H%M%S`
      cat "$pathToLog" | gzip -c >"${pathToLog}.${DATE}".gz && cat /dev/null >"$pathToLog"
      curNumberOfFiles=`ls "$pathToLog".[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9].gz | wc -l | sed -e 's/^[   ][  ]*//' -e 's%[   ][  ]*$%%' -e 's/[  ][  ]*/[    ]/g'`
      while [ $curNumberOfFiles -ge $numberOfFiles ]; do
        fileToRemove=`ls "$pathToLog".[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9].gz | head -1`
        if [ -f "$fileToRemove" ]; then
          rm -f "$fileToRemove"
          curNumberOfFiles=`ls "$pathToLog".[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]_[0-9][0-9][0-9][0-9][0-9][0-9].gz | wc -l | sed -e 's/^[   ][  ]*//' -e 's%[   ][  ]*$%%' -e 's/[  ][  ]*/[    ]/g'`
        else
          break
        fi
      done
    fi
  fi
done
0
bravomail