深色模式
脚本日志与告警
摘要:无人值守的脚本最怕「默默失败」。本文给出一套日志规范(分级、时间戳、结构化)与失败通知方案(Webhook 推送到 IM/告警平台),并说明如何避免告警风暴与重复打扰。
适用环境
bash
which logger curl jq
python3 --version
# systemd 环境可用 journalctl 查看
systemctl is-system-running 2>/dev/null || echo "非 systemd 环境"1
2
3
4
2
3
4
操作步骤
一、日志规范:三条底线
text
1. 每条日志带时间戳与级别(INFO/WARN/ERROR)
2. 输出到 stderr,正常结果输出走 stdout(便于重定向分离)
3. 落盘到固定目录,并配置 logrotate 防止撑满磁盘1
2
3
2
3
二、Bash 日志函数
bash
#!/usr/bin/env bash
set -euo pipefail
LOG_FILE="${LOG_FILE:-/var/log/ops/job.log}"
mkdir -p "$(dirname "$LOG_FILE")"
log() {
local level="${1:-INFO}"; shift
printf '%s [%s] %s\n' "$(date '+%F %T')" "$level" "$*" >&2 # 同时写 stderr
printf '%s [%s] %s\n' "$(date '+%F %T')" "$level" "$*" >> "$LOG_FILE"
}
# 失败时统一记录并带退出码
die() { log ERROR "$*"; exit 1; }
log INFO "任务开始"
[[ -f /etc/app/app.conf ]] || die "配置文件缺失,中止"
log INFO "任务结束"1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
三、Python 日志配置
python
import logging
import logging.handlers
import sys
logger = logging.getLogger("ops")
def setup_logging(log_file="/var/log/ops/job.log", level=logging.INFO):
fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
logger.setLevel(level)
logger.handlers.clear()
# 控制台走 stderr,业务输出走 stdout
sh = logging.StreamHandler(sys.stderr)
sh.setFormatter(fmt)
logger.addHandler(sh)
# 按大小轮转,避免无限增长
fh = logging.handlers.RotatingFileHandler(
log_file, maxBytes=20 * 1024 * 1024, backupCount=5, encoding="utf-8"
)
fh.setFormatter(fmt)
logger.addHandler(fh)
return logger1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
四、配置 logrotate
bash
sudo tee /etc/logrotate.d/ops-scripts >/dev/null <<'EOF'
/var/log/ops/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 root root
}
EOF
# 语法与效果预演
logrotate -d /etc/logrotate.d/ops-scripts1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
五、结构化日志(便于检索)
python
import json, logging, sys
class JsonFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
"level": record.levelname,
"msg": record.getMessage(),
"job": getattr(record, "job", "-"),
}, ensure_ascii=False)
logger.info("备份完成", extra={"job": "backup"})
# 输出: {"ts": "...", "level": "INFO", "msg": "备份完成", "job": "backup"}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
结构化日志能被日志系统直接索引,排查时可以按 job 过滤,比纯文本高效得多。
六、失败通知:Webhook 推送
bash
# notify.sh - 推送告警到 IM/告警平台的 Webhook
WEBHOOK_URL="${WEBHOOK_URL:?需要设置 WEBHOOK_URL}"
notify() {
local level="$1" title="$2" text="$3"
local payload
payload="$(jq -n --arg l "$level" --arg t "$title" --arg c "$text" \
'{level:$l, title:$t, content:$c, host:(env.HOSTNAME)}')"
curl -sS --max-time 10 -X POST -H 'Content-Type: application/json' \
-d "$payload" "$WEBHOOK_URL" >/dev/null 2>&1 || \
echo "告警推送失败(已在日志中记录)" >&2
}
# 用法
trap 'rc=$?; (( rc != 0 )) && notify ALERT "备份脚本失败" "退出码 $rc,主机 $(hostname)"; exit $rc' EXIT1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
危险
Webhook URL 属于凭证,泄露后任何人都能往你的告警群推送消息(甚至钓鱼内容)。必须放在权限受限的环境变量/配置文件中,不要写进脚本或提交到 Git。
七、避免告警风暴
bash
# 用状态文件做抑制:同一问题在 N 分钟内只推一次
STATE_DIR=/var/lib/ops; mkdir -p "$STATE_DIR"
suppress_key="backup_failed"
ts_file="$STATE_DIR/${suppress_key}.ts"
now=$(date +%s)
if [[ -f "$ts_file" ]] && (( now - $(cat "$ts_file") < 1800 )); then
log INFO "告警在抑制窗口内,跳过推送"
else
notify ALERT "备份失败" "详见日志"
echo "$now" > "$ts_file"
fi1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
恢复后应清除抑制状态,否则下次故障不会推送:
bash
rm -f "$STATE_DIR/${suppress_key}.ts" # 成功时清除1
八、告警内容要可执行
text
坏例子:「脚本失败」
好例子:「[prod] backup.sh 在 web01 失败(退出码 1,步骤:上传到 OSS),
日志:/var/log/ops/backup.log,最近 5 行:...」1
2
3
2
3
一条好告警应包含:环境、主机、脚本名、失败步骤、日志位置、建议动作。
验证
- [ ] 日志含时间戳与级别,stdout 与 stderr 分离(
./x.sh 2>/dev/null只留业务输出) - [ ]
logrotate -d预演无报错 - [ ] 故意让脚本失败,能收到一次告警推送
- [ ] 30 分钟内连续失败两次,只推送一次(抑制生效)
- [ ] 失败修复后再次失败,仍能推送(抑制状态已清除)
常见坑
- 日志打到 stdout 污染业务输出:脚本输出被其它程序解析时会混入日志行。
- 不配置轮转:脚本跑几个月后日志撑满磁盘,反而成为故障源。
- 告警太频繁:每次失败都推送,人会直接屏蔽该群,等于没有告警。
- 抑制窗口不清除:第一次故障后再也不告警,问题长期无人知。
- Webhook 硬编码在脚本里:随代码泄露,且换群改地址要改所有机器。