1. Shell脚本日志记录的重要性与现状
在运维和自动化领域,Shell脚本就像是我们日常工作的"瑞士军刀"。但很多脚本在运行后就像"黑盒子"——你知道它执行了,但不知道具体发生了什么。特别是在生产环境中,当脚本执行失败时,如果没有详细的日志记录,排查问题就像大海捞针。
我见过太多这样的场景:一个定时执行的备份脚本突然失败,但因为没有足够的日志信息,运维团队不得不花费数小时甚至数天来重现和定位问题。更糟糕的是,有些关键业务脚本在运行中出现部分失败(比如处理100个文件只成功了99个),但由于缺乏细粒度的日志记录,这种问题可能长期不被发现。
2. 日志记录优化的核心原则
2.1 日志分级:从DEBUG到FATAL
一个良好的日志系统应该像医院的急诊分级制度:
- DEBUG:开发调试用,记录每个步骤的详细信息
- INFO:常规运行信息,记录关键里程碑
- WARN:需要注意但不影响继续运行的情况
- ERROR:操作失败但脚本仍可继续
- FATAL:致命错误,必须立即终止
bash复制log() {
local level=$1
local message=$2
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
case $level in
DEBUG) echo "[$timestamp] [DEBUG] $message" ;;
INFO) echo "[$timestamp] [INFO] $message" ;;
WARN) echo "[$timestamp] [WARN] $message" >&2 ;;
ERROR) echo "[$timestamp] [ERROR] $message" >&2 ;;
FATAL) echo "[$timestamp] [FATAL] $message" >&2
exit 1 ;;
esac
}
2.2 日志内容的最佳实践
好的日志应该像侦探的记事本——包含足够的信息来重建现场:
- 时间戳:精确到秒(关键问题可能需要毫秒级)
- 执行环境:主机名、用户名、脚本路径
- 关键参数:脚本接收的输入参数
- 操作对象:正在处理的文件、数据库记录等
- 执行结果:成功/失败及原因
bash复制log "INFO" "Starting backup process on $(hostname) as $USER"
log "INFO" "Script arguments: $@"
log "INFO" "Processing file: $filename"
3. 高级日志技术实现
3.1 日志轮转与归档
日志文件就像房间——不整理就会变得一团糟。我们需要:
- 按大小或时间分割日志
- 压缩旧日志节省空间
- 自动清理过期日志
bash复制setup_log_rotation() {
local log_file=$1
local max_size_mb=10
local keep_days=30
# 检查日志大小
local size=$(du -m "$log_file" | cut -f1)
if [ "$size" -gt "$max_size_mb" ]; then
mv "$log_file" "${log_file}.$(date +%Y%m%d%H%M%S)"
gzip "${log_file}.$(date +%Y%m%d%H%M%S)"
fi
# 清理旧日志
find /var/log/myapp -name "*.gz" -mtime +$keep_days -delete
}
3.2 结构化日志(JSON格式)
对于需要被日志分析系统处理的场景,JSON格式是更好的选择:
bash复制log_json() {
local level=$1
shift
local message="$*"
local timestamp=$(date --iso-8601=seconds)
local hostname=$(hostname)
jq -n \
--arg timestamp "$timestamp" \
--arg hostname "$hostname" \
--arg level "$level" \
--arg message "$message" \
--arg script "$0" \
--arg pid "$$" \
'{
timestamp: $timestamp,
hostname: $hostname,
level: $level,
message: $message,
script: $script,
pid: $pid
}' >> /var/log/myapp.log
}
4. 实战:完整的日志系统实现
4.1 日志初始化函数
bash复制init_logging() {
local script_name=$(basename "$0")
local log_dir="/var/log/${script_name%.*}"
mkdir -p "$log_dir"
LOG_FILE="$log_dir/$(date +%Y%m%d).log"
exec 3>&1 4>&2
exec > >(tee -a "$LOG_FILE") 2>&1
trap 'cleanup_logging' EXIT
}
cleanup_logging() {
exec 1>&3 2>&4
# 可以在这里添加日志压缩或发送通知的逻辑
}
4.2 实际应用示例
bash复制#!/bin/bash
# 引入日志功能
source /path/to/logging.sh
init_logging
log "INFO" "Starting database migration"
# 检查数据库连接
if ! mysql -u "$DB_USER" -p"$DB_PASS" -h "$DB_HOST" -e "SHOW DATABASES"; then
log "ERROR" "Failed to connect to database"
exit 1
fi
# 迁移数据
for table in "${TABLES[@]}"; do
log "INFO" "Migrating table: $table"
if ! mysqldump -u "$SRC_USER" -p"$SRC_PASS" "$SRC_DB" "$table" | \
mysql -u "$DST_USER" -p"$DST_PASS" "$DST_DB"; then
log "ERROR" "Failed to migrate table $table"
# 不是致命错误,继续迁移其他表
fi
done
log "INFO" "Database migration completed"
5. 日志分析与问题排查技巧
5.1 常用日志分析命令
- 查看错误日志:
bash复制grep -i "error" /var/log/myapp.log
- 统计错误类型:
bash复制awk '/\[ERROR\]/ {print $5}' /var/log/myapp.log | sort | uniq -c | sort -nr
- 时间范围内的日志:
bash复制sed -n '/2023-05-01 10:00:00/,/2023-05-01 11:00:00/p' /var/log/myapp.log
- 跟踪实时日志:
bash复制tail -f /var/log/myapp.log | grep --line-buffered "ERROR\|WARN"
5.2 日志可视化方案
对于大型系统,可以考虑:
- ELK Stack (Elasticsearch + Logstash + Kibana)
- Grafana + Loki
- 商业方案如Splunk
简单的本地可视化:
bash复制# 生成错误统计图表
awk '/\[ERROR\]/ {print $1" "$2" "$5}' /var/log/myapp.log | \
gnuplot -p -e 'set xdata time; set timefmt "%Y-%m-%d %H:%M:%S";
set format x "%H:%M"; plot "/dev/stdin" using 1:3 with points'
6. 性能考量与最佳实践
6.1 日志性能优化
- 异步日志:对于高性能场景,考虑使用syslog或专门的日志守护进程
- 批量写入:减少磁盘I/O次数
- 日志级别动态调整:生产环境可以适当降低日志级别
bash复制# 动态调整日志级别
if [ "$ENVIRONMENT" = "production" ]; then
LOG_LEVEL="INFO"
else
LOG_LEVEL="DEBUG"
fi
log() {
# 只记录大于等于当前日志级别的消息
declare -A levels=([DEBUG]=0 [INFO]=1 [WARN]=2 [ERROR]=3 [FATAL]=4)
local msg_level=$1
local message=$2
if [ ${levels[$msg_level]} -ge ${levels[$LOG_LEVEL]} ]; then
echo "$(date) [$msg_level] $message"
fi
}
6.2 安全注意事项
-
避免记录敏感信息:
- 密码、API密钥
- 个人身份信息(PII)
- 信用卡号等支付信息
-
日志文件权限:
bash复制chmod 640 /var/log/myapp.log
chown root:adm /var/log/myapp.log
- 日志完整性保护:
bash复制# 使用immutable属性防止篡改
chattr +i /var/log/audit.log
7. 企业级日志方案进阶
7.1 集中式日志收集
对于分布式系统,需要考虑:
- 使用rsyslog或fluentd收集日志
- 日志标准化格式
- 添加唯一追踪ID
bash复制# 为每个请求生成唯一ID
generate_request_id() {
echo "$(date +%s%N)-$RANDOM"
}
REQUEST_ID=$(generate_request_id)
log() {
echo "$(date) [$(hostname)] [$REQUEST_ID] [$1] $2"
}
7.2 审计日志的特殊要求
对于需要满足合规要求的审计日志:
- 防篡改:使用WORM(Write Once Read Many)存储
- 完整性校验:定期生成哈希值
- 精确时间同步:使用NTP服务
- 长期归档:符合法定保留期限
bash复制# 生成每日日志哈希
generate_log_hash() {
local log_file=$1
sha256sum "$log_file" >> /var/log/audit_hashes.txt
gpg --sign /var/log/audit_hashes.txt
}
8. 测试你的日志系统
8.1 日志测试用例
好的日志系统需要像代码一样被测试:
- 测试日志级别过滤
- 测试日志轮转
- 测试日志内容完整性
- 测试性能影响
bash复制test_logging() {
# 测试各级别日志
for level in DEBUG INFO WARN ERROR FATAL; do
log "$level" "This is a $level test message"
done
# 测试日志轮转
fallocate -l 11M /var/log/myapp.log
setup_log_rotation /var/log/myapp.log
[ -f "/var/log/myapp.log.$(date +%Y%m%d%H%M%S).gz" ] || echo "Rotation test failed"
# 测试性能
time {
for i in {1..1000}; do
log "INFO" "Test message $i"
done
}
}
8.2 监控日志系统健康
- 监控日志目录空间使用
- 检查日志进程是否存活
- 验证日志是否持续写入
bash复制check_logging_health() {
# 检查磁盘空间
local usage=$(df -h /var/log | awk 'NR==2 {print $5}' | tr -d '%')
[ "$usage" -gt 90 ] && send_alert "Log partition is ${usage}% full"
# 检查日志是否更新
local last_modified=$(stat -c %Y /var/log/myapp.log)
local now=$(date +%s)
[ $((now - last_modified)) -gt 3600 ] && send_alert "Log file not updated in 1 hour"
}
9. 从日志到可观测性
现代系统监控已经超越了简单的日志记录,走向了可观测性的三个支柱:
- 日志(Logs):离散事件记录
- 指标(Metrics):数值型时间序列数据
- 追踪(Traces):请求的端到端生命周期
bash复制# 简单的指标收集
collect_metrics() {
local metric_name=$1
local value=$2
echo "$(date +%s),$metric_name,$value" >> /var/metrics.csv
}
# 在脚本中使用
collect_metrics "script_execution_time" "$duration"
collect_metrics "processed_files" "$count"
10. 文化变革:让日志成为团队习惯
技术实现只是基础,真正的挑战是文化变革:
- 代码审查中加入日志审查
- 将日志质量纳入定义完成(DOD)
- 定期进行日志走查(log walkthroughs)
- 用真实事故展示好日志的价值
提示:建立一个"日志知识库",收集各种典型问题的日志模式,帮助团队快速识别问题。