☰
Shell脚本实战:系统日志错误分析与自动化巡检
2026/10/11 12:34:27 网站建设 项目流程

最近在整理一批shell的高级用法练习题,发现“系统日志错误分析”这个主题特别值得展开。平时我们排查系统故障,第一反应都是登录服务器、翻日志、grep关键词,但真到了日志量一大、机器一多、报错一密集,手敲命令那套流程就完全撑不住了。这篇文章就是把一次完整的日志错误分析练习拆开,从需求设计到脚本实现,再到定时运行和结果反馈,完整过一遍。

整个项目的核心目标很明确:用shell脚本自动扫描系统日志,识别出指定时间段内的错误级别记录、按类型聚合统计、提取关键报错上下文,最后生成一份简洁可读的分析报告。这样不管是半夜被报警叫醒,还是早上例行巡检,都不需要一头扎进日志文件里人肉捞线索。对想练习shell高级特性的朋友来说,这也是一个特别合适的综合课题——正则、awk、关联数组、管道组合、并发处理、信号控制全都能练到。

先说清楚这套方案适合谁。如果你已经有基础的shell知识,写过一些简单的循环和判断脚本,但觉得水平一直停留在“能跑”而不是“能用”,那这个项目能帮你把零散的知识点串成一套完整的数据处理流程。如果你是运维或者系统管理员,那实用性就更直接了,日志分析本身就是日常工作的高频需求,脚本着地之后很多重复劳动都能省掉。

1. 日志分析脚本的需求拆解与设计思路

动手写代码之前,第一步先把需求想明白。所谓“系统日志错误分析”,听起来很宽泛,但落到具体场景,无非是回答三个问题:系统出了什么问题、什么时候出的、出了多少次。围绕这三个问题,我把功能需求拆成了以下几个模块。

1.1 日志源的分析范围与格式适配

以Linux系统举例,最常见的日志源是/var/log/messages、/var/log/syslog,以及各种应用自己的日志文件。不同发行版路径不一样,不同应用时间戳格式也不一样,所以脚本第一步就要确定日志源。我的做法是允许通过参数传入日志路径,而不写死。

LOG_FILE="${1:-/var/log/messages}"

这样脚本就有了通用性。比如我要分析Nginx的错误日志,可以直接传/var/log/nginx/error.log,如果想分析某个Java应用自己输出的日志,也可以传应用日志路径,只要时间戳格式能对上就行。

日志格式是另一个关键问题。syslog的标准格式一般是这样的:

Feb 12 10:24:31 hostname sshd[12345]: error: PAM authentication failed

而有些应用日志可能是ISO格式时间戳:

2025-02-12T10:24:31,123 ERROR [main] com.example.Service - connection timeout

处理不同格式最简单的方式,是把时间戳的解析逻辑单独做成函数,每种格式写一个分支。后面详细展开。

1.2 错误等级过滤:什么样的记录算“错误”

很多刚入门的朋友会把“错误分析”理解成“grep -i error”,这其实是个坑。日志里带有error关键字的行,未必全是真正的错误,有些可能是业务逻辑里的正常返回;反过来,不少真正的故障会以WARN、FATAL甚至普通INFO形式出现,比如out of memory、segfault这类关键字。所以错误等级的过滤规则要做得更细。

我的设计是两层过滤机制:第一层,匹配标准日志等级关键词,包括ERROR、WARN、FATAL、CRIT,不区分大小写;第二层,识别高频故障特征词,包括fail、timeout、refused、unreachable、denied、segfault、oom这些。这样既能抓标准错误,也能抓“非标准但明显是故障”的记录。

ERROR_PATTERN="(ERROR|WARN|FATAL|CRIT|fail|timeout|refused|unreachable|denied|segfault|oom|OutOfMemory)"

用过一阵子之后,可以根据自己服务器的实际情况往这个模式里加词。比如有时候slow、retry这类词反复出现,虽然没有明确报错,但配合次数统计看也是性能问题的重要信号。

1.3 输出目标的定义:不止是“看”日志,而是拿到可用的结论

