每天手动处理服务器日志到凌晨 用了这5个Shell脚本后准点下班 附完整代码和避坑指南
凌晨两点,你盯着屏幕上滚动的一行行日志,眼睛干涩,咖啡已经凉了第三杯。鼠标点得指尖发麻,就为了从几千条ERROR里找出那条该死的异常栈。这种日子,谁爱过谁过吧——直到我写出了下面这5个脚本。
不是吹牛,自从用上它们,我每天六点准时合上笔记本,回家睡个整觉。今天把压箱底的全掏出来,代码可以直接拿去用,避坑指南是拿真金白银买来的教训。
一、日志清理脚本:让磁盘不再告急
服务器跑着跑着,日志文件能把你硬盘塞满。我见过太多人等着磁盘满了才慌忙清理,结果业务直接挂掉。这个脚本每天早上八点自动运行,清理三天前的旧日志,保留最近日志结构不动。
#!/bin/bash
# log_clean.sh - 自动清理过期日志文件
# 每天凌晨2点执行,清理3天前的日志
LOG_BASE_DIR="/var/log/myapp"
RETENTION_DAYS=3
BACKUP_DIR="/data/log_backup"
DATE=$(date +%Y%m%d_%H%M%S)
CLEAN_LOG="${LOG_BASE_DIR}/clean_$(date +%Y%m%d).log"
# 颜色定义,输出更清晰
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 日志函数
log_info() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] INFO${NC} $1" | tee -a "$CLEAN_LOG"; }
log_warn() { echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] WARN${NC} $1" | tee -a "$CLEAN_LOG"; }
log_error() { echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ERROR${NC} $1" | tee -a "$CLEAN_LOG"; }
# 检查目录是否存在
if [ ! -d "$LOG_BASE_DIR" ]; then
log_error "日志目录不存在: $LOG_BASE_DIR"
exit 1
fi
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 统计清理前的磁盘使用
DISK_BEFORE=$(df -h "$LOG_BASE_DIR" | tail -1 | awk '{print $5}')
log_info "清理前磁盘使用: $DISK_BEFORE"
# 开始清理
CLEANED_COUNT=0
CLEANED_SIZE=0
# 查找并压缩3天前的日志(先压缩再删除,避免数据丢失)
find "$LOG_BASE_DIR" -type f -name "*.log" -mtime +${RETENTION_DAYS} | while read logfile; do
# 跳过正在写入的日志文件(最后访问时间大于5分钟的才处理)
if [ -f "$logfile" ] && [ $(find "$logfile" -mmin +5 | wc -l) -eq 1 ]; then
# 移动到备份目录并压缩
filename=$(basename "$logfile")
mv "$logfile" "$BACKUP_DIR/${filename%.log}_${DATE}.log"
gzip "$BACKUP_DIR/${filename%.log}_${DATE}.log" 2>/dev/null
CLEANED_COUNT=$((CLEANED_COUNT + 1))
log_info "已清理: $logfile"
else
log_warn "跳过正在写入的日志: $logfile"
fi
done
# 清理超过7天的备份(备份保留一周即可)
find "$BACKUP_DIR" -type f -mtime +7 -delete 2>/dev/null
# 统计清理后的磁盘使用
DISK_AFTER=$(df -h "$LOG_BASE_DIR" | tail -1 | awk '{print $5}')
log_info "清理后磁盘使用: $DISK_AFTER"
log_info "本次共清理 $CLEANED_COUNT 个日志文件"
# 磁盘使用超过90%时发送告警
USAGE_NUM=$(df "$LOG_BASE_DIR" | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$USAGE_NUM" -gt 90 ]; then
log_error "磁盘使用率过高: ${USAGE_NUM}%,请及时处理!"
# 这里可以接钉钉/企业微信告警
fi
exit 0
关键细节说明:
脚本里有个容易被忽视的细节——检查文件最后修改时间是否超过5分钟。这是为了判断日志是否还在被写入。你想想,如果日志文件还在被应用实时写入,你直接删掉它,轻则日志丢失,重则应用报错。find 命令的 -mmin +5 就是干这个的。
二、错误日志分析脚本:5分钟定位问题根源
以前我最头疼的就是从几万条日志里找ERROR。现在这个脚本跑完,错误分类、频次、时间分布一目了然。
#!/bin/bash
# error_analyze.sh - 错误日志智能分析
# 分析指定时间范围内的错误日志并生成报告
LOG_FILE="${1:-/var/log/myapp/application.log}"
START_TIME="${2:-$(date -d '24 hours ago' '+%Y-%m-%d %H:%M:%S')}"
END_TIME="${3:-$(date '+%Y-%m-%d %H:%M:%S')}"
REPORT_DIR="/data/log_reports"
REPORT_FILE="${REPORT_DIR}/error_report_$(date +%Y%m%d_%H%M%S).txt"
# 创建报告目录
mkdir -p "$REPORT_DIR"
# 检查日志文件是否存在
if [ ! -f "$LOG_FILE" ]; then
echo "错误: 日志文件不存在 - $LOG_FILE"
exit 1
fi
echo "========================================" > "$REPORT_FILE"
echo " 错误日志分析报告" >> "$REPORT_FILE"
echo "========================================" >> "$REPORT_FILE"
echo "日志文件: $LOG_FILE" >> "$REPORT_FILE"
echo "分析时间: $START_TIME 至 $END_TIME" >> "$REPORT_FILE"
echo "生成时间: $(date '+%Y-%m-%d %H:%M:%S')" >> "$REPORT_FILE"
echo "========================================" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# 统计各类错误数量
echo "[一、错误类型统计]" >> "$REPORT_FILE"
echo "----------------------------------------" >> "$REPORT_FILE"
# 使用grep提取错误行,然后用awk统计
grep -E "ERROR|FATAL|Exception|WARN" "$LOG_FILE" 2>/dev/null | \
grep -E "$START_TIME.*$END_TIME|$START_TIME|$END_TIME" 2>/dev/null | \
sed 's/\[.*\]//g' | \ # 去掉时间戳方便分类
awk -F'ERROR\|FATAL\|Exception\|WARN' '{if($2!="") print $2}' | \
sort | uniq -c | sort -rn | head -20 >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# 提取错误堆栈摘要
echo "[二、异常堆栈摘要(前10条)]" >> "$REPORT_FILE"
echo "----------------------------------------" >> "$REPORT_FILE"
grep -A 10 "Exception\|Error" "$LOG_FILE" 2>/dev/null | \
grep -E "Caused by|at |Exception" | \
head -50 >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# 按小时统计错误分布
echo "[三、错误时间分布(按小时)]" >> "$REPORT_FILE"
echo "----------------------------------------" >> "$REPORT_FILE"
grep -E "ERROR|FATAL|Exception" "$LOG_FILE" 2>/dev/null | \
grep -oP '\d{4}-\d{2}-\d{2} \d{2}' | \
sort | uniq -c | sort -k2 >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# 找出高频错误IP(如果是Web应用)
echo "[四、高频错误IP统计(Top 10)]" >> "$REPORT_FILE"
echo "----------------------------------------" >> "$REPORT_FILE"
grep -E "ERROR|Exception" "$LOG_FILE" 2>/dev/null | \
grep -oP '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' | \
sort | uniq -c | sort -rn | head -10 >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
# 关键错误列表(需要人工关注的)
echo "[五、需人工关注的关键错误]" >> "$REPORT_FILE"
echo "----------------------------------------" >> "$REPORT_FILE"
grep -E "OutOfMemoryError|Connection refused|timeout|数据库连接|磁盘满" "$LOG_FILE" 2>/dev/null >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"
echo "报告生成完毕: $REPORT_FILE" >> "$REPORT_FILE"
echo "========================================" >> "$REPORT_FILE"
# 输出到终端
cat "$REPORT_FILE"
exit 0
运行方式很简单:
# 分析最近24小时的日志(默认)
bash error_analyze.sh
# 指定时间范围分析
bash error_analyze.sh /var/log/myapp/app.log "2024-01-15 08:00:00" "2024-01-15 18:00:00"
这个脚本最实用的地方是”需人工关注的关键错误”部分。OutOfMemoryError、连接超时、数据库问题这些,都是优先级最高的,直接挑出来让你一眼看到重点。
三、日志轮转增强脚本:比cron更可靠的轮转
Linux自带的logrotate有时候不够灵活,特别是你需要按大小轮转、保留特定数量、还要通知下游系统的时候。这个脚本弥补了那些空白。
#!/bin/bash
# log_rotate_enhanced.sh - 增强版日志轮转脚本
# 支持按大小轮转、保留策略、轮转后通知
LOG_DIR="${1:-/var/log/myapp}"
MAX_SIZE_MB="${2:-100}" # 日志文件超过100MB则轮转
BACKUP_COUNT="${3:-7}" # 保留最近7个备份
DATE=$(date +%Y%m%d_%H%M%S)
STATUS_LOG="${LOG_DIR}/rotate_status.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$STATUS_LOG"
}
log "开始执行日志轮转,目标目录: $LOG_DIR"
# 检查目录
if [ ! -d "$LOG_DIR" ]; then
log "错误: 目录不存在 $LOG_DIR"
exit 1
fi
ROTATED=0
# 遍历所有log文件
find "$LOG_DIR" -type f -name "*.log" ! -name "*.log.*" ! -name "rotate_status.log" | while read logfile; do
# 获取文件大小(单位:MB)
FILE_SIZE=$(du -m "$logfile" 2>/dev/null | awk '{print $1}')
if [ "$FILE_SIZE" -ge "$MAX_SIZE_MB" ]; then
log "发现大文件: $logfile (${FILE_SIZE}MB)"
# 轮转文件名
ROTATED_NAME="${logfile}.${DATE}.log"
# 先删除最老的备份(保持数量限制)
ls -t "${logfile}".*.log 2>/dev/null | tail -n +$((BACKUP_COUNT + 1)) | xargs rm -f 2>/dev/null
# 移动并重命名
mv "$logfile" "$ROTATED_NAME"
# 创建空文件保持应用不报错
touch "$logfile"
chmod 644 "$logfile"
# 可选:压缩旧日志节省空间
if command -v gzip &> /dev/null; then
gzip "$ROTATED_NAME"
log "已压缩: ${ROTATED_NAME}.gz"
fi
ROTATED=$((ROTATED + 1))
log "轮转完成: $logfile -> ${ROTATED_NAME}.gz"
fi
done
# 发送轮转完成通知(钉钉/企业微信/Webhook)
if [ "$ROTATED" -gt 0 ]; then
WEBHOOK_URL="${WEBHOOK_URL:-}"
if [ -n "$WEBHOOK_URL" ]; then
curl -s -X POST "$WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"日志轮转完成,共处理 ${ROTATED} 个文件\"}}" \
--connect-timeout 5 2>/dev/null
log "已发送轮转通知"
fi
fi
log "日志轮转执行完毕"
exit 0
把这个脚本加到 crontab 里,每小时检查一次:
# 每小时检查一次日志大小
0 * * * * /opt/scripts/log_rotate_enhanced.sh /var/log/myapp 100 7
四、日志聚合脚本:多服务器日志统一收集
你有5台、10台甚至更多服务器,每台都去查日志?不存在的。这个脚本配合rsync或者logstash,把分散的日志聚合到一个地方。
#!/bin/bash
# log_aggregate.sh - 日志聚合收集脚本
# 从多台服务器收集日志到统一存储
COLLECT_DIR="/data/collected_logs"
REMOTE_SERVERS=(
"192.168.1.10"
"192.168.1.11"
"192.168.1.12"
"10.0.0.5"
)
LOG_PATHS=(
"/var/log/myapp/application.log"
"/var/log/myapp/error.log"
"/var/log/nginx/access.log"
)
SSH_USER="logcollector"
SSH_KEY="/home/${SSH_USER}/.ssh/id_rsa"
DATE=$(date +%Y%m%d)
HOUR=$(date +%H)
STATUS_FILE="/var/log/log_aggregate_${DATE}.log"
mkdir -p "$(dirname "$STATUS_FILE")" "$COLLECT_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$STATUS_FILE"
}
log "========== 开始日志聚合 =========="
SUCCESS_COUNT=0
FAIL_COUNT=0
for SERVER in "${REMOTE_SERVERS[@]}"; do
log "正在收集 ${SERVER} 的日志..."
# 创建该服务器的收集目录
SERVER_DIR="${COLLECT_DIR}/${SERVER}"
mkdir -p "$SERVER_DIR"
for LOG_PATH in "${LOG_PATHS[@]}"; do
# 获取日志文件名
LOG_NAME=$(basename "$LOG_PATH" .log)
REMOTE_FILE="${LOG_PATH}"
LOCAL_FILE="${SERVER_DIR}/${LOG_NAME}_${DATE}_${HOUR}.log"
# 使用scp收集日志(只收集最近1小时的内容)
# 先在远程服务器上截取内容,避免传输大文件
SSH_CMD="tail -n 50000 '${REMOTE_FILE}' 2>/dev/null | head -n 10000"
if ssh -i "$SSH_KEY" -o ConnectTimeout=5 -o StrictHostKeyChecking=no "$SSH_USER@${SERVER}" "$SSH_CMD" > "$LOCAL_FILE" 2>/dev/null; then
# 统计收集到的行数
LINE_COUNT=$(wc -l < "$LOCAL_FILE")
log "✓ ${SERVER}:${REMOTE_FILE} 收集到 ${LINE_COUNT} 行"
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
else
log "✗ ${SERVER}:${REMOTE_FILE} 收集失败"
FAIL_COUNT=$((FAIL_COUNT + 1))
fi
done
done
log "聚合完成: 成功 ${SUCCESS_COUNT}, 失败 ${FAIL_COUNT}"
# 聚合后的日志可以进一步处理或推送到ELK
if [ -f "$COLLECT_DIR/all_logs_${DATE}.log" ]; then
# 合并所有日志到一个文件(可选)
cat "${COLLECT_DIR}"/*/*.log > "${COLLECT_DIR}/all_logs_${DATE}.log" 2>/dev/null
log "已生成聚合日志: all_logs_${DATE}.log"
fi
exit 0
运行后你会在 /data/collected_logs/ 下看到按服务器和日期组织的日志结构,分析起来方便得多。
五、日志实时监控脚本:有问题立刻知道
等日志出事了再去看,黄花菜都凉了。这个脚本实时监控关键错误,触发条件立刻发告警。
#!/bin/bash
# log_monitor.sh - 日志实时监控告警
# 监控关键错误并发送告警通知
LOG_FILE="${1:-/var/log/myapp/application.log}"
ALERT_LOG="/var/log/log_monitor_alert.log"
CHECK_INTERVAL="${2:-30}" # 每秒检查一次
# 告警规则配置
declare -A ALERT_RULES
ALERT_RULES=(
["OutOfMemoryError"]="critical"
["Connection refused"]="warning"
["timeout"]="warning"
["Database error"]="critical"
["disk full"]="critical"
[" Segmentation Fault"]="fatal"
)
# 告警冷却时间(同一错误5分钟内不重复告警)
COOLDOWN=300
ALERT_STATE_DIR="/tmp/log_monitor_state"
mkdir -p "$ALERT_STATE_DIR"
log_alert() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$ALERT_LOG"
# 根据级别发送不同渠道告警
case "$level" in
"fatal"|"critical")
send_dingtalk_alert "$message" "🚨 严重告警"
;;
"warning")
send_dingtalk_alert "$message" "⚠️ 警告"
;;
esac
}
send_dingtalk_alert() {
local message="$1"
local title="$2"
local webhook="${DINGTALK_WEBHOOK:-}"
if [ -z "$webhook" ]; then
return
fi
curl -s -X POST "$webhook" \
-H 'Content-Type: application/json' \
-d "{
\"msgtype\": \"markdown\",
\"markdown\": {
\"title\": \"${title}\",
\"text\": \"### ${title}\n> **日志文件**: ${LOG_FILE}\n> **时间**: $(date '+%Y-%m-%d %H:%M:%S')\n> **内容**: ${message}\n> **服务器**: $(hostname)\"
}
}" --connect-timeout 5 2>/dev/null
}
# 检查是否需要冷却
should_alert() {
local key="$1"
local state_file="${ALERT_STATE_DIR}/${key//[/_}.state"
if [ -f "$state_file" ]; then
last_alert=$(cat "$state_file")
current_time=$(date +%s)
elapsed=$((current_time - last_alert))
if [ "$elapsed" -lt "$COOLDOWN" ]; then
return 1 # 冷却中,不告警
fi
fi
date +%s > "$state_file"
return 0 # 可以告警
}
# 主监控循环
if [ ! -f "$LOG_FILE" ]; then
echo "错误: 日志文件不存在 - $LOG_FILE"
exit 1
fi
# 获取文件当前大小,从末尾开始监控
TAIL_POS=$(wc -c < "$LOG_FILE")
log_alert "info" "监控已启动,日志文件: $LOG_FILE"
while true; do
# 等待检查间隔
sleep "$CHECK_INTERVAL"
# 检查文件是否有新内容
CURRENT_POS=$(wc -c < "$LOG_FILE" 2>/dev/null)
if [ "$CURRENT_POS" -gt "$TAIL_POS" ]; then
# 读取新增内容
NEW_CONTENT=$(tail -c +$((TAIL_POS + 1)) "$LOG_FILE" 2>/dev/null)
TAIL_POS=$CURRENT_POS
# 检查每条规则
for pattern in "${!ALERT_RULES[@]}"; do
if echo "$NEW_CONTENT" | grep -q "$pattern"; then
level="${ALERT_RULES[$pattern]}"
if should_alert "$pattern"; then
# 提取关键信息
relevant_line=$(echo "$NEW_CONTENT" | grep "$pattern" | tail -1 | cut -c1-200)
log_alert "$level" "$relevant_line"
fi
fi
done
fi
# 检查文件是否被轮转(大小变小了)
if [ "$CURRENT_POS" -lt "$TAIL_POS" ]; then
TAIL_POS=$CURRENT_POS
log_alert "info" "检测到日志文件轮转,重置监控位置"
fi
done
把这个脚本放到后台运行:
# 后台启动监控
nohup bash log_monitor.sh /var/log/myapp/application.log 10 > /dev/null 2>&1 &
# 或者用supervisor管理
# [program:log_monitor]
# command=bash /opt/scripts/log_monitor.sh /var/log/myapp/application.log 10
# autostart=true
# autorestart=true
六、避坑指南:这些坑我踩过,你别再踩了
坑一:直接rm删除正在写入的日志
这是最常见的错误。你以为删了就是干净了,其实文件句柄还占着磁盘空间,直到应用重启才会释放。正确做法是清空内容而不是删除文件:
# 错误做法
rm /var/log/myapp/app.log
# 正确做法
> /var/log/myapp/app.log
# 或者
cat /dev/null > /var/log/myapp/app.log
坑二:脚本权限问题
日志脚本通常需要root权限,但随意给777权限是安全炸弹。正确做法是用sudoers配置特定命令:
# /etc/sudoers.d/log_scripts
www-data ALL=(root) NOPASSWD: /opt/scripts/log_rotate_enhanced.sh
坑三:时间格式不一致
不同应用的日志时间格式五花八门,分析时经常对不上。统一用ISO 8601格式是最稳妥的:
# 推荐格式
2024-01-15 14:30:25,123 ERROR [main] - 连接超时
# 避免这种
[Jan 15, 2024 2:30:25 PM] 错误 ...
坑四:没有测试直接上生产
脚本写好别急着加cron,先手动跑一遍看看效果。拿小日志文件测试,确认没问题再加到定时任务里。我的建议是先在测试环境跑一周。
坑五:忽略了日志的写入锁
高并发场景下,多个进程同时写日志可能互相阻塞。给每个进程独立的日志文件,然后用聚合脚本汇总,比单文件高并发写入性能好得多。
最后说两句
这五个脚本,我从半年前开始用,每天帮我节省至少两个小时。一开始也踩过不少坑,比如第一次写轮转脚本的时候把线上日志全清了,吓得我冷汗直流。后来慢慢打磨,才成了现在这个版本。
代码都贴在这里了,直接复制粘贴就能用。不过每个人的环境不一样,建议你先在测试服务器跑一遍,确认没问题再上生产。有问题随时在评论区问我,看到就回。
准点下班不是梦,脚本帮你干脏活。
