python:持续读取文件,即使在日志轮转后
我有一个简单的Python脚本,它会不断读取日志文件(就像命令tail -f
一样)。
while True:
line = f.readline()
if line:
print line,
else:
time.sleep(0.1)
我该如何确保在日志文件被logrotate轮换后,我仍然可以继续读取这个日志文件呢?
也就是说,我需要做到和tail -F
一样的效果。
我使用的是python 2.7
。
5 个回答
使用 'tail -F'
-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())
我根据@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)
感谢@tdelaney和@Dolda2000的回答,我得到了以下内容。这段代码应该可以在Linux和Windows上都运行,并且能够处理logrotate的copytruncate
或create
选项(分别是先复制然后把文件大小截断为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
描述符仍然指向被重命名的文件,因此在描述符关闭并重新打开之前,最后几行内容会被读取到。
你可以通过记录你在文件中的位置来实现这个功能,当你想读取时再重新打开文件。当日志文件更新时,你会发现文件变小了,因为你重新打开文件,所以也能处理掉任何被删除的情况。
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
,而这个转换过程中使用的临时缓冲区会导致 seek
和 tell
无法正常工作(除了在移动到0或文件末尾时)。所以,应该以二进制模式打开文件("rb"),然后逐行手动解码。你需要知道文件的编码方式,以及这种编码下换行符的样子。对于utf-8来说,它的换行符是 b"\n"
(顺便说一下,这也是utf-8比utf-16更优秀的原因之一)。
只要你打算在Unix系统上这样做,最稳妥的方法就是检查打开的文件是否仍然指向同一个i-node,如果不再指向,就重新打开它。你可以通过os.stat
和os.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
,我想这对你来说应该不是问题。:)