脚本最后输出什么、怎么输出,这个一开始就要定好。我最终设计了三种输出形式。

第一种是终端汇总,适合手动执行时快速查看,几十行关键统计直接打在屏幕上,让人一眼看出今天大概出了什么状况。第二种是详细报告文件,把按类型聚合的错误列表、上下文片段、出现次数全部写进文件,方便留存和事后翻查。第三种是触发报警,只有发现FATAL级别或者次数超过阈值的错误时,才调用报警函数发通知,避免正常操作引起的零散错误也来轰打扰人。

+---------------------+----------------------------+ | 输出形式 | 触发条件 | +---------------------+----------------------------+ | 终端汇总 | 手动执行时 | | 报告文件 | 每次运行都生成 | | 报警通知 | FATAL或错误次数超过阈值 | +---------------------+----------------------------+

这样脚本就不会变成一个只会把几千行日志重新打印一遍的“复读机”,而是真正在帮你过滤噪音、提炼问题。

2. 核心处理流程:awk + 正则 + 关联数组的组合实践

日志分析脚本的数据处理核心,我选了awk而不是纯grep加循环,原因很简单:字段型文本处理上awk的效率和代码简洁度都更优。awk本身内置正则支持,又是天然的行处理模型,加上关联数组做统计聚合,几乎就是为日志分析这个场景量身准备的。

2.1 awk状态机:让同一遍扫描干多件事

awk的BEGIN和END块可以让同一遍扫描同时完成多种统计。我这里的实现思路是,在BEGIN块里初始化计数器,在主体块里逐行判断并累加,在END块里汇总输出结果。

function analyze_log() { local log_file="$1" awk -v error_pattern="${ERROR_PATTERN}" ' BEGIN { total_lines = 0; error_lines = 0; warn_lines = 0; } { total_lines++; if (match($0, error_pattern)) { error_lines++; # 提取错误类型特征 has_fatal = match($0, /FATAL/); has_oom = match($0, /OutOfMemory|oom/); has_timeout = match($0, /timeout|timed out/); has_refused = match($0, /refused/); ... } } END { printf "总行数: %d\n", total_lines; printf "错误相关行数: %d\n", error_lines; printf "FATAL: %d, OOM: %d, TIMEOUT: %d, REFUSED: %d\n", fatal_count, oom_count, timeout_count, refused_count; }' "$log_file" }

一个关键细节是awk里的match()函数和~匹配的区别。match()会设置RSTART和RLENGTH,适合做提取;而~操作符只做布尔判断,如果你只是想知道“这行是否匹配”,用$0 ~ pattern就够,性能更好。我上面的示例为了简洁混用了,实际精调的时候大家可以按需取舍。

2.2 聚合统计与TopN输出:错误分布一目了然

按错误类型统计次数只是第一步,更有价值的是按“来源程序/进程”聚合,找出哪个程序是报错大户。syslog日志里进程名通常在第三个字段(不同格式位置不同),我可以截取这部分作为聚合维度。

awk '{ for (i=1; i<=NF; i++) { if ($i ~ /\[[0-9]+\]:/) { process = $(i-1); break; } } cnt[process]++ } END { for (p in cnt) print cnt[p], p }' "$log_file" | sort -rn | head -n 10

之所以要在[数字]:这个模式前取上一个字段,是因为很多日志行格式类似sshd[12345]: error...,进程名在左括号前。这段代码放在整体脚本里之后,我会再封装一层,确保进程名提取的健壮性,因为不同服务日志的进程名格式差异确实不小。有些是app[123],有些是thread-1这种纯线程标识,还有的根本就是一行句子开头,没有进程名。遇到后者,就归到unknown类。

TopN输出的意义在于,它直接把“哪出了问题”的回答前置了。比如最近一次跑脚本,Top1是sshd,Top2是crond,那我就知道最优先去排查sshd的认证异常,而不是漫无目的地翻整个日志。

2.3 上下文信息提取:单行报错往往说不清问题

