将子进程的stdout/stderr输出到有限大小的日志文件

1 投票
3 回答
2026 浏览
提问于 2025-04-17 00:10

我有一个程序,它会不停地往 stderr 发送信息,我想把这些信息记录到一个文件里。

foo 2> /tmp/foo.log

其实我是在用 Python 的 subprocess.Popen 来启动这个程序,不过为了这个问题,直接从命令行启动也没问题。

with open('/tmp/foo.log', 'w') as stderr:
  foo_proc = subprocess.Popen(['foo'], stderr=stderr)

问题是,过几天我的日志文件可能会变得非常大,比如超过 500 MB。我想保留所有的 stderr 信息,但只想要最近的内容。怎么才能把日志文件的大小限制在 1 MB 左右呢?这个文件应该像一个循环缓冲区,最近的信息会被写入,但旧的信息会被删除,这样文件的大小就不会超过设定的限制。

我不太确定是否已经有优雅的 Unix 方法可以做到这一点,可能我还不知道,有什么特殊的文件可以用。

如果能用日志轮换的方式解决我的需求,那也可以,只要不需要中断正在运行的程序就行。

3 个回答

1

你可以利用“打开文件描述”的一些特性(这和“打开文件描述符”是不同的,但有很大关系)。特别是,当前的写入位置是和打开的文件描述关联的,所以两个共享同一个打开文件描述的进程,可以各自调整写入位置。

在这种情况下,原始进程可以保留子进程的标准错误文件描述符,并且定期在写入位置达到1 MiB时,将指针重新定位到文件的开头,这样就能实现你想要的循环缓冲区效果。

最大的挑战是确定当前消息的写入位置,这样你才能从最旧的内容(就在文件位置前面)读取到最新的内容。新写入的行不太可能完全覆盖旧的行,所以会有一些残留。你可以尝试在子进程的每一行前加上一个已知的字符序列(比如“XXXXXX”),然后让子进程的每次写入都覆盖之前的标记……但这需要对正在运行的程序有控制权。如果你无法控制这个程序,或者不能修改它,那这个方法就不行了。

另一种选择是定期截断文件(也许在复制之后),让子进程以追加模式写入(因为文件在父进程中是以追加模式打开的)。你可以安排在截断之前将文件中的内容复制到一个备用文件,以保留之前的1 MiB数据。这样你可能会使用到最多2 MiB,这比500 MiB要好得多,如果你真的空间不够,大小也可以调整。

祝你玩得开心!

1

使用循环缓冲区的方法会比较难实现,因为每当有数据被删除时,你就得重新写整个文件。

使用logrotate或者类似的方式会更合适。在这种情况下,你可以这样做:

import subprocess
import signal

def hupsignal(signum, frame):
    global logfile
    logfile.close()
    logfile = open('/tmp/foo.log', 'a')

logfile = open('/tmp/foo.log', 'a')
signal.signal()
foo_proc = subprocess.Popen(['foo'], stderr=subprocess.PIPE)
for chunk in iter(lambda: foo_proc.stderr.read(8192), ''):
    # iterate until EOF occurs
    logfile.write(chunk)
    # or do you want to rotate yourself?
    # Then omit the signal stuff and do it here.
    # if logfile.tell() > MAX_FILE_SIZE:
    #     logfile.close()
    #     logfile = open('/tmp/foo.log', 'a')

这不是一个完整的解决方案,可以把它当作伪代码来看,因为我没有测试过,也不太确定某些地方的语法。可能需要一些修改才能让它工作。但你应该能理解这个思路。

这也是一个如何使用logrotate的例子。当然,如果需要的话,你也可以自己手动旋转日志文件。

3

你可以使用标准库里的日志包来实现这个功能。与其直接把子进程的输出连接到一个文件,不如这样做:

import logging

logger = logging.getLogger('foo')

def stream_reader(stream):
    while True:
        line = stream.readline()
        logger.debug('%s', line.strip())

这样做会记录从流中接收到的每一行信息,你可以用一个叫 RotatingFileHandler 的工具来设置日志,这个工具可以让日志文件自动轮换。然后你就可以安排读取这些数据并进行记录。

foo_proc = subprocess.Popen(['foo'], stderr=subprocess.PIPE)

thread = threading.Thread(target=stream_reader, args=(foo_proc.stderr,))
thread.setDaemon(True) # optional 
thread.start()

# do other stuff

thread.join() # await thread termination (optional for daemons)

当然,你也可以调用 stream_reader(foo_proc.stderr),但我猜你可能还有其他事情要做,而不是一直盯着 foo 子进程的输出。

下面是配置日志的一种方法(这段代码只需要执行一次):

import logging, logging.handlers

handler = logging.handlers.RotatingFileHandler('/tmp/foo.log', 'a', 100000, 10)
logging.getLogger().addHandler(handler)
logging.getLogger('foo').setLevel(logging.DEBUG)

这样会创建最多10个大小为100K的文件,文件名为 foo.log(轮换后会变成 foo.log.1、foo.log.2 等,其中 foo.log 是最新的)。你也可以设置成1000000和1,这样就只会有 foo.log 和 foo.log.1,当文件大小超过1000000字节时就会进行轮换。

撰写回答