使用 Bash 脚本进行日志轮换

发布于 2024-11-03 03:25:07 字数 572 浏览 0 评论 0原文

我有以下问题:

我有一个应用程序,它不断向 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 不是一个选项,幕后有一个我可能无法更改的完整机制。

谢谢, f.

I have the following issue:

I have an application, which continuously produces output to stderr and stdout. The output of this application is captured in a logfile (the app is redirected as: &> log.txt ). I don't have any options to produce a proper logging to file for this.

Now, I have a cron job, which runs every hour and beside of doing other things, it also tries to rotate this logfile above, by copying it to log.txt.1 and then creates an empty file and copies it to log.txt

It looks like:

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

The problem is, that the application is still writing to it, and because of this I get some very strange stuff in the log.txt.1, it starts with a lot of garbage characters, and the actual log file is somewhere at the end.

Do you have any idea, how to make a correct log rotating for this specific situation (I also tried cat log.txt > log.txt.1, does not work)? Using logrotate for this specific application not an option, there is a whole mechanism behind the scenes that I may not change.

Thanks,
f.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

海未深 2024-11-10 03:25:07

好的,这是一个想法,灵感来自 http://en.wikibooks.org/wiki/Bourne_Shell_Scripting/ Files_and_streams

  1. 创建命名管道:

    mkfifo /dev/mypipe
    
  2. 将 stdout 和 stderr 重定向到命名管道:

    <前><代码>&> /dev/mypipe

  3. 从 mypipe 读取到文件中:

    猫< /dev/mypipe > /var/log/log.txt &
    
  4. 当你需要日志轮换时,杀死猫,轮换日志,然后重新启动猫。

现在,我还没有测试过这个。告诉我们进展如何。

注意:您可以为命名管道指定任何名称,例如 /var/tmp/pipe1 、 /var/log/pipe 、 /tmp/abracadabra 等。只需确保在启动后在日志记录脚本运行之前重新创建管道即可。


或者,不要使用 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 和 STDOUT。 STDERR 到命名管道:

    ...事物... &> $管道名称
    
  3. 启动记录器:

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

    您可能需要 nohup 上面的内容,使其在注销后仍能生存。

  4. 完成!

第3步:如果需要logrotate,请暂停记录器:

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

我建议将上述步骤放入其自己的脚本中。请随意将第二行更改为您想要使用的任何日志旋转方法。


注意:如果您擅长 C 编程,您可能想要编写一个简短的 C 程序来执行 recorder.sh 的功能。编译的 C 程序肯定会比 nohup 分离的 bash 脚本更轻。


注释 2: David Newcomb 在评论中提供了有用的警告:当记录器未运行时,写入管道将被阻塞,并且可能< em>导致程序不可预测地失败。确保录音机关闭(或旋转)的时间尽可能短。

因此,如果您可以确保旋转确实很快,您可以替换睡眠(仅接受整数值的内置命令)与 /bin/sleep(接受浮点值的程序)并将睡眠周期设置为 0.5 或更短。

Okay, here's an idea, inspired by http://en.wikibooks.org/wiki/Bourne_Shell_Scripting/Files_and_streams

  1. make a named pipe:

    mkfifo /dev/mypipe
    
  2. redirect stdout and stderr to the named pipe:

    &> /dev/mypipe
    
  3. read from mypipe into a file:

    cat < /dev/mypipe > /var/log/log.txt &
    
  4. when you need to log-rotate, kill the cat, rotate the log, and restart the cat.

Now, I haven't tested this. Tell us how it goes.

Note: you can give the named pipe any name, like /var/tmp/pipe1 , /var/log/pipe , /tmp/abracadabra , and so on. Just make sure to re-create the pipe after booting before your logging-script runs.


Alternatively, don't use cat, but use a simple script file:

#!/bin/bash

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

This script guarantees an output for every newline read. (cat might not start outputting until its buffer is full or it encounters an EOF)


Final -- and TESTED -- attempt

IMPORTANT NOTE: Please read the comments from @andrew below. There are several situations which you need to be aware of.

Alright! Finally got access to my Linux box. Here's how:

Step 1: Make this recorder 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

Step 2: put the recorder into work:

  1. Make a named pipe:

    mkfifo $PIPENAME
    
  2. Redirect your application's STDOUT & STDERR to the named pipe:

    ...things... &> $PIPENAME
    
  3. Start the recorder:

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

    You might want to nohup the above to make it survive logouts.

  4. Done!

Step 3: If you need to logrotate, pause the recorder:

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

I suggest putting the above steps into its own script. Feel free to change the 2nd line to whatever log-rotating method you want to use.


Note : If you're handy with C programming, you might want to make a short C program to perform the function of recorder.sh. Compiled C programs will certainly be lighter than a nohup-ed detached bash script.


