使用tee(或等效工具)但限制最大文件大小或旋转到新文件。

33

我想从一个UNIX进程中捕获输出,但限制最大文件大小并/或旋转到新文件。

我看过logrotate,但它不是实时工作的。据我了解,它是一个并行运行的“清理”任务。

什么是正确的解决方案?我想我会编写一个小脚本来完成这个任务,但我希望能有现有的文本工具简单地解决问题。

想象一下:

my_program | tee --max-bytes 100000 log/my_program_log

将产生以下结果... 始终将最新的日志文件写入: log/my_program_log

然后,当它填满时...重命名为log/my_program_log000001,并开始一个新的 log/my_program_log。

7个回答

33

使用split:

my_program | tee >(split -d -b 100000 -)

如果您不想看到输出,可以直接使用管道符号进行分割:
my_program | split -d -b 100000 -

关于日志轮换,coreutils 中没有自动执行该操作的工具。您可以创建一个符号链接,并使用 bash 命令定期更新它:

while ((1)); do ln -fns target_log_name $(ls -t | head -1); sleep 1; done

1
啊...我忘记了Bash(和其他一些shell)中的>()运算符。我使用它的频率太低了。你的回答是最简洁的。 - kevinarpe
对于使用 tee 的第一种解决方案,我为什么不应该使用 my_program | tee | split -d -b 100000 - - asafc
为了得到一个良好的文件命名,例如使用 tee >(split --additional-suffix=.log -d -b 1000000 - debug.0) - rubo77
使用tee和split的绝妙方法。 - deltaray
1
@asafc:tee 读取 stdin 并写入两个副本,一个写入文件,另一个写入 teestdout。需要拆分/旋转的是文件输出。您的建议将消耗用于在终端上显示的 stdout 副本,从而破坏使用 tee 的整个原因。 - Ben Voigt
此外,如果您正在处理基于行的文本内容,并且不想在输出文件中断开行,请使用“-l <lines>”而不是“-b <bytes>”。 - weiresr

8
在软件包apache2-utils中存在一个名为rotatelogs的实用程序,它完全符合您的要求。
简介:
rotatelogs [ -l ] [ -L linkname ] [ -p program ] [ -f ] [ -t ] [ -v ] [ -e ] [ -c ] [ -n number-of-files ] logfile rotationtime|filesize(B|K|M|G) [ offset ]
示例:
your_program | rotatelogs -n 5 /var/log/logfile 1M

您可以在此链接上阅读完整的手册。

使用“-n 5”参数,程序每次启动时都会开始写入第一个文件。相反,如果您设置为“-n 1”,它将每次写入同一文件,从而为我们提供连续的日志记录。 - Utkarsh Naiknaware

5
或者使用awk
program | awk 'BEGIN{max=100} {n+=length($0); print $0 > "log."int(n/max)}'

它会让行之间保持在一起,因此最大值不是精确的,但这对于日志记录来说是很好的。您可以使用awk的sprintf格式化文件名。

这是一个可管道化的脚本,使用awk实现。

#!/bin/bash
maxb=$((1024*1024))    # default 1MiB
out="log"              # output file name
width=3                # width: log.001, log.002
while getopts "b:o:w:" opt; do
  case $opt in
    b ) maxb=$OPTARG;;
    o ) out="$OPTARG";;
    w ) width=$OPTARG;;
    * ) echo "Unimplented option."; exit 1
  esac
done
shift $(($OPTIND-1))

IFS='\n'              # keep leading whitespaces
if [ $# -ge 1 ]; then # read from file
  cat $1
else                  # read from pipe
  while read arg; do
    echo $arg
  done
fi | awk -v b=$maxb -v o="$out" -v w=$width '{
    n+=length($0); print $0 > sprintf("%s.%0.*d",o,w,n/b)}'

将这段代码保存到名为“bee”的文件中,运行“chmod +x bee”,然后您就可以使用它了。

program | bee

或者将现有文件分割为

bee -b1000 -o proglog -w8 file

1
我同意你的评论:“它保持行在一起,所以最大值不是精确的,但这对于日志记录非常有用。” - kevinarpe

5

为了将大小限制在100字节以内,你可以简单地使用dd命令:

my_program | dd bs=1 count=100 > log

当写入100字节时,dd将关闭管道,my_program会收到EPIPE错误。

2
这个问题最直接的解决方法可能是使用Python和专为此目的设计的日志记录模块。创建一个从stdin读取并写入stdout的脚本,并实现下面描述的日志轮换。
"logging"模块提供
class logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0,
              backupCount=0, encoding=None, delay=0)

这段文字的意思是:“它正好可以做到你所要求的那样。你可以使用maxBytes和backupCount值来允许文件在预定大小时翻转。此外,当创建了一定数量的文件后,你可能希望将文件进行轮换,以便保持文件数目和文件大小都有限制。对于这种用法模式,logging包提供了RotatingFileHandler。”出处为“docs.python.org”。
import glob
import logging
import logging.handlers

LOG_FILENAME = 'logging_rotatingfile_example.out'

# Set up a specific logger with our desired output level
my_logger = logging.getLogger('MyLogger')
my_logger.setLevel(logging.DEBUG)

# Add the log message handler to the logger
handler = logging.handlers.RotatingFileHandler(
              LOG_FILENAME, maxBytes=20, backupCount=5)

my_logger.addHandler(handler)

# Log some messages
for i in range(20):
    my_logger.debug('i = %d' % i)

# See what files are created
logfiles = glob.glob('%s*' % LOG_FILENAME)

for filename in logfiles:
    print(filename)

结果应该是6个单独的文件,每个文件都包含应用程序日志历史的一部分:
logging_rotatingfile_example.out
logging_rotatingfile_example.out.1
logging_rotatingfile_example.out.2
logging_rotatingfile_example.out.3
logging_rotatingfile_example.out.4
logging_rotatingfile_example.out.5

最新的文件始终为logging_rotatingfile_example.out,每次达到大小限制时,它都会被重命名为后缀.1。现有的每个备份文件都会被重新命名以增加后缀(.1变为.2等),并且.6文件会被删除。
显然,此示例将日志长度设置得太小了,这是一个极端的例子。您应该将maxBytes设置为适当的值。

2
我很困惑。我的程序不是Python。这对我有什么帮助?我想使用标准的GNU coreutils:awk/tee/split等。 - kevinarpe

1

另一种解决方案是使用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

这个脚本并不是一见钟情,但是为了安装rotatelog而不得不安装apache让我重新审视它,我必须说它相当不错! - Red Pill
性能:当我让一个测试应用程序向标准输出发送大量信息并计算记录数时,我发现使用rotatelog与testapp > mylogfile没有任何区别。然而,使用脚本我们可以在时间间隔内吞下并写入约40%的记录。 - Red Pill

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接