Python 异步有什么办法实现类似 Linux 的 tail -f 的功能吗?

2021-12-07 17:44:49 +08:00
 LeeReamond

需求:web 服务里需要监测日志,服务是异步的,理论上最好用 ws 的方式传输更新数据,而不要让前端轮询。

linux 的 shell 下用 tail -f 可以完美满足需求,但是 shell 登录太麻烦了。

想要实现的效果是,给文件一个钩子,把它像流一样处理,文件不变的时候就 await 阻塞住,文件更新的话则得到 await 返回,返回的内容就是新增的内容(姑且目前认为只会新增)

类似于下面这样的伪代码这种感觉

app = Framework()

@app.ws('/ws/log-tail')
async def client_async_log(ws, file_name):
    async with ws.connect() as conn:
        file = Tail(file_name)
        while True:
            string = await file.update_content()
            await conn.send(string)

不知道有没有方式实现。按照我的想法似乎以前倒是用过类似 watchdog 的服务,文件更新后可以得到一个异步回调,但是只知道更新了,不知道更新了哪些内容啊

2503 次点击
所在节点    Python
18 条回复
Lotussha
2021-12-07 17:50:40 +08:00
刚好我也需要这个功能
fgwmlhdkkkw
2021-12-07 17:55:17 +08:00
socket af_unix
可以吗?我没试过
hsfzxjy
2021-12-07 17:55:57 +08:00
defunct9
2021-12-07 17:55:58 +08:00
没有
2i2Re2PLMaDnghL
2021-12-07 18:45:34 +08:00
基于文件系统的 notify 也是只知道更新了内容,不知道更新哪些内容。
tail.c 里似乎也是自行判断的,甚至 regular file 还可能会变小(提示 file truncated ),判断这种情况的代码处打上了这样的注释(有两处,略微不同):
```
/* XXX: This is only a heuristic, as the file may have also
been truncated and written to if st_size >= size
(in which case we ignore new data <= size).
Though in the inotify case it's more likely we'll get
separate events for truncate() and write(). */
```

所以实现方法就还是 inotify 之类的,然后再把它包装成你需要的样子。
qieqie
2021-12-07 19:06:14 +08:00
只考虑追加写的话,先 lseek 再一直 read 就行了(结合 notify ),os 会负责记住这个 fd 的 offset 的
akira
2021-12-07 19:21:00 +08:00
看看 filebeat 之类的是不是你要的
jaredyam
2021-12-07 20:03:31 +08:00
Tailing a File • A Python version of 'tail -f'
import time
import os
def follow(thefile):
thefile.seek(0, os.SEEK_END) # End-of-file
while True:
line = thefile.readline()
if not line:
time.sleep(0.1) # Sleep briefly
continue
yield line
• Idea : Seek to the end of the file and repeatedly try to read new lines. If new data is written to the file, we'll pick it up.

Copyright (C) 2018, http://www.dabeaz.com Example File: follow.py
iyaozhen
2021-12-07 20:17:41 +08:00
之前写过一个,给 tail -f 套了个 epoll 的壳
https://github.com/iyaozhen/filebeat.py
37Y37
2021-12-07 20:55:53 +08:00
LeeReamond
2021-12-07 21:29:54 +08:00
@iyaozhen
@Lotussha
@2i2Re2PLMaDnghL
@qieqie
@jaredyam 感谢楼上老哥回复,看了看代码,似乎核心代码是 popen 然后依赖于 popen.stdout 搞些事情。我自己试了试这个 fd 是可以 while True: p.stdout.readline()的,最简单的套个线程转协程就行了。。楼上老哥说也可以 epoll
LeeReamond
2021-12-07 21:37:22 +08:00
@iyaozhen 老哥我看你那个代码里,tail -f filename comment ,为啥把 comment 加在这个位置就可以显示在进程信息里,我在本地的 shell 里试了试不好使。还有你的 popen 设的 buffsize=-1 是干啥用的
iyaozhen
2021-12-07 23:16:39 +08:00
@LeeReamond 额,那个其实语法就是 tail -f 两个文件,第二个肯定不存在,但进程里面可以看见命令信息,防止运维给 kill 了

默认就是-1
negative bufsize (the default) means the system default of io.DEFAULT_BUFFER_SIZE will be used.
so1n
2021-12-07 23:32:08 +08:00
aionotify 可以很好的解决
ClericPy
2021-12-08 01:08:11 +08:00
以前用过 Python 的第三方库

不过既然你都提到 tail -f 了, 为啥不用 tail -F 呢, 分分钟写个脚本给你管道符转发出去就行了

tail -F xxx.log | python3 serv.py

看完是不是感觉很无聊, 因为 aws 挂了我特么在加班找问题...
Richard14
2021-12-08 01:15:25 +08:00
https://gist.github.com/RedmondLee/e92341616a020fbe1fed85903a264efc

试着写了个最小实现,用另一个线程封装 selector ,每次流更新后提醒主线程的事件循环去获取内容(因为 EVENT_READ 已经准备好,read 可以认为是非阻塞的)。

但是最后结果还是不太对,主线程的 call_soon 函数虽然正常执行,但是其过程中的 future.set_result(None)并不能触发主事件循环中 await 这个 future 的函数返回。或者说偶尔能触发,偶尔不能触发,搞不太清楚为什么。
qW7bo2FbzbC0
2021-12-10 14:40:06 +08:00
1.没有读取位置信息时,从开始,或者倒数 X KB 处读取,有上次读取位置信息时接着读
2.读到文件结尾时自动结束,并记录位置信息,等待间隔后,再次读取位置信息接着读
3.甚至读取前调用 stat 记录文件 inode 信息,inode 变化后,重置位置信息,从零(开头)开始读

考虑到大日志文件的性能问题,我使用了 go 进行实现
考虑到大量数据传输,与解耦问题,我使用了 kafka 进行中转
考虑到超长行问题,我使用了 kafka 的压缩算法 ==> cpu 和内存占用略高

然后就是,需要定义行切割和碎片行合并问题
fighterhit
2021-12-12 02:29:46 +08:00
无意间看到有个 golang 版的 tail -f 实现:github.com/hpcloud/tail ,可以参考下

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/820680

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX