在 linux 运维中,有常见的两类风险:
- Nginx 出现持续 5xx,业务异常但无人感知
- SSH 被持续暴力破解,安全风险不断累积
很多人第一反应是上 ELK、Prometheus,但对中小规模场景来说,这往往成本过高。
其实,只用系统自带工具(tail + awk + curl),就可以实现一套稳定、实时、低成本的日志告警方案。
但要注意:绝大多数“网上脚本”都存在严重问题,例如
- pipeline 子 shell 导致计数失效
- 伪“1分钟窗口”(实际只是定时清零)
- grep 缓冲导致告警延迟
- 日志轮转后监控中断

一、设计原则
这套方案遵循以下原则:
1. 单进程状态管理(避免 shell 坑)
所有统计逻辑在 awk 内完成,避免:
- 子进程变量隔离
- 状态不一致
2. 真正的滑动时间窗口
采用:
- systime() + 事件清理机制
- 而不是简单“每分钟清零”
实现“近似实时滑动窗口”。
3. 不依赖日志格式细节
只依赖:
- $1 → IP
- $9 → HTTP 状态码
避免 fragile 正则。
4. 低依赖
仅依赖:
- tail
- awk
- curl
5. 安全优先
- 不使用 eval
- 支持白名单
- webhook 校验严格
二、终极生产脚本
保存为:
36/usr/local/bin/log-alert.sh1.
#!/bin/bash# ================= 配置项 =================NGINX_LOG="/var/log/nginx/access.log"SSH_MODE="file"# file 或 journaldSSH_LOG="/var/log/secure"WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的key"NGINX_THRESHOLD=10SSH_THRESHOLD=5SILENT=300STATE_DIR="/dev/shm/log_alert"mkdir -p "$STATE_DIR"chmod 700 "$STATE_DIR"WHITELIST=("127.0.0.1""10.0.0.0/8""192.168.0.0/16")# ================= 工具检查 =================for cmd in tail awk curl; docommand -v $cmd >/dev/null || { echo"缺少依赖: $cmd"; exit 1; }done
# ================= webhook 校验 =================resp=$(curl -s --max-time 5 "$WEBHOOK")echo"$resp" | grep -q '"errcode":0' || {echo"Webhook 无效或不可用"exit 1}# ================= 告警函数 =================send_alert() {local title="$1"local content="$2"
curl -s -o /dev/null -X POST "$WEBHOOK" \ -H "Content-Type: application/json" \ -d "{
\"msgtype\": \"text\",
\"text\": {\"content\": \"${title}\n${content}\"}
}" &}# ================= 白名单判断 =================is_white() {local ip="$1"
[[ "$ip" == "127.0.0.1" ]] && return 0for w in"${WHITELIST[@]}"; do
[[ "$ip" == "$w" ]] && return 0donereturn 1}# ================= NGINX 监控 =================tail -F "$NGINX_LOG" | awk -v t="$NGINX_THRESHOLD" -v s="$SILENT"'{
ip=$1
code=$9
now=systime()
if (code !~ /^5[0-9][0-9]$/) next
key=ip
events[key][++cnt[key]]=now
# 清理旧数据 for (i in events[key]) {
if (now - events[key][i] > 60) delete events[key][i]
}
n=0
for (i in events[key]) n++
if (n >= t && now - last[key] > s) {
last[key]=now
print "NGINX|"ip"|"n fflush()
}}' | while IFS='|'read -r type ip count; do
is_white "$ip" && continue
send_alert "Nginx 5xx告警""IP: $ip\n1分钟: $count 次"done ================= SSH 监控 =================if [[ "$SSH_MODE" == "journald" ]]; then SRC="journalctl -f _COMM=sshd"else
SRC="tail -F $SSH_LOG"fi
bash -c "$SRC" | grep "Failed password" | awk -v t="$SSH_THRESHOLD" -v s="$SILENT"'{
for(i=1;i 60) delete events[key][i]
}
n=0
for(i in events[key]) n++
if(n >= t && now - last[key] > s){
last[key]=now
print "SSH|"ip"|"n fflush()
}}' | while IFS='|'read -r type ip count; do
is_white "$ip" && continue
send_alert "SSH暴力破解告警""IP: $ip\n失败次数: $count"done &wait -n
exit 11.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.59.60.61.62.63.64.65.66.67.68.69.70.71.72.73.74.75.76.77.78.79.80.81.82.83.84.85.86.87.88.89.90.91.92.93.94.95.96.97.98.99.100.101.102.103.104.105.106.107.108.109.110.111.112.113.114.115.116.117.118.119.120.121.122.123.124.125.126.127.128.129.130.131.132.133.134.135.136.137.138.139.140.141.142.143.144.三、systemd
[Unit]Description=Log Alert Service After=network.target[Service]ExecStart=/bin/bash /usr/local/bin/log-alert.sh Restart=always RestartSec=3StandardOutput=journal StandardError=journal NoNewPrivileges=yes ProtectSystem=full[Install]WantedBy=multi-user.target1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
四、必须知道的几个关键点
1. 这是“近似滑动窗口”
不是精确时间序列系统,但对运维告警完全够用。
2. /dev/shm 是内存
重启后状态会丢失(正常)。
3. 默认只支持简单白名单
CIDR 未做完整实现(避免复杂度)。
4. 日志格式必须标准
否则需要改 $1 $9。











