简介:LogViewer 是一款面向开发、运维及测试人员的轻量级日志查看工具,主要解决大体积日志文件打开慢、检索卡顿的问题。压缩包共 5 个文件,以 exe 可执行主程序为核心,辅以 chm 帮助文档、html 说明页面、manifest 配置清单和 txt 文本说明,整体仅 551KB,体积小巧、部署负担低。作者实测可稳定处理 46G 日志文件,对 4G 以上日志场景尤为适用,已有 941 人学习下载。借助它,读者可快速完成服务器排障、应用异常定位和日志内容浏览,在低资源占用下保持较流畅的查看体验;在服务器巡检、故障复盘等场景中,也能减少等待大文件加载的时间。配套帮助文档与说明文本便于初次使用者了解功能边界和基本用法,适合需要频繁查阅大型日志、又不想部署重型分析平台的初中级技术人员备用。
1. 日志查看工具 logviewer:先解决排障现场的可检索,再谈界面
凌晨两点排一个偶发超时,终端里tail -f刷得比心跳快,关键字明明在文件里,翻回去却找不到上下文——多数团队要的日志查看工具不是再开一个窗口,而是把日志 log 变成可检索、可定位、可复现的现场。logviewer 这个资源包就是围绕这个场景做的:把本地或内网目录里的日志按时间、级别、来源和关键字切开,保留原始行号与上下文,适合后端、运维、测试,以及需要临时看线上日志又不想把文件到处拷的人。它不替代集中式日志平台,定位是排障现场的第一层放大镜:先在本机把可疑窗口缩到几十行,再决定要不要往更大的平台走。
2. 把日志流拆成可检索结构:logviewer 的字段模型与增量读取
日志查看工具最容易翻车的地方,不是界面不好看,而是底层把日志当成一堆纯文本。纯文本也能grep,但一旦要做时间窗过滤、级别筛选、上下文合并和增量刷新,就必须先把每一行拆成稳定字段。logviewer 这类工具的落地顺序应该是:先定字段模型,再定读取方式,最后才是页面上的搜索框和颜色。字段模型不稳,后面所有过滤都是玄学;读取方式不对,文件一轮转,页面就停在旧内容上。
2.1 先定字段再谈界面:时间、级别、来源、消息、上下文
我一般先把日志行拆成五类信息:时间戳、级别、来源、消息体、上下文归属。时间戳决定能不能做时间窗;级别决定能不能快速压掉 INFO 噪音;来源决定多文件混看时能不能区分模块;消息体是检索目标;上下文归属决定多行堆栈、异常栈、SQL 片段能不能跟着主行一起显示。很多日志查看工具只做了关键字高亮,没有做“行归属”,结果一搜ERROR,下面十几行堆栈被当成独立行,翻页时断成两截,排障时非常难受。
字段抽取不要求一开始就覆盖所有格式。常见做法是先支持两三类主流格式,再用一条“兜底规则”把不认识的行挂到上一行后面。比如以2025-01-01T00:00:00+08:00开头的行视为新记录,不以时间戳开头的行视为上一条记录的续行。这样多行堆栈不会丢,未知格式也不会把整个解析器卡死。字段名可以和资源包里的配置对齐,但内部模型最好固定成“时间、级别、来源、消息、原始行号、续行标记”,前端换皮肤不影响索引逻辑。
| 字段 | 作用 | 缺失时的后果 |
|---|---|---|
| 时间戳 | 时间窗过滤、排序 | 只能按文件顺序翻,跨文件乱序 |
| 级别 | 压噪音、告警视图 | 所有行一个颜色,ERROR 被 INFO 淹没 |
| 来源 | 多文件混看、模块定位 | 不知道哪条来自哪个服务 |
| 消息体 | 关键字检索 | 只能全文扫,命中率低 |
| 原始行号 | 回查原文件、对照grep | 页面行号和文件行号对不上 |
| 续行标记 | 多行堆栈合并 | 上下文断裂,异常栈只剩第一行 |
2.2 增量读取为什么不能只用 readlines:从 inode 到 offset
很多自建日志查看器一开始都用readlines()全量读,日志小的时候没问题,几百 MB 以后就开始卡。更麻烦的是增量刷新:文件被轮转后,旧文件句柄还指着旧 inode,新日志写进了新文件,页面却一直显示旧内容。这个问题不是界面刷新频率能解决的,必须在读取层记录“文件身份 + 字节偏移”。常见做法是每轮读取前先stat一下路径,拿到 inode 和 size;如果 inode 变了,就重新打开文件并把偏移归零;如果 size 比上次小,也视为轮转或截断,重新读。
读取时不要按字符读,按字节读到缓冲区,再按换行切分,最后用探测到的编码解码。这样既能处理大文件,也能避免多字节字符被截断。偏移量要落在“完整行之后”,不能落在缓冲区中间,否则下次刷新会从半行开始,出现乱码或重复行。多行合并也要在这一层做:新读到的行如果不是时间戳开头,就挂到上一条记录上,而不是直接 push 到列表里。这样做的好处是,前端拿到的永远是“记录”,不是“行”,分页和上下文都好算。
2.3 一个可抄的日志格式探测脚本
在把目录接进 logviewer 之前,我习惯先跑一个格式探测脚本,确认编码、时间戳覆盖率、级别命中率。下面这段脚本只依赖标准库,适合放在资源包旁边做前置检查:
import re import sys from itertools import islice from pathlib import Path # 常见时间戳模式,按实际日志格式增删;这里只做“行首探测” TIME_PATTERNS = [ re.compile(r"^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}"), re.compile(r"^\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2}"), ] LEVEL_PATTERN = re.compile(r"\b(TRACE|DEBUG|INFO|WARN|WARNING|ERROR|FATAL)\b", re.I) def probe(path: Path, sample_lines: int = 2000) -> None: text = [] used_encoding = "utf-8" for enc in ("utf-8", "gb18030"): try: with path.open("r", encoding=enc, errors="strict") as f: text = list(islice(f, sample_lines)) used_encoding = enc break except UnicodeDecodeError: continue if not text: print(f"file={path} empty_or_unreadable") return time_hits = sum(any(p.match(line) for p in TIME_PATTERNS) for line in text) level_hits = sum(bool(LEVEL_PATTERN.search(line)) for line in text) print(f"file={path} lines={len(text)} encoding={used_encoding}") print(f"time_header_ratio={time_hits / len(text):.2%}") print(f"level_hit_ratio={level_hits / len(text):.2%}") print("sample:") for line in text[:5]: print(repr(line[:160])) if __name__ == "__main__": # 用法:python log_probe.py ./sample-logs/app.log 2000 probe(Path(sys.argv[1]), int(sys.argv[2]) if len(sys.argv) > 2 else 2000)逻辑说明:脚本先按utf-8严格解码,失败后切到gb18030,再失败就由调用方决定是否用替换字符模式。采样行数默认 2000,只读文件头部,不会把大文件拖进内存。时间戳覆盖率和级别命中率是给 logviewer 配置用的:覆盖率低于 70% 时,不要急着开时间窗过滤,先确认日志是不是多行格式或者时间戳不在行首;级别命中率异常低时,先检查级别字段是不是被写成了中文或者缩写。
参数说明:第一个参数是日志文件路径,第二个参数是采样行数。采样行数不要一上来设成十万,头两千行足够判断格式;如果日志文件头部是启动 banner、没有业务行,可以临时用tail -n 2000截一段再探测。编码探测顺序可以按团队实际调整,但不要同时用宽松解码跑统计,否则乱码行会被算进覆盖率,得到虚假的安全感。
2.4 选型:本地文件查看器与集中式平台的分工
logviewer 这种本地文件查看器,优势是轻、快、权限边界清楚:日志文件就在这台机器或这个挂载目录里,不需要先把数据推到远端,也不用等索引构建。它适合排障前十分钟:确认报错行、看上下文、对照时间窗、把可疑片段复制出来。集中式平台的优势是跨机器检索、长期留存、聚合告警,但它的索引延迟和查询成本不适合所有临时排障。两者不是替代关系,合理的分工是:先用 logviewer 在本机把问题缩到一个文件、一个时间窗、一个关键字,再把确定的线索带到集中式平台做跨服务关联。
选型时看三个边界:日志是否已经落到本地目录、是否允许把日志内容读到浏览器、是否需要多人同时看同一份日志。如果日志里有令牌、手机号、内部路径,优先只监听127.0.0.1,并且默认只读;如果多人要看,至少加一层反向代理鉴权,不要直接把端口暴露到公网。资源包如果自带示例配置,先按示例跑通,再逐项改路径和过滤规则,不要一上来就把生产目录挂进去。
3. 跑通第一个本地检索:从解压到打开 logviewer 的检查顺序
拿到 logviewer 资源后,最省时间的顺序不是先读全部源码,而是先确认入口、依赖和示例日志能不能跑通。很多“工具不能用”的问题,最后都落在入口名不对、依赖没装全、日志路径写错这三件事上。下面这套检查顺序按“目录对齐 → 虚拟环境 → 采样日志 → 检索对照”走,每一步都有可回退的检查点,跑不通时知道该看哪里,而不是对着浏览器空白页猜。
3.1 目录与入口先对齐:资源包常见结构核对
先把资源包解压到一个短路径下,避免路径里带空格和中文,减少启动参数转义问题。常见结构是入口脚本、依赖清单、示例配置、示例日志分开摆放;不同资源包的命名可能不同,先看根目录有没有README、requirements.txt、config目录和sample目录。入口名不确定时,不要靠猜,直接用--help探参数;如果python logviewer.py --help报模块找不到,再检查是不是包内模块启动方式,比如python -m logviewer --help。这一步的目标不是记住所有参数,而是确认“入口存在、依赖能导入、帮助能打印”。
目录核对时重点看三件事:日志路径是文件还是目录、配置里有没有写死绝对路径、示例日志编码和你的真实日志是否一致。很多资源包默认读./logs,你把日志放在./data/logs,页面当然空白。还有一种情况是配置里默认开了增量跟随,但示例日志是静态文件,页面看起来“不刷新”,其实不是故障。先把路径改成示例目录,确认能出结果,再换真实目录。
3.2 用虚拟环境起服务:命令、端口与只读挂载
依赖不要装到系统 Python 里,尤其是日志查看工具经常要读文件、开端口,环境越干净越容易排查。下面是一套通用的启动顺序,入口名按资源包实际名称替换:
python3 -m venv .venv source .venv/bin/activate python -m pip install --upgrade pip python -m pip install -r requirements.txt # 先看帮助,确认入口和参数名 python logviewer.py --help # 只监听本机,只读打开示例目录 python logviewer.py \ --path ./sample-logs \ --host 127.0.0.1 \ --port 8088 \ --readonly逻辑说明:虚拟环境把依赖隔离在项目目录内,避免和系统里已有包冲突;--help用来对齐参数名,资源包如果用的是--log-dir而不是--path,按帮助输出替换;启动命令只监听127.0.0.1,避免把日志内容暴露到局域网;--readonly是底线,日志查看工具不应该有删除、截断、重命名日志的能力。
参数说明:--path指向日志目录或单个文件,先用示例目录;--host建议保持本机回环地址;--port选一个不冲突的端口,8088 被占用就换 8090;--readonly如果资源包没有这个参数,就在配置里把写操作关掉,或者用文件系统权限兜底。Windows 下激活命令换成.venv\Scripts\activate,路径分隔符也相应调整。
注意:不要随手把
--host写成0.0.0.0。日志里可能有 token、手机号、内部路径,先只在本机看,确认脱敏和鉴权方案后再考虑多人访问。
3.3 首次加载用采样日志,不要直接怼 10GB 文件
第一次加载直接指向生产日志目录,是很容易翻车的做法:页面卡住、内存暴涨、浏览器渲染几万行,最后你分不清是工具慢还是日志太大。更稳的方式是先截一段采样日志,确认检索、分页、上下文都正常,再逐步放大。采样时保留原始行号和格式,不要手工改时间戳,否则后面验证时间窗会对不上。
mkdir -p sample-logs # 用真实日志的一段做样本,路径按实际替换 cp /path/to/app.log sample-logs/app.log # 只取前 3000 行做首次加载,避免一上来就全量扫描 head -n 3000 sample-logs/app.log > sample-logs/sample.log wc -l sample-logs/sample.log逻辑说明:cp保留原始文件的一份副本,方便对照;head截取头部 3000 行,既能覆盖启动阶段日志,又不会让首次加载等太久;wc -l确认样本行数,后面页面显示的总行数应该和它对得上。如果日志头部全是启动 banner,可以改用tail -n 3000截取尾部,或者用sed -n '10000,13000p'截中间一段。
参数说明:-n 3000是采样行数,首次跑通可以降到 500;样本文件建议单独放一个目录,不要和真实日志混在一起,避免 logviewer 递归扫描时把副本也读进去。样本目录权限设成只读,防止误操作。
3.4 验证一次完整检索:关键字、时间窗、上下文行
页面能打开不等于检索正确。我一般用一条grep做对照:同一个关键字、同一个文件、同样的上下文行数,页面结果的行号和条数应该能对上。对不上时,先查编码,再查多行合并规则,最后查时间窗时区。下面这条命令用来生成对照基线:
# 统计关键字命中行号,并带 5 行上下文 grep -n -C 5 'timeout' sample-logs/sample.log | head -n 80 # 只看命中行号,方便和页面结果逐条对照 grep -n 'timeout' sample-logs/sample.log | head -n 20 # 统计样本总行数 wc -l sample-logs/sample.log逻辑说明:grep -n输出原始行号,-C 5带前后 5 行上下文,head限制输出,避免终端刷屏。页面里搜同一个关键字,先看命中条数是否一致,再看第一条命中行号是否一致,最后看上下文是不是完整。如果页面命中更多,可能是把续行也算进了消息体;如果页面命中更少,可能是时间窗过滤把部分行挡掉了,或者编码不一致导致关键字没匹配上。
参数说明:-C 5的 5 可以换成 logviewer 配置里的context_lines;head -n 80只是终端限制,不影响文件本身。对照时不要用grep -i和页面的大小写敏感设置混用,否则条数对不上是正常的。时间窗验证时,选一个已知时间点前后 10 分钟,页面结果应该和sed -n截取的时间段一致。
4. 把大日志压到可读窗口:过滤、分页与高亮的参数配置
日志 log 的检索体验,八成取决于过滤链路,而不是前端颜色。一个 2GB 的文件,如果不做级别过滤和时间窗裁剪,搜一个关键字能返回几万行,浏览器渲染再快也没法看。logviewer 的配置重点应该放在“先缩小候选集,再分页,再给上下文,最后才高亮”。参数不要拍脑袋,先按基线跑,再根据日志量和排查习惯微调;调大任何一个参数,都要知道代价在哪里。
4.1 过滤表达式的最小集合:级别、时间、来源、关键字
过滤条件不要一上来做复杂表达式,先覆盖四类:级别、时间范围、来源文件、关键字。级别过滤用来压掉 INFO 噪音,时间范围用来把跨天日志切成可读窗口,来源过滤用来在多文件混看时锁定模块,关键字过滤才是最后一步。顺序上建议“时间 → 来源 → 级别 → 关键字”,因为时间窗通常能把数据量降一个数量级,先做关键字全量扫描最容易卡。
级别映射要提前统一。有的日志写WARN,有的写WARNING,有的写E,如果配置里只认ERROR,就会漏掉FATAL。常见做法是在配置里维护一张映射表,把WARN/WARNING、ERR/ERROR、FATAL/CRITICAL归并到同一档。来源过滤不要只靠文件名,最好允许按目录前缀匹配,因为同一个服务可能按天轮转出多个文件。
| 参数 | 建议起点 | 作用 | 调大后的代价 |
|---|---|---|---|
time_window | 30 分钟到 2 小时 | 先裁时间范围 | 窗口太大,候选集暴涨 |
levels | ERROR,WARN | 压掉低级别噪音 | 只留 ERROR 可能漏掉前因 |
sources | 当前服务目录 | 多文件混看时锁定范围 | 目录太上层会扫到无关日志 |
keyword | 一个具体错误码或短语 | 最后收窄 | 太短的关键字命中过多 |
case_sensitive | 默认关闭 | 兼容大小写差异 | 开启后可能漏掉Timeout |
4.2 分页与上下文:默认 200 行为什么比全部加载稳
分页不是体验问题,是内存和渲染的边界。一次返回 200 行,前端只渲染当前页,后端只保留当前窗口的上下文,内存曲线是平的;一次返回全部命中,日志量一大就会把浏览器和后端一起拖慢。上下文行数也要有上限,默认 5 行够看普通错误,多行堆栈可以单独开“合并续行”视图,而不是把上下文调到 50 行。上下文太大时,翻页会变得很重,而且真正有用的信息反而被淹没。
分页参数要和“跟随刷新”一起考虑。跟随模式下,新日志不断追加,如果当前页是按偏移量算的,新行插入会导致页码漂移。常见做法是跟随模式只展示“最新 N 行”,不参与历史分页;切回历史检索时暂停跟随,按时间窗和行号定位。这样既不会错过新日志,也不会让正在看的页面突然跳走。
4.3 高亮规则配置:用 YAML 管住颜色和优先级
高亮规则不要写死在代码里,放在配置文件里更方便团队统一。下面是一份按常见字段写的示例,键名要按资源包实际配置映射,不要硬套:
logviewer: path: ./sample-logs encoding: utf-8 page_size: 200 context_lines: 5 follow: true max_scan_bytes: 268435456 filters: levels: [ERROR, WARN] time_from: "2025-01-01T00:00:00+08:00" time_to: "2025-01-01T23:59:59+08:00" keyword: "timeout" highlight: - name: error pattern: "(?i)\\b(ERROR|FATAL)\\b" color: "#c0392b" priority: 100 - name: slow pattern: "(?i)timeout|timed out|elapsed=[0-9]{4,}ms" color: "#d35400" priority: 80逻辑说明:page_size控制单页记录数,200 是兼顾可读性和内存的起点;context_lines控制命中行前后展示多少行,5 行适合普通错误;follow控制是否跟随新日志,排障时开着,做历史对照时关掉;max_scan_bytes限制单次扫描上限,避免一个超大文件把内存打满。highlight里priority越大越先匹配,错误级别的高亮应该压过普通关键字高亮,否则一行里颜色会打架。
参数说明:time_from和time_to必须带时区偏移,否则跨时区日志会筛错;pattern是正则,注意转义,YAML 双引号里反斜杠要写成\\b;color只是示例色值,按团队习惯改。max_scan_bytes不要设得比机器可用内存还大,256MB 对多数排障场景够用,超过就先用时间窗裁一刀。
4.4 大文件场景的参数基线
日志超过 1GB 时,我一般按这套基线起步:时间窗先缩到 30 分钟,级别只留ERROR,WARN,来源限定到当前服务目录,关键字用错误码而不是泛词,分页保持 200,上下文 5 行,跟随刷新关闭。等定位到具体文件后,再逐步放宽时间窗和级别。这样做的原因是,大文件检索的瓶颈通常在“扫描字节数”和“返回行数”,不在前端颜色;先把候选集压到几千行,后面怎么高亮都不会卡。
如果资源包支持索引,优先让索引覆盖时间戳和级别两个字段,不要一上来给消息体做全文索引。消息体变化大,索引维护成本高;时间戳和级别稳定,过滤收益最直接。索引目录要放在本地磁盘,不要放在网络挂载上,否则刷新延迟会比日志本身还难排查。
5. 避坑与排查:logviewer 落地的五个常见翻车点
日志查看工具的血泪经验,基本都集中在“文件在变、编码在变、时间在变”这三件事上。页面能打开只是开始,真正难的是轮转后还能不能跟上、中文会不会乱码、大文件会不会卡死、时间窗会不会筛错、级别过滤会不会失效。下面五条按“现象 → 原因 → 解决”写,每条都给一个可执行的检查动作,遇到问题时不要先改代码,先按顺序把现象复现出来。
5.1 文件轮转后新日志不刷新:句柄还指着旧 inode
现象:日志文件明明已经轮转,新内容也写进了新文件,logviewer 页面却停在旧内容,刷新按钮也没用。原因通常是读取层只打开了一次文件句柄,轮转后旧句柄仍指向旧 inode,新文件路径虽然变了,进程并不知道。另一种情况是读取层记录了字节偏移,但没检查文件大小回退,截断后偏移越界,读出来是空。
解决:每轮刷新前先stat路径,比较 inode 和 size;inode 变化或 size 变小就重新打开文件并把偏移归零。可以用下面的命令确认句柄指向:
LOG=/path/to/app.log stat -c 'inode=%i size=%s file=%n' "$LOG" PID=$(pgrep -f 'logviewer.py' | head -n 1) lsof -p "$PID" 2>/dev/null | grep "$LOG" || echo "未看到句柄"逻辑说明:stat输出当前路径的 inode 和大小,和 logviewer 内部记录的 inode 对比;lsof看进程打开的句柄,如果句柄指向的文件名带.1或已删除标记,说明还在读旧文件。没有lsof的机器可以用/proc/$PID/fd目录替代。参数说明:LOG换成实际日志路径,PID换成 logviewer 进程号;pgrep -f可能匹配到多个进程,先确认再操作。
5.2 中文乱码和多行堆栈错位:编码与合并规则没对齐
现象:页面里中文变成问号或方块,关键字搜不到;或者异常堆栈只剩第一行,下面几行被当成独立记录。原因一般是读取时用了错误编码,或者多行合并规则只认时间戳开头,没把at ...、Caused by:这类续行挂到上一条。编码问题还会导致字节偏移和字符偏移混用,翻页时出现半行。
解决:先用探测脚本确认编码,再在配置里固定编码,不要依赖系统默认。多行合并规则按“新记录开头”判断,而不是按“包含关键字”判断。排查编码可以用下面这段:
python - <<'PY' from pathlib import Path p = Path('sample-logs/app.log') raw = p.read_bytes()[:4096] for enc in ('utf-8', 'gb18030'): try: print(enc, raw.decode(enc)[:120]) except UnicodeDecodeError as e: print(enc, 'fail', e) PY逻辑说明:只读前 4096 字节,分别用utf-8和gb18030严格解码,哪个成功就优先用哪个;两个都失败时,说明文件可能混合编码或开头是二进制,需要先切分文件。参数说明:[:4096]可以改成更大,但不要一次读整文件;sample-logs/app.log换成实际样本路径。解决多行错位时,在配置里把续行规则改成“不以时间戳开头即续行”,并确认前端展示的是合并后的记录,不是原始行。
5.3 大文件一搜就卡:全量扫描和前端渲染一起上
现象:输入关键字后页面转圈,浏览器内存飙升,甚至标签页崩溃。原因通常不是正则太慢,而是后端把整个文件扫了一遍,又把几万条命中一次性返回给前端渲染。还有一个隐藏原因是上下文行数设得太大,每条命中都带几十行,返回体积成倍增长。
解决:先限制扫描字节数和返回条数,再开分页。时间窗、级别、来源三个条件至少用两个,不要只靠关键字全量扫。前端只渲染当前页,滚动加载要按需请求,不要一次性塞进 DOM。如果资源包支持索引,优先用时间戳和级别索引;不支持索引时,先用grep在命令行缩小范围,再把小文件交给 logviewer 看上下文。
5.4 时间窗筛不准:时区、时间戳格式和“本地时间”混用
现象:明明选了 10:00 到 11:00,页面却返回 02:00 到 03:00 的日志,或者跨天日志少了一段。原因多是日志时间戳带时区、配置里写的是本地时间、浏览器又按本地时区解释,三层时区叠在一起。还有一种情况是时间戳格式有T和空格两种,解析器只认一种,另一种被当成无时间行。
解决:配置里的时间范围统一带偏移量,比如2025-01-01T10:00:00+08:00;解析器同时支持T和空格分隔;页面展示时明确标注时区。可以用下面的命令确认机器时区和 UTC 的差异:
python - <<'PY' from datetime import datetime, timezone print('local:', datetime.now().astimezone().isoformat()) print('utc :', datetime.now(timezone.utc).isoformat()) PY逻辑说明:输出本地时间和 UTC 时间,用来对照日志里的时间戳。如果日志写的是 UTC,配置里却按本地时间筛,就会差出整小时。参数说明:这段脚本只做对照,不改系统时区;生产环境改时区影响面大,优先在 logviewer 配置里做偏移。
5.5 级别过滤失效:字段名和大小写不一致
现象:配置里写了levels: [ERROR],页面还是出现大量 INFO;或者WARN能过滤,WARNING过滤不了。原因通常是日志里的级别字段名不统一,有的是level=ERROR,有的是[WARN],还有的是小写error;解析器只按固定位置取,没做归一化。
解决:先统计样本里的级别分布,再决定映射表。可以用grep -oE快速看:
grep -oE '\b(TRACE|DEBUG|INFO|WARN|WARNING|ERROR|FATAL)\b' sample-logs/app.log \ | sort | uniq -c | sort -nr逻辑说明:提取样本里出现的级别词并计数,确认实际有哪些写法;如果计数为 0,说明级别不在行首或者用了中文,需要调整解析规则。参数说明:-oE只输出匹配部分,sort | uniq -c做频次统计,sort -nr按数量倒序。解决时在配置里维护映射:WARNING -> WARN、ERR -> ERROR、FATAL -> ERROR,匹配时统一转大写。
6. 用一条基准日志做回归:logviewer 每次改配置后的三组断言
改过滤规则最怕“看起来能用”。我习惯准备一条固定基准日志,里面放启动、警告、错误、超时、恢复、停止几类行,每次改配置后先跑三组断言:总行数、ERROR 行号、timeout 上下文窗口。这三组能覆盖分页、级别过滤和上下文合并,比手工点页面可靠。下面这段脚本会生成样本并做断言,可以直接放在资源包旁边当回归检查:
from pathlib import Path BASE = Path("baseline.log") BASE.write_text( "\n".join([ "2025-01-01T00:00:00+08:00 INFO boot", "2025-01-01T00:00:01+08:00 INFO load config", "2025-01-01T00:00:02+08:00 WARN slow disk", "2025-01-01T00:00:03+08:00 ERROR timeout calling db", "2025-01-01T00:00:04+08:00 ERROR retry failed", "2025-01-01T00:00:05+08:00 INFO fallback", "2025-01-01T00:00:06+08:00 DEBUG cache hit", "2025-01-01T00:00:07+08:00 INFO request done", "2025-01-01T00:00:08+08:00 WARN queue high", "2025-01-01T00:00:09+08:00 ERROR timeout calling cache", "2025-01-01T00:00:10+08:00 INFO recovery", "2025-01-01T00:00:11+08:00 INFO stop", ]) + "\n", encoding="utf-8", ) lines = BASE.read_text(encoding="utf-8").splitlines() # 断言 1:总行数,防止分页或合并规则把行吃掉 assert len(lines) == 12, len(lines) # 断言 2:ERROR 行号,防止级别过滤或大小写归一化出错 error_lines = [i + 1 for i, line in enumerate(lines) if "ERROR" in line] assert error_lines == [4, 5, 10], error_lines # 断言 3:timeout 命中行号与上下文窗口,防止多行合并和上下文行数漂移 timeout_lines = [i + 1 for i, line in enumerate(lines) if "timeout" in line.lower()] assert timeout_lines == [4, 10], timeout_lines window = lines[max(0, timeout_lines[0] - 3): timeout_lines[0] + 2] assert len(window) == 5, window print("baseline ok", error_lines, timeout_lines)逻辑说明:脚本先生成 12 行固定日志,再按行号做断言。第一组盯总行数,防止分页参数或续行合并把记录数改掉;第二组盯 ERROR 行号,防止级别映射和大小写处理出错;第三组盯 timeout 的行号和上下文窗口,防止时间窗过滤和多行合并把上下文截断。页面配置改完后,用同一个文件搜ERROR和timeout,条数和行号应该和断言一致。
参数说明:baseline.log可以换成你自己的样本名;error_lines == [4, 5, 10]和timeout_lines == [4, 10]要随样本内容改,不要硬套;window取第一个 timeout 行前 3 行到后 1 行,共 5 行,对应配置里的context_lines: 5。如果页面显示 6 行,说明上下文边界多算了一行;如果显示 3 行,说明续行合并把中间行吃掉了。
从那以后我每次改 logviewer 的过滤规则,都先用这条基准日志跑一遍三组断言,再碰真实目录;踩过一次时区配置把夜间日志全过滤掉的坑,就不敢省这一步了。你拿到 logviewer 资源后,也建议先把入口脚本、示例日志和这份基准文件放在同一层,按第 3 章的检查顺序跑通,再用--help对齐实际参数,最后才把路径指向真实日志。希望帮到你。
本文还有配套的精品资源,点击获取