日志里很多错误行单独看完全不知道原因,必须看前后几行才有线索。迁移代码里为每行错误记录附带上下文,也是个常见的awk练习题。

function extract_context() { local log_file="$1" local line_num="$2" local context_lines="${3:-2}" start=$((line_num - context_lines)) [ $start -lt 1 ] && start=1 end=$((line_num + context_lines)) sed -n "${start},${end}p" "$log_file" }

提取上下文之后,我会把它和原始错误行一起写入报告文件,格式如下:

--- [2025-02-12 10:24:31] FATAL sshd --- Feb 12 10:24:29 hostname sshd[12345]: PAM service(sshd) ignoring max retries Feb 12 10:24:30 hostname sshd[12345]: error: PAM authentication failed Feb 12 10:24:31 hostname sshd[12345]: fatal: Unable to negotiate with 192.0.2.10 Feb 12 10:24:32 hostname sshd[12345]: Connection closed by authenticating user

这段上下文信息比单个错误行有用得多,我实际排查问题的时候,基本都是靠这种上下文找出因果链。

3. 错误类型分类与严重程度分级:从“有报错”到“有结论”

日志分析不只是把错误串打出来,更重要的任务是给出一个可执行的优先级判断。总不能每次扫出几百行error就全都当成事故去处理,得有一个分级体系把真正紧急的挑出来。

3.1 安全类错误的识别与标记

安全类错误是优先级最高的类别,因为它们可能意味着入侵尝试、非法访问或认证攻击。常见特征包括:认证失败、权限拒绝、sudo失败、用户不存在、连接被重置。

SECURITY_PATTERN="(authentication failure|pam_|sudo|illegal user|refused.*connection|permission denied)"

这些关键词一旦命中,不管整体错误次数多少,都自动进入高危名单。我在脚本里单独维护了一个security_events关联数组,每次命中安全模式就记录进程名、IP(如果日志里有)、时间戳,最后在报告里单独出一节“安全事件”。

另外一个细节是,现代日志里大量认证失败都来自各种扫描器,不能见一次就报警,合理的做法是统计同一来源IP的失败次数,超过阈值才标记为可疑。这个聚合逻辑可以这样写:

awk ' { ip = ""; # 提取日志里的IP地址 if (match($0, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) { ip = substr($0, RSTART, RLENGTH); } if (ip != "" && $0 ~ /authentication failure|failed password/) { ip_count[ip]++; } } END { for (ip in ip_count) { if (ip_count[ip] >= threshold) { print "可疑IP: " ip " 失败次数: " ip_count[ip]; } } }' "$log_file"

这里match()加substr()的配合是关键步骤,值得专门练一遍。

3.2 性能与资源类错误的识别

第二类高价值错误是和系统资源相关的,包括:内存不足、磁盘空间不足、文件句柄耗尽、CPU软锁、网络超时。这类问题通常不是一次性的,而是持续恶化,早点发现能避免后续的级联故障。

资源类错误的识别同样走关键词模式,但我会更关注频率。比如OutOfMemory出现一次可能是某个进程临时申请了超大内存,未必是系统性问题;但如果一小时内出现十几次,那基本可以确定是泄漏或者内存配置不合理。所以我在脚本里对这些关键词单独做时间窗口统计,按小时切片计算命中率。

function check_resource_errors() { local log_file="$1" awk ' /OutOfMemory|oom-killer|No space left on device|cannot allocate memory|segfault/ { cnt++; # 把时间字段转成小时级别的时间窗口 split($1" "$2, t, ":"); hour_window = t[1] " " substr($2, 1, 2); hour_count[hour_window]++; } END { for (h in hour_count) { if (hour_count[h] >= 3) { print "小时窗口 " h " 资源类错误 " hour_count[h] " 次"; } } }' "$log_file" }

有些日志时间戳里有小时信息,有些只有月份日期和时分秒,所以切割逻辑要根据实际格式调整,但不管具体怎么写,按窗口聚合并过滤的思路是通用不变的。

3.3 网络相关错误的识别

