python:持续读取文件,即使在日志轮转后

9 投票
5 回答
14178 浏览
提问于 2025-04-18 18:46

我有一个简单的Python脚本,它会不断读取日志文件(就像命令tail -f一样)。

while True:
    line = f.readline()
    if line:
        print line,
    else:
        time.sleep(0.1)

我该如何确保在日志文件被logrotate轮换后,我仍然可以继续读取这个日志文件呢?

也就是说,我需要做到和tail -F一样的效果。

我使用的是python 2.7

5 个回答

0

使用 'tail -F'

man tail

-F 这个选项和 --follow=name --retry 是一样的

-f, --follow[={name|descriptor}] 这个选项会在文件增长时输出追加的数据;

--retry 如果文件无法访问,会不断尝试打开这个文件

-F 这个选项会跟踪文件的名字而不是描述符。

所以当日志轮换(logrotate)发生时,它会跟踪新的文件。

import subprocess

def tail(filename: str) -> Generator[str, None, None]:
    proc = subprocess.Popen(["tail", "-F", filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    while True:
        line = proc.stdout.readline()
        if line:
            yield line.decode("utf-8")
        else:
            break

for line in tail("/config/logs/openssh/current"):
    print(line.strip())
2

我根据@pawamoy的那个很棒的东西,做了一个变种,把它改成了一个生成器函数,方便我用来监控和跟踪日志。

def tail_file(file):
    """generator function that yields new lines in a file

    :param file:File Path as a string
    :type file: str
    :rtype: collections.Iterable
    """
    seek_end = True
    while True:  # handle moved/truncated files by allowing to reopen
        with open(file) as f:
            if seek_end:  # reopened files must not seek end
                f.seek(0, 2)
            while True:  # line reading loop
                line = f.readline()
                if not line:
                    try:
                        if f.tell() > os.path.getsize(file):
                            # rotation occurred (copytruncate/create)
                            f.close()
                            seek_end = False
                            break
                    except FileNotFoundError:
                        # rotation occurred but new file still not created
                        pass  # wait 1 second and retry
                    time.sleep(1)
                yield line

这个可以像下面这样简单使用

import os, time
access_logfile = '/var/log/syslog'
loglines = tail_file(access_logfile)
for line in loglines:
    print(line)
3

感谢@tdelaney和@Dolda2000的回答,我得到了以下内容。这段代码应该可以在Linux和Windows上都运行,并且能够处理logrotate的copytruncatecreate选项(分别是先复制然后把文件大小截断为0,或者移动文件再重新创建)。

file_name = 'my_log_file'
seek_end = True
while True:  # handle moved/truncated files by allowing to reopen
    with open(file_name) as f:
        if seek_end:  # reopened files must not seek end
            f.seek(0, 2)
        while True:  # line reading loop
            line = f.readline()
            if not line:
                try:
                    if f.tell() > os.path.getsize(file_name):
                        # rotation occurred (copytruncate/create)
                        f.close()
                        seek_end = False
                        break
                except FileNotFoundError:
                    # rotation occurred but new file still not created
                    pass  # wait 1 second and retry
                time.sleep(1)
            do_stuff_with(line)

使用copytruncate选项时有一个限制,就是如果在程序休眠的时候,有新内容被添加到文件里,而旋转操作发生在程序醒来之前,那么最后几行内容就会“丢失”(这些内容仍然在现在的“旧”日志文件中,但我找不到一个好的方法来“跟踪”这个文件以完成读取)。这个限制在使用“移动并创建”的create选项时就不成问题,因为f描述符仍然指向被重命名的文件,因此在描述符关闭并重新打开之前,最后几行内容会被读取到。

5

你可以通过记录你在文件中的位置来实现这个功能,当你想读取时再重新打开文件。当日志文件更新时,你会发现文件变小了,因为你重新打开文件,所以也能处理掉任何被删除的情况。

import time

cur = 0
while True:
    try:
        with open('myfile') as f:
            f.seek(0,2)
            if f.tell() < cur:
                f.seek(0,0)
            else:
                f.seek(cur,0)
            for line in f:
                print line.strip()
            cur = f.tell()
    except IOError, e:
        pass
    time.sleep(1)

这个例子隐藏了一些错误,比如找不到文件,因为我不太清楚日志轮换的细节,比如在文件短时间不可用的情况下。

注意:在Python 3中,情况有所不同。普通的 open 会把 bytes 转换成 str,而这个转换过程中使用的临时缓冲区会导致 seektell 无法正常工作(除了在移动到0或文件末尾时)。所以,应该以二进制模式打开文件("rb"),然后逐行手动解码。你需要知道文件的编码方式,以及这种编码下换行符的样子。对于utf-8来说,它的换行符是 b"\n"(顺便说一下,这也是utf-8比utf-16更优秀的原因之一)。

20

只要你打算在Unix系统上这样做,最稳妥的方法就是检查打开的文件是否仍然指向同一个i-node,如果不再指向,就重新打开它。你可以通过os.statos.fstat来获取文件的i号,具体在st_ino这个字段里。

代码可能长这样:

import os, sys, time

name = "logfile"
current = open(name, "r")
curino = os.fstat(current.fileno()).st_ino
while True:
    while True:
        buf = current.read(1024)
        if buf == "":
            break
        sys.stdout.write(buf)
    try:
        if os.stat(name).st_ino != curino:
            new = open(name, "r")
            current.close()
            current = new
            curino = os.fstat(current.fileno()).st_ino
            continue
    except IOError:
        pass
    time.sleep(1)

我怀疑这个方法在Windows上可能不太好用,但既然你提到tail,我想这对你来说应该不是问题。:)

撰写回答