Linux下闭源遗留应用循环覆盖日志的提取与syslog导入方案咨询
解决方案建议
一、基于文件偏移量的日志追踪脚本(落地你的第一个方案)
这是最直接不需要修改应用的方案,核心是通过追踪进程对日志文件的写入偏移,识别循环覆盖的边界,提取完整日志转发到syslog。
获取进程的文件偏移量
用lsof工具可以直接拿到应用进程当前的日志文件写入偏移:# 替换<应用PID>和<日志文件路径> lsof -p <应用PID> -o -d $(lsof <日志文件路径> | grep -v PID | awk '{print $4}' | cut -d'u' -f1) | awk 'NR>1 {print $4}' | cut -d't' -f2执行后返回的数字就是当前写入位置的字节偏移量。
日志读取与转发逻辑
写一个循环脚本(Shell或Python均可),核心步骤:- 记录上一次的偏移量
last_offset,每次循环获取当前偏移current_offset和日志文件大小file_size。 - 如果
current_offset<last_offset:说明触发了循环覆盖,需要先从last_offset读到文件末尾,再从文件开头读到current_offset,把这两段内容转发到syslog。 - 如果
current_offset>=last_offset:直接读取last_offset到current_offset之间的内容转发。 - 缓存不完整行:每次读取后保留最后一行未结束的内容,下次读取时拼接成完整行再转发。
- 记录上一次的偏移量
Python核心逻辑示例
import os import subprocess import time LOG_PATH = "/var/log/legacy_app.log" APP_PID = 12345 # 替换为你的应用PID LAST_OFFSET = 0 PARTIAL_LINE = "" def get_current_offset(): # 拿到目标文件的文件描述符 fd_result = subprocess.run( ["lsof", LOG_PATH], capture_output=True, text=True ) fd = fd_result.stdout.splitlines()[1].split()[4].split('u')[0] # 获取当前偏移 offset_result = subprocess.run( ["lsof", "-p", str(APP_PID), "-o", "-d", fd], capture_output=True, text=True ) return int(offset_result.stdout.splitlines()[1].split()[3].split('t')[1]) while True: current_offset = get_current_offset() file_size = os.path.getsize(LOG_PATH) with open(LOG_PATH, "rb") as f: if current_offset < LAST_OFFSET: # 处理循环覆盖:先读旧偏移到末尾 f.seek(LAST_OFFSET) data = f.read() lines = (PARTIAL_LINE + data.decode()).split("\n") PARTIAL_LINE = lines.pop() if lines else "" for line in lines: subprocess.run(["logger", "-t", "legacy-app", line]) # 再读开头到新偏移 f.seek(0) data = f.read(current_offset) lines = (PARTIAL_LINE + data.decode()).split("\n") PARTIAL_LINE = lines.pop() if lines else "" for line in lines: subprocess.run(["logger", "-t", "legacy-app", line]) else: # 正常读取新增内容 f.seek(LAST_OFFSET) data = f.read(current_offset - LAST_OFFSET) lines = (PARTIAL_LINE + data.decode()).split("\n") PARTIAL_LINE = lines.pop() if lines else "" for line in lines: subprocess.run(["logger", "-t", "legacy-app", line]) LAST_OFFSET = current_offset time.sleep(1)
二、LD_PRELOAD钩子修改日志行为(落地你的第二个方案)
通过拦截应用的系统调用,强制修改其日志写入逻辑,让日志变成可被tail正常监控的模式。
编写钩子库
下面的C代码会拦截lseek和write函数:当应用试图把日志文件的偏移重置到开头时,改为追加到文件末尾;当文件大小超过设定阈值时,自动截断文件从头写入(避免无限增长)。#define _GNU_SOURCE #include <stdio.h> #include <dlfcn.h> #include <unistd.h> #include <sys/stat.h> #include <string.h> #define MAX_LOG_SIZE (1024 * 1024 * 100) // 替换为你的日志文件最大大小(这里是100MB) const char *TARGET_LOG = "/var/log/legacy_app.log"; // 替换为你的日志路径 off_t (*original_lseek)(int fd, off_t offset, int whence); ssize_t (*original_write)(int fd, const void *buf, size_t count); off_t lseek(int fd, off_t offset, int whence) { char path[1024]; snprintf(path, sizeof(path), "/proc/self/fd/%d", fd); if (readlink(path, path, sizeof(path)) != -1 && strcmp(path, TARGET_LOG) == 0) { // 拦截seek到开头的操作,改为跳转到文件末尾 if (whence == SEEK_SET && offset == 0) { struct stat st; fstat(fd, &st); return original_lseek(fd, st.st_size, SEEK_SET); } } return original_lseek(fd, offset, whence); } ssize_t write(int fd, const void *buf, size_t count) { char path[1024]; snprintf(path, sizeof(path), "/proc/self/fd/%d", fd); if (readlink(path, path, sizeof(path)) != -1 && strcmp(path, TARGET_LOG) == 0) { struct stat st; fstat(fd, &st); // 如果写入后超过最大大小,截断文件从头写 if (st.st_size + count > MAX_LOG_SIZE) { ftruncate(fd, 0); original_lseek(fd, 0, SEEK_SET); } } return original_write(fd, buf, count); } void _init() { original_lseek = dlsym(RTLD_NEXT, "lseek"); original_write = dlsym(RTLD_NEXT, "write"); }编译与使用
编译成共享库:gcc -shared -fPIC -o log_fix.so log_fix.c -ldl启动应用时加载这个库:
LD_PRELOAD=/path/to/log_fix.so /path/to/legacy_app之后就可以用
tail -f /var/log/legacy_app.log | logger -t legacy-app直接把日志转发到syslog了。
三、备选方案:定期镜像解析
如果上述方案都无法落地,可以用dd定期创建日志文件的镜像,再从镜像中提取增量日志:
# 每10分钟做一次镜像 while true; do dd if=/var/log/legacy_app.log of=/tmp/log_mirror_$(date +%s) sleep 600 done
然后写脚本对比前后两次镜像的内容,提取新增的日志片段转发到syslog。这个方案适合日志量较小的场景,缺点是会占用额外磁盘空间。
内容的提问来源于stack exchange,提问作者Adam C.
相关产品推荐
相关产品推荐