Note 2: David Newcomb provided a helpful warning in the comments: While the recorder is not running then writes to the pipe will block and may cause the program to fail unpredictably. Make sure the recorder is down (or rotating) for as short time as possible.

So, if you can ensure that rotating happens really quickly, you can replace sleep (a built-in command which accepts only integer values) with /bin/sleep (a program that accepts float values) and set the sleep period to 0.5 or shorter.

生死何惧 2024-11-10 03:25:07

首先,你真的不应该在这里重新发明方轮。您的同事可能反对按自动应用的每日计划轮换日志到 /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

另一种方法是让写入程序本身在每次写入时跟踪日志大小并进行轮换。这限制了您可以写入的位置以及程序本身支持的循环逻辑的选择。但它的优点是作为一个独立的解决方案,并在每次写入时检查日志大小,而不是按计划检查日志大小。许多语言的标准库都有这样的功能。作为一种嵌入式解决方案,这是在 Apache 的 rotatelogs

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

First of all, you really should not reinvent the square wheel here. Your peers are probably against rotating the logs on daily schedule which automatically applies to all scripts in /etc/logrotate.d/ - this can be avoided by placing the script elsewhere.


Now, the standard approach to log rotation (that is implemented in logrotate) can be implemented by any other facility just as well. E.g. here's a sample implementation in 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>

The last item is done by sending SIGHUP or (less often) SIGUSR1 and having a signal handler in the daemon that replaces the corresponding file descriptor or variable. This way, the switch is atomic, so there's no interruption in logging availability. In bash, this would look like:

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

The other approach is to make the writing program itself keep track of the log size each time it writes to it and do the rotation. This limits your options in where you can write to and what rotation logic is to what the program itself supports. But it has the benefit of being a self-contained solution and checking the log size at each write rather than on schedule. Many languages' standard libraries have such a facility. As a drop-in solution, this is implemented in Apache's rotatelogs:

<your_program> 2>&1 | rotatelogs <opts> <logfile> <rotation_criteria>
吹泡泡o 2024-11-10 03:25:07

这个周末我写了一个logrotee。如果我之前读过 @JdeBP 的 关于 multilog 的精彩答案,我可能不会。

我专注于它的轻量级并且能够对其输出块进行 bzip2,例如:

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

不过,还有很多工作要做和测试。

I wrote a logrotee this weekend. I probably wouldn't if I've read @JdeBP's great answer about multilog before.

I focused on it being lightweight and being able to bzip2 its output chunks like:

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

There's a lot of to be done and tested yet, though.

愿与i 2024-11-10 03:25:07

您还可以通过 Apacherotatelogs 实用程序传输输出。
或以下脚本:

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

You can also pipe your output thru Apache rotatelogs utility.
Or following 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
等你爱我 2024-11-10 03:25:07

您可以利用 rotatelogs此处的文档 )。该实用程序会将脚本的标准输出与日志文件分离,以透明的方式管理轮换。例如:

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

当输出文件达到100M时会自动轮换(可以配置根据时间间隔轮换)。

You can leverage rotatelogs (docs here). This utility will decouple your script's stdout from the log file, managing the rotation in a transparent way. For example:

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

will automatically rotate the output file when it reaches 100M (can be configured to rotate based on a time interval).

吃素的狼 2024-11-10 03:25:07

最简单的脚本可能是这样的rotatelog.sh:

#! /bin/bash

#DATE_FMT="%Y%m%d-%H%M" # for testing
#DATE_FMT="%Y%m%d-%H"   # rotate each hour
DATE_FMT="%Y%m%d"       # rotate each day

if [ "$1" != "" ]
then
        f=$1
else
        f="rotatelog"
fi

p=$(date +${DATE_FMT})

r=$f-$p.log
exec 2>&1 > $r

while read l
do
        d=$(date +${DATE_FMT})
        if [ $p != $d ]
        then
                x=$r
                p=$d
                r=$f-$p.log
                exec 2>&1 > $r
                gzip $x
        fi
        echo $l
done

您可以使用如下内容:

your_process | rotatelog.sh yout_log_path_pattern

A simplest script could be something like this rotatelog.sh:

#! /bin/bash

#DATE_FMT="%Y%m%d-%H%M" # for testing
#DATE_FMT="%Y%m%d-%H"   # rotate each hour
DATE_FMT="%Y%m%d"       # rotate each day

if [ "$1" != "" ]
then
        f=$1
else
        f="rotatelog"
fi

p=$(date +${DATE_FMT})

r=$f-$p.log
exec 2>&1 > $r

while read l
do
        d=$(date +${DATE_FMT})
        if [ $p != $d ]
        then
                x=$r
                p=$d
                r=$f-$p.log
                exec 2>&1 > $r
                gzip $x
        fi
        echo $l
done

You can use like:

your_process | rotatelog.sh yout_log_path_pattern
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文