网络错误在分布式系统和微服务架构里尤其常见,搞清是连接被拒、解析失败还是超时,对定位故障层次特别有帮助。

  • Connection refused:通常是端口没监听或者防火墙拦截。
  • Connection timed out:通常是网络不可达,或者对端防火墙丢弃了包。
  • Unknown host/Name or service not known:DNS解析失败。
  • Broken pipe/Connection reset by peer:通常是两端处理节奏不一致。

脚本里我会把这些关键词分门别类统计,然后输出成因分布。这种分类信息在排查时能大大缩小范围,比如某天突然一堆Connection timed out,那大概率不是应用层出问题,而是网络链路的锅,直接跳过去查网络配置就好。

4. 聚合统计的关键实现:去重、次数统计与关联数组的深层用法

前面几节里已经用了不少关联数组,这里专门展开讲一下awk关联数组在日志统计里的几个高级用法。有人说awk的关联数组和Python的字典类似,理解上确实可以这样类比——键值对、动态扩容、遍历顺序不确定。但awk是文本行处理语言,用关联数组做统计天然比Python写起来更简洁。

4.1 按分钟去重统计:防止突发刷屏撑爆报告

日志分析里有个经典问题:某次网络抖动导致几十台机器在同一秒各打了几十条错误,如果你直接按错误行去统计,报告会膨胀得很夸张。正确做法是“事件去重”,而不是“日志行去重”。

举个例子,同一时刻同一进程的同一错误,应该算一次事件。去重键可以是“时间戳+进程名+错误类型”。

awk ' { key = $1 " " $2 " " process_name " " error_type; if (!(key in seen)) { seen[key] = 1; event_count++; } } END { print "去重后事件数: " event_count }' "$log_file"

这里seen[key]的赋值方式在awk里很常见,第一次遇到键时数组为空,赋值为1,然后后续就跳过。也可以用if (key in seen) continue; seen[key]=1先判断后赋值,两种写法等价,看个人习惯。

4.2 时间窗口切片统计

把连续日志按固定时间窗口切片,是发现“规律性问题”的好方法。比如每天凌晨3点数据库备份会触发大量锁等待,这种周期性问题,在全天统计里根本看不出异常,但按小时切片一眼就能注意到凌晨时段错误数飙升。

时间窗口切片的关键代码就是前面提到的把时间戳转换成窗口标识,这里补充一个细节:如果日志跨天(比如跨过午夜),时间字段里没有日期信息的日志(syslog格式只有Feb 12),切割时要注意跨天边界。我写了一个window_by_month_day_hour函数,把月份、日期、小时拼成窗口ID,这样跨天统计也没问题。

