Лог вращается с помощью Bash script

У меня есть следующая проблема:

У меня есть приложение, которое непрерывно выводит выходные данные на stderr и stdout. Выход этого приложения записывается в файл журнала (приложение перенаправляется как: &> log.txt). У меня нет никаких возможностей для создания правильного ведения журнала для этого.

Теперь у меня есть задание 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 для этого конкретного приложения не является опцией, есть целый механизм за кулисами, которые я не могу изменить.

Спасибо, е.

Ответ 1

Хорошо, вот идея, вдохновленная http://en.wikibooks.org/wiki/Bourne_Shell_Scripting/Files_and_streams

  • сделать именованный канал:

    mkfifo /dev/mypipe
    
  • перенаправить stdout и stderr в именованный канал:

    &> /dev/mypipe
    
  • прочитайте из файла mypipe в файл:

    cat < /dev/mypipe > /var/log/log.txt &
    
  • когда вам нужно лог-поворот, убить кошку, повернуть журнал и перезапустить cat.

Теперь я не тестировал это. Расскажите, как это происходит.

Примечание. Вы можете дать именованному каналу любое имя, например /var/tmp/pipe 1,/var/log/pipe,/tmp/abracadabra и т.д. Просто убедитесь, что заново создаете канал после загрузки до запуска журнала < script.


В качестве альтернативы, не используйте cat, а используйте простой файл script:

#!/bin/bash

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

Этот script гарантирует вывод для каждой прочитанной строки. (cat может не запускаться, пока его буфер не будет заполнен или не встретится с EOF)


Финал - и TESTED - попытка

ВАЖНОЕ ПРИМЕЧАНИЕ: Пожалуйста, прочитайте комментарии от @andrew ниже. Существует несколько ситуаций, о которых вам нужно знать.

Хорошо! Наконец получил доступ к моей Linux-коробке. Вот как:

Шаг 1: Сделайте этот рекордер script:

#!/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: включите рекордер:

  • Сделайте именованный канал:

    mkfifo $PIPENAME
    
  • Перенаправить приложение STDOUT и STDERR на именованный канал:

    ...things... &> $PIPENAME
    
  • Запустите рекордер:

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

    Вы можете захотеть nohup выше, чтобы выжить в результате выхода из системы.

  • Готово!

Шаг 3: Если вам нужно зайти в логротат, остановите рекордер:

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

Я предлагаю помещать вышеуказанные шаги в свой собственный script. Не стесняйтесь менять вторую строку на любой метод логарифмического вращения, который вы хотите использовать.


Примечание.. Если вам удобно программировать на С, вы можете сделать короткую программу на C для выполнения функции recorder.sh. Скомпилированные C-программы, безусловно, будут легче, чем nohup-ed отдельно bash script.


Примечание 2: Дэвид Ньюкомб предоставил полезное предупреждение в комментариях: пока рекордер не работает, записи в канал блокируются и могут привести к непредсказуемому сбою программы. Убедитесь, что рекордер выключен (или вращается) как можно короче.

Итак, если вы можете убедиться, что ротация происходит очень быстро, вы можете заменить sleep (встроенную команду, которая принимает только целые значения) с помощью /bin/sleep (программа, принимающая значения с плавающей запятой) и установить период ожидания до 0.5 или короче.

Ответ 2

Вы также можете подключить свой выход через утилиту Apache rotatelogs. Или после script:

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

Ответ 3

Прежде всего, вы действительно не должны изобретать квадратное колесо здесь. Вероятно, ваши сверстники против вращения журналов по ежедневному расписанию, которые автоматически применяются ко всем сценариям в /etc/logrotate.d/ - этого можно избежать, разместив script в другом месте.


Стандартный подход к вращению журнала, который реализован в logrotate, упоминается в https://serverfault.com/questions/55610/logrotate-and-open-files и есть:

MAXLOG=<maximum index of a log copy>
for i in `seq $((MAXLOG-1)) -1 1`; do mv "log."{$i,$((i+1))}; 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

Другой подход заключается в том, чтобы программа записи сама отслеживала размер журнала каждый раз, когда он записывает на него, и выполняет поворот. Это ограничивает ваши возможности в том, где вы можете писать, и в какой логике вращения сама программа способна, но имеет преимущества быть автономным решением и проверять размер журнала постоянно, а не по графику. В стандартных библиотеках многих языков есть такой объект. В качестве выходного решения это реализовано в Apache rotatelogs.

Ответ 4

В эти выходные я написал logrotee. Вероятно, я бы этого не сделал, если раньше читал @JdeBP отличный ответ о multilog.

Я сосредоточился на том, чтобы быть легким и быть в состоянии bzip2 его выходных блоков, таких как:

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

Там многое предстоит сделать и проверить.