function window_id() { # 入参: "Feb 12 10:24:31" # 返回: "02-12-10" local mon_day hour mon_day=$(echo "$1" | awk '{printf "%s-%02d", $1, $2}') hour=$(echo "$1" | awk '{split($3, t, ":"); print t[1]}') printf "%s-%s" "$mon_day" "$hour" }

这种按月-日-小时的三级窗口,基本能覆盖日常巡检的分析粒度。如果你要更细的分钟级窗口,把小时换成分钟就行,但一般来说分钟级切片适合故障时段回溯,不适合日常巡检,因为输出的维度太碎了,反而不利于快速判断。

4.3 重复错误识别与阈值控制

最后说一个经验之谈:日志分析脚本跑一段时间之后,最大的噪音来源不是错误本身,而是同一错误反复刷屏。比如某个非核心服务每天固定打印几十次Connection refused,虽然每次都真实发生,但只要不处理,它就是恒定噪音,时间长了人就会麻木,反而把真正的严重错误淹没了。

我的做法是引入“重复模式识别”:对相同错误文本出现次数做统计,如果同一错误在时间窗口内出现超过预设阈值,就不再输出每一条具体信息,而是合并成一个汇总条目:“该错误重复出现N次,持续时段为X到Y”。这样一来,报告里既保留了问题的真实度,又不会被刷屏冲垮。

这个逻辑用awk实现非常简单:

if (++error_text_count[error_text] == 1) { first_seen[error_text] = timestamp; } last_seen[error_text] = timestamp;

输出时只对error_text_count[error_text]大于阈值的条目打印汇总信息。实际跑下来,我经常发现报告体积直接压缩到原来的五分之一,但信息量一点没少,反而是因为噪音少了,真正需要关注的点浮出来得更快了。

5. 定时任务与自动巡检:脚本的常态运行与结果落地

脚本写完之后它只是一堆代码,真正让它产生价值的方式是放进定时任务,每天自动巡检,生成报告。然后配合一定的手段,让结果能及时送到人眼前。

5.1 设计巡检任务:频率、时间与参数选择

巡检频率要看具体场景。一般的生产服务器建议每天跑一次,放在凌晨日志轮转之前;高负载或者核心服务所在的机器,建议每6小时跑一次;如果已经进入故障排查阶段,可以手动压到每10分钟跑一次,紧密观察趋势。

crontab的配置我建议写成这样:

# 每天凌晨2:30运行日志分析 30 2 * * * /usr/local/bin/log_analyzer.sh /var/log/messages /var/reports/daily_$(date +\%Y\%m\%d).txt # 每6小时运行一次,仅输出汇总不生成完整附件 0 */6 * * * /usr/local/bin/log_analyzer.sh /var/log/messages --brief

这里有一个重要的坑:cron环境变量和交互式shell不一样,PATH通常只有/usr/bin:/bin,如果你的脚本用到/usr/local/bin下的工具,必须在脚本开头重新设置PATH或者使用全路径。我踩过好几次“手动跑没问题,定时跑就报command not found”的坑,后来统一在脚本开头加一行:

export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

再一个坑是date命令在cron里的%符号要转义,在crontab文件里直接写%(百分比)会被当作特殊字符处理,要用\%转义,这个细节很容易被忽略。

5.2 报警通知的触发阈值与防抖设计

报警机制是整个脚本里最容易写着写着就跑偏的部分,因为很容易陷入“任何错误都报警”,结果谁都被报警淹没。

我的触发策略是双层阈值:

  • 第一层,硬性条件:出现FATAL级别错误、检测到安全事件、检测到OOM,满足任意一条立即报警。
  • 第二层,软性条件:普通错误次数超过设定的阈值,比如30分钟内同一个错误类型超过20次。软性条件报警时,报告标题标注为“警告”,提醒关注但不用立刻处理。

防抖设计的核心是:同一个报警主题在一小时内最多推送一次,避免日志里的几百条错误触发几百条报警——这个问题不解决,报警等于没有。

if [ -f /tmp/log_monitor_last_alert_${alert_key} ]; then last_alert=$(cat /tmp/log_monitor_last_alert_${alert_key}) current=$(date +%s) if [ $((current - last_alert)) -lt 3600 ]; then echo "已发送过报警,跳过本次" >> "$REPORT_FILE" exit 0 fi fi echo $(date +%s) > /tmp/log_monitor_last_alert_${alert_key}

这里用临时文件存时间戳不是最优雅的方案,但在纯shell环境下够用,也不需要额外的数据库依赖。生产环境真要做得更严谨,可以引入lock文件和flock保证并发安全,但单机巡检任务一般跑不冲突。

5.3 报告保留与归档策略:别让存储空间成为新问题

定时任务跑起来的另一个细节是报告文件的持续积累。每天一份报告,看起来不大,但几个月之后可能就有几百兆。我在脚本里加了一个归档逻辑:只保留最近30天的报告,更早的按月份打包成tar.gz存到归档目录,再早30天以上的直接删除。

DAYS_TO_KEEP=30 find "$REPORT_DIR" -name "*.txt" -mtime +$DAYS_TO_KEEP -delete

这个清理动作不要放在主脚本里,建议放在crontab里单独跑,避免日志分析脚本因为清理逻辑挂掉而连带影响分析结果。隔离任务也是一种工程上值得养成的好习惯。

6. 实测中的意外情况与进阶优化

跑这套脚本的实操过程中,有几个意外情况非常典型,写出来给各位参考。这些问题在网上教程里基本不会提,只有真跑过才会撞上。

6.1 日志轮转带来的分析缺口

系统日志一般都有logrotate会按时轮转,昨天的日志会变成messages.1、messages.2.gz这种带序号或者压缩过的文件。如果脚本只盯着/var/log/messages这一个文件分析,就会漏掉大部分前一天下午到凌晨的日志,导致分析结果和实际故障时间对不上。

解决方法是脚本里加一层“日志源补全”逻辑,自动扫描最近几天的轮转文件。gz文件要先解压,可以用zcat直接读取而不用写临时文件,省磁盘空间。

for f in "$LOG_FILE" "$LOG_FILE".1 "$LOG_FILE".2; do if [ -f "$f" ]; then analyze_log "$f" fi done for gz in "$LOG_FILE"*.gz; do [ -f "$gz" ] && zcat "$gz" | analyze_log - done

analyze_log -里用-表示从标准输入读取,这个习惯值得掌握,在管道处理里能省很多临时文件。

6.2 中文/多字节日志的编码兼容问题

这不是每个系统都会遇到,但一旦遇到就非常头疼。某些应用会输出包含中文信息的日志,如果shell脚本的locale没设对,awk匹配中文字符就会出错,甚至直接乱码。

我的建议是不要依赖locale,而是统一用LC_ALL=C强制处理字节流,然后在正则匹配里避免直接写中文字符。真要按中文关键词匹配,可以用十六进制字节序列。不过日常的系统日志分析,绝大部分还是纯英文关键词,这个坑了解即可,真遇到再专项处理也不迟。

6.3 从“能跑”到“好用”的几点优化

脚本多跑几轮之后,我陆续加了不少细节优化,这些优化不影响主流程,但对日常使用的体验影响很大。

控制台加颜色输出。终端汇总里,ERROR行用红色显示,WARN用黄色,正常统计信息用绿色。这对快速浏览帮助很大,一眼就能扫出重点。

if [ -t 1 ]; then RED='\033[0;31m' YELLOW='\033[1;33m' GREEN='\033[0;32m' NC='\033[0m' fi

这个判断太重要了,-t 1检测标准输出是否终端,如果是cron跑重定向到文件里,就不输出颜色控制符,避免报告文件里充满\033这种垃圾字符。

加入并发解析能力。系统日志特别大时,单进程awk处理可能会比较慢,可以用xargs -P把多个轮转文件并发处理,最后再合并统计结果。这里我用的是分文件独立的策略,因为日志文件之间天然独立,并发处理的边界很清楚。

find "$LOG_DIR" -name "messages*" -print0 | xargs -0 -n 1 -P 4 analyze_log

这是典型的“知道并行边界在哪才能并行”的例子,乱并行会导致统计重复或者结果失真,必须确保每个文件只会被一个进程处理,且汇总操作放在所有子进程结束之后。

支持自定义阈值配置。脚本里的各种阈值(重复错误次数、报警频率、窗口大小)都抽到顶部配置区,用变量统一管理,避免每次调参都进代码里翻。

6.4 关联数组的极限与替代方案

有朋友会问,awk的关联数组在日志量很大时会不会内存爆炸?这个担心有道理但也不用过度紧张。以普通文本日志为例,一行几百字节,几十万行日志,awk关联数组的键值对数量取决于“错误类型的种类数”,而不是日志行数。所以正常环境下,一个error_type_count数组的元素数量可能也就几百个,内存占用完全可控。

但如果你的日志包含大量随机性很强的内容,比如每行都有不同的错误码和时间戳组合,键值对数量就会暴涨,这时候就要考虑降低聚合维度。比如只按日期、小时、错误类型三层聚合,去掉具体时间秒;或者直接换用Python/Go这类语言来处理。shell和awk的优势在于“轻量、免编译、处处可用”,真要处理上GB级别的日志,拉Python才是合理的决定。脚本工具选型最忌讳的就是把awk硬用在它不该扛的场景里。

7. 完整脚本框架与练习拓展思路

到这里,核心逻辑基本都讲完了,把各模块串起来,一个完整的脚本框架大概长这样。这不代表唯一写法,但可以作为参考骨架,往里面填自己的业务逻辑。

#!/bin/bash # # 系统日志错误分析脚本 # 用法: log_analyzer.sh [日志路径] [报告路径] export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" LOG_FILE="${1:-/var/log/messages}" REPORT_FILE="${2:-/tmp/log_report.txt}" ERROR_PATTERN="(ERROR|WARN|FATAL|CRIT|fail|timeout|refused|unreachable|denied|segfault|oom|OutOfMemory)" SECURITY_PATTERN="(authentication failure|pam_|sudo|illegal user|permission denied)" TIMEOUT_THRESHOLD=20 ALERT_COOLDOWN=3600 logger() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$REPORT_FILE" } analyze_basic_stats() { local log_file="$1" awk -v pattern="$ERROR_PATTERN" ' BEGIN { total=0; err=0 } { total++; if ($0 ~ pattern) err++ } END { printf "总日志行数: %d\n", total; printf "错误相关行数: %d\n", err; }' "$log_file" } analyze_error_types() { local log_file="$1" awk -v pattern="$ERROR_PATTERN" -v sec_pattern="$SECURITY_PATTERN" ' { if ($0 ~ pattern) { err_type = "other"; if ($0 ~ /FATAL/) err_type = "fatal"; else if ($0 ~ /timeout|timed out/) err_type = "timeout"; else if ($0 ~ /refused/) err_type = "refused"; else if ($0 ~ /OutOfMemory|oom/) err_type = "oom"; else if ($0 ~ /segfault/) err_type = "segfault"; else if ($0 ~ sec_pattern) err_type = "security"; count[err_type]++; } } END { for (t in count) { printf "%-10s: %d\n", t, count[t]; } }' "$log_file" } extract_context() { local log_file="$1" local line_num="$2" local context_lines="${3:-2}" local start=$((line_num - context_lines)) [ $start -lt 1 ] && start=1 local end=$((line_num + context_lines)) sed -n "${start},${end}p" "$log_file" } # 主流程 logger "========== 日志分析开始 $(date '+%F %T') ==========" logger "日志源: $LOG_FILE" analyze_basic_stats "$LOG_FILE" logger "错误类型分布:" analyze_error_types "$LOG_FILE" logger "分析结束" # 报警逻辑(示例:FATAL数量大于1即触发) fatal_count=$(analyze_error_types "$LOG_FILE" | awk '/fatal/{print $2}') if [ -n "$fatal_count" ] && [ "$fatal_count" -gt 1 ]; then echo "[ALERT] 检测到 ${fatal_count} 条FATAL错误,请立即检查!" >> "$REPORT_FILE" fi

这看起来很长,但实际上每个函数都很薄,你可以按需裁剪或者增补。我在实际项目中往往还会追加更细的分类函数、历史趋势比较逻辑,甚至把多台机器的报告汇总到一台机器上做横向对比,那已经是另一个更大的项目了。

对练习shell的人来说,我建议再额外做三个拓展方向。第一个是给脚本增加参数解析,支持--since指定起始时间、--until指定结束时间、--level指定最低等级,让脚本从“一次性全扫”变成“按需灵活分析”。第二个是把分析结果转成更适合程序消费的格式,比如输出JSON,方便对接监控系统。第三个是加入历史趋势对比,自动把本次的错误统计和上周同一天对比,如果某个错误类型周环比增长超过50%,就在报告里重点标注,这种趋势型报警比绝对次数阈值更有预测价值。

最后分享一个实用的调试技巧:开发阶段不要直接用生产日志文件测,而是用tail截取一小段日志存成测试文件,然后把测试文件喂给脚本。这样跑一遍只要几秒,调参迭代都快得多。等逻辑确认无误了,再切回完整日志文件跑。这个小习惯能让你省下大量等待时间,也是我从这个项目里学到的最大效率提升点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询