该项目是一个完整的服务器运维监控系统,可以通过一个完整的python+shell的项目,让小白也能理解python和shell的大致编写过程。
项目结构
bash
┌─────────────────────────────────────────────────────┐
│ 自动化服务器运维监控系统 │
├─────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ Shell 脚本层 │ │ Python 脚本层 │ │
│ │ │ │ │ │
│ │ • 系统信息采集 │───▶│ • 数据分析与存储 │ │
│ │ • 日志轮转 │ │ • 异常检测与告警 │ │
│ │ • 服务健康检查 │ │ • HTML 报告生成 │ │
│ │ • 备份管理 │ │ • REST API 数据接口 │ │
│ └──────────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
项目包含 4 个 Shell 脚本 和 4 个 Python 脚本,下面逐一展开。
1. 系统信息采集脚本 system_collector.sh
bash
#!/bin/bash
#===============================================================
# 脚本名称: system_collector.sh
# 功能描述: 采集服务器各项运行指标,输出为 JSON 格式供 Python 分析
# 作者: 运维团队
# 版本: 1.0
#===============================================================
# ============================================================
# 【规则1】Shebang 行(第一行)
# #!/bin/bash 告诉系统用 /bin/bash 解释器执行此脚本
# 这是 Shell 脚本的固定写法,必须是文件的第一行
# ============================================================
# ============================================================
# 【规则2】变量定义
# Shell 变量赋值时 = 两边不能有空格
# 使用 $变量名 或 ${变量名} 来引用变量
# ============================================================
LOG_DIR="/var/log/sysmonitor" # 日志存储目录
OUTPUT_DIR="/tmp/sysmonitor/data" # 数据输出目录
TIMESTAMP=$(date +"%Y-%m-%d_%H:%M:%S") # 当前时间戳
OUTPUT_FILE="${OUTPUT_DIR}/sysinfo_${TIMESTAMP}.json" # 输出文件路径
HOSTNAME=$(hostname) # 获取主机名
# ============================================================
# 【规则3】函数定义
# 格式: function 函数名 { ... } 或 函数名() { ... }
# 函数内用 return 返回整数状态码,用 echo 返回字符串结果
# ============================================================
# --- 函数:创建必要的目录 ---
init_environment() {
# ============================================================
# 【规则4】条件判断
# [ -d "$LOG_DIR" ] 是 test 命令的简写,-d 判断目录是否存在
# if 结构: if [条件]; then ... elif [条件]; then ... else ... fi
# 注意: [ 后面和 ] 前面必须有空格
# ============================================================
if [ ! -d "$LOG_DIR" ]; then
mkdir -p "$LOG_DIR"
# -p 参数:递归创建目录,父目录不存在时一并创建,已存在时不报错
echo "[INFO] 创建日志目录: $LOG_DIR"
fi
if [ ! -d "$OUTPUT_DIR" ]; then
mkdir -p "$OUTPUT_DIR"
echo "[INFO] 创建数据目录: $OUTPUT_DIR"
fi
}
# --- 函数:获取 CPU 使用率 ---
get_cpu_usage() {
# ============================================================
# 【规则5】命令替换
# $(command) 会执行 command 并将其输出作为值赋给变量
# 这是 Shell 中最常用的数据获取方式
# ============================================================
# top 命令的 -bn1 参数:批量模式运行1次(非交互)
# grep 过滤含 "Cpu" 的行
# awk 提取第2列(用户态CPU使用率)
# sed 去掉小数点后的多余字符
local cpu_usage
cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | sed 's/%//')
# ============================================================
# 【规则6】local 关键字
# local 声明的变量只在函数内部有效,避免污染全局命名空间
# 这是良好的 Shell 编程习惯
# ============================================================
echo "$cpu_usage"
}
# --- 函数:获取内存使用情况 ---
get_memory_info() {
local mem_total mem_used mem_free mem_usage_percent
# ============================================================
# 【规则7】管道与文本处理三剑客:grep / awk / sed
# free -m: 以 MB 为单位显示内存信息
# grep Mem: 过滤出内存汇总行
# awk '{print $N}': 提取第 N 列
# ============================================================
mem_total=$(free -m | grep "Mem:" | awk '{print $2}')
mem_used=$(free -m | grep "Mem:" | awk '{print $3}')
mem_free=$(free -m | grep "Mem:" | awk '{print $4}')
# ============================================================
# 【规则8】算术运算
# Shell 原生不支持浮点运算
# 方法1: $(( )) 只能做整数运算
# 方法2: bc 命令可以做浮点运算(需要安装)
# 方法3: awk 可以做浮点运算
# 这里用 awk 计算百分比
# ============================================================
mem_usage_percent=$(awk "BEGIN {printf \"%.1f\", ($mem_used/$mem_total)*100}")
# 返回多个值时用空格分隔,调用方用数组接收
echo "$mem_total $mem_used $mem_free $mem_usage_percent"
}
# --- 函数:获取磁盘使用情况 ---
get_disk_info() {
# ============================================================
# 【规则9】while read 循环
# 常用于逐行读取命令输出或文件内容
# 格式: command | while read 变量1 变量2 ...; do ... done
# ============================================================
local disk_json="["
local first=true
df -h | grep -v "^Filesystem" | while read filesystem size used avail percent mountpoint; do
# 跳过 tmpfs 等虚拟文件系统
if [[ "$filesystem" == /dev/* ]]; then
if [ "$first" = true ]; then
first=false
else
disk_json+=","
fi
# 去掉百分号
percent_clean=${percent%\%}
disk_json+="{\"device\":\"$filesystem\",\"size\":\"$size\",\"used\":\"$used\",\"avail\":\"$avail\",\"percent\":$percent_clean,\"mount\":\"$mountpoint\"}"
fi
echo "$disk_json"
done | tail -1
# ============================================================
# tail -1 取最后一行,因为 while 循环中每行都 echo 了中间结果
# ============================================================
}
# --- 函数:获取网络流量 ---
get_network_info() {
local interface="eth0"
# ============================================================
# 【规则10】从 /proc 文件系统读取内核数据
# Linux 的 /proc 是虚拟文件系统,直接反映内核状态
# /proc/net/dev 包含网络接口的收发字节数
# ============================================================
if [ -f "/proc/net/dev" ]; then
local rx_bytes tx_bytes
rx_bytes=$(cat /proc/net/dev | grep "$interface" | awk '{print $2}')
tx_bytes=$(cat /proc/net/dev | grep "$interface" | awk '{print $10}')
# 将字节转换为 MB(整数除法)
local rx_mb=$((rx_bytes / 1024 / 1024))
local tx_mb=$((tx_bytes / 1024 / 1024))
echo "{\"interface\":\"$interface\",\"rx_mb\":$rx_mb,\"tx_mb\":$tx_mb}"
else
echo "{\"interface\":\"unknown\",\"rx_mb\":0,\"tx_mb\":0}"
fi
}
# --- 函数:获取系统负载 ---
get_load_average() {
# ============================================================
# 【规则11】cut 命令
# cut -d 指定分隔符,-f 指定取第几个字段
# uptime 输出示例: 10:30:00 up 5 days, load average: 0.15, 0.10, 0.05
# ============================================================
local load1 load5 load15
load1=$(uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $1}' | tr -d ' ')
load5=$(uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $2}' | tr -d ' ')
load15=$(uptime | awk -F'load average:' '{print $2}' | awk -F',' '{print $3}' | tr -d ' ')
echo "{\"load1\":$load1,\"load5\":$load5,\"load15\":$load15}"
}
# --- 函数:获取运行中的进程数 ---
get_process_count() {
# ============================================================
# 【规则12】wc 命令
# wc -l 统计行数
# ps aux 列出所有进程
# ============================================================
local total_procs running_procs
total_procs=$(ps aux | wc -l)
running_procs=$(ps aux | awk '$8 ~ /R/ {count++} END {print count+0}')
echo "{\"total\":$total_procs,\"running\":$running_procs}"
}
# ============================================================
# 【规则13】主流程控制
# 脚本的主体逻辑通常放在最后,用 main 函数组织
# ============================================================
main() {
echo "=========================================="
echo " 系统信息采集 - $TIMESTAMP"
echo "=========================================="
# 初始化环境
init_environment
# 采集各项指标
echo "[INFO] 正在采集 CPU 信息..."
local cpu=$(get_cpu_usage)
echo "[INFO] 正在采集内存信息..."
local mem_info=$(get_memory_info)
local mem_total=$(echo "$mem_info" | awk '{print $1}')
local mem_used=$(echo "$mem_info" | awk '{print $2}')
local mem_free=$(echo "$mem_info" | awk '{print $3}')
local mem_percent=$(echo "$mem_info" | awk '{print $4}')
echo "[INFO] 正在采集磁盘信息..."
local disk_info=$(get_disk_info)
echo "[INFO] 正在采集网络信息..."
local net_info=$(get_network_info)
echo "[INFO] 正在采集系统负载..."
local load_info=$(get_load_average)
echo "[INFO] 正在采集进程信息..."
local proc_info=$(get_process_count)
# ============================================================
# 【规则14】Here Document(此处文档)
# cat << EOF ... EOF 可以将多行文本输出
# 常用于生成配置文件、JSON、HTML 等结构化文本
# ============================================================
cat > "$OUTPUT_FILE" << EOF
{
"timestamp": "$TIMESTAMP",
"hostname": "$HOSTNAME",
"cpu": {
"usage_percent": $cpu
},
"memory": {
"total_mb": $mem_total,
"used_mb": $mem_used,
"free_mb": $mem_free,
"usage_percent": $mem_percent
},
"disk": $disk_info,
"network": $net_info,
"load_average": $load_info,
"processes": $proc_info
}
EOF
echo "[SUCCESS] 数据已保存到: $OUTPUT_FILE"
echo "[INFO] 数据内容预览:"
cat "$OUTPUT_FILE"
# ============================================================
# 【规则15】退出状态码
# exit 0 表示成功,非零表示失败
# 其他脚本可通过 $? 获取上一个命令的退出状态
# ============================================================
exit 0
}
# 执行 main 函数
main "$@"
# ============================================================
# 【规则16】"$@" 传递所有命令行参数
# 这样 main 函数内部可以访问脚本的命令行参数
# ============================================================
2. 日志轮转脚本 log_rotator.sh
bash
#!/bin/bash
#===============================================================
# 脚本名称: log_rotator.sh
# 功能描述: 日志轮转管理 - 压缩旧日志、清理过期日志、统计日志大小
#===============================================================
# ============================================================
# 【规则17】getopts 命令行参数解析
# getopts 是 Shell 内置的参数解析工具
# "d:l:m:" 中,字母是选项名,冒号表示该选项需要参数值
# OPTARG 存储选项的参数值,OPTIND 存储下一个参数的索引
# ============================================================
LOG_BASE_DIR="/var/log/sysmonitor"
MAX_AGE_DAYS=30
MAX_SIZE_MB=100
COMPRESS=true
usage() {
# ============================================================
# 【规则18】函数内使用 cat 输出帮助信息
# 这是 Shell 脚本中输出多行帮助文本的标准做法
# ============================================================
cat << USAGE_EOF
用法: $0 [选项]
选项:
-d <目录> 日志基础目录 (默认: $LOG_BASE_DIR)
-l <天数> 日志最大保留天数 (默认: $MAX_AGE_DAYS)
-m <大小MB> 单个日志最大MB (默认: $MAX_SIZE_MB)
-h 显示帮助信息
示例:
$0 -d /var/log/myapp -l 7 -m 50
USAGE_EOF
exit 1
}
# 解析命令行参数
while getopts "d:l:m:h" opt; do
case $opt in
d) LOG_BASE_DIR="$OPTARG" ;;
l) MAX_AGE_DAYS="$OPTARG" ;;
m) MAX_SIZE_MB="$OPTARG" ;;
h) usage ;;
# ============================================================
# 【规则19】case 语句
# 类似其他语言的 switch-case
# *) 是通配符,匹配所有未列出的情况(相当于 default)
# ;; 相当于 break
# ============================================================
\?) echo "无效选项: -$OPTARG" >&2; usage ;;
# ============================================================
# 【规则20】标准错误输出
# >&2 将输出重定向到 stderr(标准错误)
# 错误信息应输出到 stderr,正常信息输出到 stdout
# ============================================================
esac
done
# ============================================================
# 【规则21】for 循环遍历文件
# 格式: for 变量 in 列表; do ... done
# find 命令用于递归搜索文件
# ============================================================
# --- 功能1:压缩超过指定大小的日志文件 ---
compress_large_logs() {
echo "[INFO] === 开始压缩大日志文件 ==="
local count=0
# find 命令详解:
# $LOG_BASE_DIR - 搜索的起始目录
# -name "*.log" - 文件名匹配 *.log
# -size +${MAX_SIZE_MB}M - 文件大小超过指定 MB
# -type f - 只匹配普通文件
find "$LOG_BASE_DIR" -name "*.log" -size +${MAX_SIZE_MB}M -type f | while read -r logfile; do
# ============================================================
# 【规则22】read -r 参数
# -r 防止反斜杠转义,是读取文件路径时的标准写法
# ============================================================
local filesize
filesize=$(du -h "$logfile" | awk '{print $1}')
echo "[INFO] 压缩: $logfile (大小: $filesize)"
if [ "$COMPRESS" = true ]; then
gzip "$logfile"
# gzip 会原地压缩文件,生成 .gz 文件
count=$((count + 1))
fi
done
echo "[INFO] 共压缩 $count 个日志文件"
}
# --- 功能2:删除超过保留天数的旧日志 ---
cleanup_old_logs() {
echo "[INFO] === 开始清理过期日志 (>${MAX_AGE_DAYS}天) ==="
local count=0
# -mtime +N 表示修改时间在 N 天前的文件
find "$LOG_BASE_DIR" \( -name "*.log" -o -name "*.log.gz" \) -mtime +${MAX_AGE_DAYS} -type f | while read -r oldfile; do
# ============================================================
# 【规则23】find 中的逻辑运算
# \( ... \) 分组(注意反斜杠转义)
# -o 表示 OR(或)
# -a 表示 AND(与,默认就是 AND)
# ============================================================
echo "[INFO] 删除过期日志: $oldfile"
rm -f "$oldfile"
count=$((count + 1))
done
echo "[INFO] 共清理 $count 个过期日志文件"
}
# --- 功能3:统计日志目录空间占用 ---
report_disk_usage() {
echo "[INFO] === 日志目录空间统计 ==="
# ============================================================
# 【规则24】du 和 df 命令
# du -sh: 显示目录总大小(人类可读格式)
# df -h: 显示文件系统磁盘使用情况
# ============================================================
local total_size
total_size=$(du -sh "$LOG_BASE_DIR" 2>/dev/null | awk '{print $1}')
# 2>/dev/null 将 stderr 重定向到 /dev/null(丢弃错误信息)
local file_count
file_count=$(find "$LOG_BASE_DIR" -type f 2>/dev/null | wc -l)
echo "┌──────────────────────────────────┐"
echo "│ 日志目录: $LOG_BASE_DIR"
echo "│ 总大小: $total_size"
echo "│ 文件数: $file_count"
echo "│ 保留策略: ${MAX_AGE_DAYS} 天"
echo "└──────────────────────────────────┘"
}
# --- 主流程 ---
main() {
echo "=========================================="
echo " 日志轮转管理 - $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
# ============================================================
# 【规则25】目录存在性校验
# 在执行操作前检查目录是否存在,避免误操作
# ============================================================
if [ ! -d "$LOG_BASE_DIR" ]; then
echo "[ERROR] 日志目录不存在: $LOG_BASE_DIR"
exit 1
fi
compress_large_logs
cleanup_old_logs
report_disk_usage
echo "[SUCCESS] 日志轮转完成"
}
main "$@"
3. 服务健康检查脚本 health_checker.sh
bash
#!/bin/bash
#===============================================================
# 脚本名称: health_checker.sh
# 功能描述: 检查各类服务是否正常运行,异常时触发告警
#===============================================================
# ============================================================
# 【规则26】数组定义与使用
# 格式: 数组名=(元素1 元素2 元素3)
# 引用: ${数组名[索引]} 或 ${数组名[@]}(所有元素)
# 长度: ${#数组名[@]}
# ============================================================
declare -a SERVICES=("nginx" "mysql" "redis-server" "sshd")
declare -a PORTS=(80 3306 6379 22)
ALERT_EMAIL="admin@example.com"
CHECK_INTERVAL=5 # 检查间隔(秒)
# ============================================================
# 【规则27】关联数组(Bash 4.0+)
# 类似其他语言的字典/Map,用字符串作为键
# 格式: declare -A 数组名
# ============================================================
declare -A SERVICE_STATUS
# --- 函数:检查端口是否监听 ---
check_port() {
local port=$1
local service_name=$2
# ============================================================
# 【规则28】ss/netstat 网络工具
# ss -tlnp: 显示 TCP 监听端口及对应进程
# -t: TCP, -l: 监听状态, -n: 数字格式, -p: 显示进程
# grep -q: 静默模式,只返回退出状态,不输出内容
# ============================================================
if ss -tlnp | grep -q ":${port} "; then
SERVICE_STATUS[$service_name]="RUNNING"
echo "[✓] $service_name (端口 $port) - 运行中"
return 0
# ============================================================
# 【规则29】return 与 exit 的区别
# return: 从函数返回,只退出函数
# exit: 退出整个脚本
# ============================================================
else
SERVICE_STATUS[$service_name]="STOPPED"
echo "[✗] $service_name (端口 $port) - 未运行"
return 1
fi
}
# --- 函数:检查进程是否存在 ---
check_process() {
local process_name=$1
# ============================================================
# 【规则30】pgrep 命令
# pgrep 按名称查找进程,返回进程ID
# -x 精确匹配进程名
# ============================================================
if pgrep -x "$process_name" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
# --- 函数:检查磁盘空间 ---
check_disk_space() {
local threshold=90 # 告警阈值(百分比)
echo "[INFO] === 磁盘空间检查 (阈值: ${threshold}%) ==="
# ============================================================
# 【规则31】while read 结合 IFS
# IFS (Internal Field Separator) 控制 read 如何分割字段
# 临时修改 IFS 不影响全局
# ============================================================
df -h | awk 'NR>1 {print $5, $6}' | while read usage mount; do
local percent=${usage%\%}
# ============================================================
# 【规则32】整数比较运算符
# -eq 等于 -ne 不等于
# -gt 大于 -lt 小于
# -ge 大于等于 -le 小于等于
# 注意:Shell 原生只支持整数比较,不支持浮点数
# ============================================================
if [ "$percent" -ge "$threshold" ]; then
echo "[WARNING] 磁盘 $mount 使用率 ${percent}% 超过阈值 ${threshold}%!"
else
echo "[OK] 磁盘 $mount 使用率 ${percent}%"
fi
done
}
# --- 函数:检查系统负载 ---
check_system_load() {
local cpu_cores
cpu_cores=$(nproc)
# nproc 返回 CPU 核心数
local load1
load1=$(cat /proc/loadavg | awk '{print $1}')
# ============================================================
# 【规则33】awk 进行浮点数比较
# Shell 的 [ ] 不支持浮点数比较
# 可以用 awk 来做浮点数判断
# ============================================================
local is_high
is_high=$(awk "BEGIN {print ($load1 > $cpu_cores * 2) ? 1 : 0}")
if [ "$is_high" -eq 1 ]; then
echo "[WARNING] 系统负载过高! 负载: $load1, CPU核心: $cpu_cores"
else
echo "[OK] 系统负载正常. 负载: $load1, CPU核心: $cpu_cores"
fi
}
# --- 函数:发送告警通知 ---
send_alert() {
local message=$1
# ============================================================
# 【规则34】邮件发送(需要系统配置 mail 命令)
# echo "内容" | mail -s "主题" 收件人
# ============================================================
if command -v mail > /dev/null 2>&1; then
echo "$message" | mail -s "[告警] 服务器健康检查异常" "$ALERT_EMAIL"
echo "[INFO] 告警邮件已发送至: $ALERT_EMAIL"
else
echo "[WARNING] mail 命令不可用,告警信息仅输出到日志"
echo "$(date '+%Y-%m-%d %H:%M:%S') [ALERT] $message" >> /var/log/sysmonitor/alerts.log
fi
}
# --- 主流程 ---
main() {
echo "=========================================="
echo " 服务健康检查 - $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
local has_error=false
# ============================================================
# 【规则35】C 风格 for 循环
# 格式: for (( 初始化; 条件; 递增 )); do ... done
# 这是 Bash 特有的语法,类似 C 语言
# ============================================================
echo ""
echo "[INFO] === 服务端口检查 ==="
for (( i=0; i<${#SERVICES[@]}; i++ )); do
if ! check_port "${PORTS[$i]}" "${SERVICES[$i]}"; then
has_error=true
fi
done
echo ""
check_disk_space
echo ""
check_system_load
echo ""
echo "=========================================="
echo " 检查结果汇总"
echo "=========================================="
# ============================================================
# 【规则36】遍历关联数组
# ${!数组名[@]} 获取关联数组的所有键
# ============================================================
for service in "${!SERVICE_STATUS[@]}"; do
echo " $service: ${SERVICE_STATUS[$service]}"
done
if [ "$has_error" = true ]; then
send_alert "部分服务异常,请检查服务器 $HOSTNAME"
exit 1
else
echo "[SUCCESS] 所有服务运行正常"
exit 0
fi
}
main "$@"
4. 自动备份脚本 backup_manager.sh
bash
#!/bin/bash
#===============================================================
# 脚本名称: backup_manager.sh
# 功能描述: 数据库和配置文件的自动备份管理
#===============================================================
# ============================================================
# 【规则37】source / . 命令
# source 在当前 Shell 环境中执行另一个脚本
# 可以加载外部配置文件中的变量
# ============================================================
CONFIG_FILE="/etc/sysmonitor/backup.conf"
# 如果配置文件存在则加载
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
# ============================================================
# 【规则38】变量默认值
# ${变量:-默认值} 如果变量未设置或为空,则使用默认值
# 这是设置配置默认值的优雅方式
# ============================================================
BACKUP_DIR="${BACKUP_DIR:-/backup/sysmonitor}"
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-3306}"
DB_USER="${DB_USER:-root}"
DB_PASS="${DB_PASS:-}"
RETENTION_DAYS="${RETENTION_DAYS:-7}"
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
# --- 函数:备份 MySQL 数据库 ---
backup_database() {
local db_name=$1
local dump_file="${BACKUP_DIR}/db_${db_name}_${BACKUP_DATE}.sql"
echo "[INFO] 开始备份数据库: $db_name"
# ============================================================
# 【规则39】mysqldump 数据库备份
# --single-transaction: 一致性备份(不锁表)
# --routines: 导出存储过程
# --triggers: 导出触发器
# ============================================================
if [ -n "$DB_PASS" ]; then
mysqldump -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" \
--single-transaction --routines --triggers \
"$db_name" > "$dump_file" 2>/dev/null
else
mysqldump -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" \
--single-transaction --routines --triggers \
"$db_name" > "$dump_file" 2>/dev/null
fi
# ============================================================
# 【规则40】反斜杠续行
# 当命令太长时,用 \ 在行尾续行
# 注意: \ 后面不能有任何字符(包括空格)
# ============================================================
# ============================================================
# 【规则41】检查命令执行结果
# $? 是上一个命令的退出状态码
# 0 = 成功,非0 = 失败
# ============================================================
if [ $? -eq 0 ] && [ -s "$dump_file" ]; then
# -s 检查文件是否存在且大小大于0
local filesize
filesize=$(du -h "$dump_file" | awk '{print $1}')
echo "[SUCCESS] 数据库备份完成: $dump_file (大小: $filesize)"
# 压缩备份文件
gzip "$dump_file"
echo "[INFO] 已压缩: ${dump_file}.gz"
else
echo "[ERROR] 数据库备份失败: $db_name"
return 1
fi
}
# --- 函数:备份配置文件 ---
backup_configs() {
local config_backup="${BACKUP_DIR}/configs_${BACKUP_DATE}.tar.gz"
echo "[INFO] 开始备份配置文件..."
# ============================================================
# 【规则42】tar 归档命令
# -c: 创建归档 -z: gzip压缩 -f: 指定文件名
# -C: 切换到指定目录再打包(避免打包完整路径)
# ============================================================
tar -czf "$config_backup" \
-C / etc/nginx/nginx.conf \
-C / etc/mysql/my.cnf \
-C / etc/ssh/sshd_config \
2>/dev/null
if [ $? -eq 0 ]; then
echo "[SUCCESS] 配置文件备份完成: $config_backup"
else
echo "[WARNING] 部分配置文件备份失败(可能文件不存在)"
fi
}
# --- 函数:清理旧备份 ---
cleanup_old_backups() {
echo "[INFO] 清理 ${RETENTION_DAYS} 天前的旧备份..."
local deleted_count
deleted_count=$(find "$BACKUP_DIR" -type f \
\( -name "*.sql.gz" -o -name "*.tar.gz" \) \
-mtime +${RETENTION_DAYS} \
-delete -print | wc -l)
# ============================================================
# 【规则43】find -delete 参数
# -delete 直接删除匹配的文件
# -print 在删除前打印文件名(用于统计)
# ============================================================
echo "[INFO] 已清理 $deleted_count 个旧备份文件"
}
# --- 函数:生成备份报告 ---
generate_report() {
local report_file="${BACKUP_DIR}/backup_report_${BACKUP_DATE}.txt"
cat > "$report_file" << REPORT_EOF
============================================
备份报告
时间: $(date '+%Y-%m-%d %H:%M:%S')
主机: $(hostname)
============================================
备份目录: $BACKUP_DIR
保留策略: ${RETENTION_DAYS} 天
当前备份文件列表:
$(ls -lh "$BACKUP_DIR"/*.gz 2>/dev/null || echo " 无备份文件")
磁盘使用情况:
$(df -h "$BACKUP_DIR" | tail -1)
============================================
REPORT_EOF
echo "[INFO] 备份报告: $report_file"
}
# --- 主流程 ---
main() {
echo "=========================================="
echo " 自动备份管理 - $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="
mkdir -p "$BACKUP_DIR"
# 备份数据库(示例数据库名)
for db in myapp myapp_logs; do
backup_database "$db"
done
# 备份配置文件
backup_configs
# 清理旧备份
cleanup_old_backups
# 生成报告
generate_report
echo "[SUCCESS] 备份任务全部完成"
}
main "$@"
5. 数据分析引擎(python) analyzer.py
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
===============================================================
模块名称: analyzer.py
功能描述: 读取 Shell 脚本采集的 JSON 数据,进行统计分析和异常检测
===============================================================
【Python 规则1】文件头声明
- #!/usr/bin/env python3 指定 Python3 解释器
- # -*- coding: utf-8 -*- 声明文件编码(Python3 默认 UTF-8,可省略)
- 三引号 """ 包裹的是模块文档字符串(docstring)
"""
# ============================================================
# 【Python 规则2】导入模块
# 标准库优先,第三方库其次,本项目模块最后
# 每组之间空一行
# ============================================================
import json # JSON 解析
import os # 操作系统接口
import glob # 文件路径匹配
import statistics # 统计计算
from datetime import datetime # 日期时间
from collections import defaultdict # 默认字典
from typing import List, Dict, Any, Optional, Tuple # 类型提示
from dataclasses import dataclass, field # 数据类
# ============================================================
# 【Python 规则3】dataclass 数据类(Python 3.7+)
# @dataclass 装饰器自动生成 __init__, __repr__, __eq__ 等方法
# 用于定义纯数据容器,比手写 class 简洁得多
# ============================================================
@dataclass
class MetricThreshold:
"""指标阈值配置"""
cpu_warning: float = 80.0 # CPU 告警阈值(百分比)
cpu_critical: float = 95.0 # CPU 严重告警阈值
memory_warning: float = 80.0 # 内存告警阈值
memory_critical: float = 95.0 # 内存严重告警阈值
disk_warning: float = 85.0 # 磁盘告警阈值
disk_critical: float = 95.0 # 磁盘严重告警阈值
load_factor: float = 2.0 # 负载因子(相对于 CPU 核心数)
@dataclass
class Alert:
"""告警信息"""
level: str # 告警级别: WARNING / CRITICAL
metric: str # 指标名称
value: float # 当前值
threshold: float # 阈值
message: str # 告警描述
timestamp: str = "" # 告警时间
def __post_init__(self):
"""
【Python 规则4】__post_init__ 方法
dataclass 初始化后自动调用,用于设置依赖字段或验证
"""
if not self.timestamp:
self.timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
class SystemAnalyzer:
"""
系统数据分析器
【Python 规则5】类的定义
- class 类名(继承的父类):
- 第一个参数永远是 self(代表实例本身)
- __init__ 是构造方法
- 实例属性用 self.xxx 定义
"""
def __init__(self, data_dir: str, threshold: Optional[MetricThreshold] = None):
"""
初始化分析器
参数:
data_dir: 数据文件目录
threshold: 告警阈值配置,为 None 时使用默认值
【Python 规则6】类型提示(Type Hints)
- str: 字符串
- Optional[X]: 可以是 X 或 None
- List[X]: X 的列表
- Dict[K, V]: 键为 K、值为 V 的字典
- Tuple[A, B]: 元组
类型提示不影响运行,但提高代码可读性和 IDE 支持
"""
self.data_dir = data_dir
self.threshold = threshold or MetricThreshold()
# ============================================================
# 【Python 规则7】三元表达式
# 值1 if 条件 else 值2
# 等价于其他语言的 条件 ? 值1 : 值2
# ============================================================
# 存储所有采集数据
self.records: List[Dict[str, Any]] = []
# 存储告警信息
self.alerts: List[Alert] = []
# 存储统计摘要
self.summary: Dict[str, Any] = {}
def load_data(self) -> int:
"""
从目录加载所有 JSON 数据文件
返回:
成功加载的文件数量
【Python 规则8】文档字符串(Docstring)
- 用三引号包裹,放在函数/类定义下方
- 描述功能、参数、返回值
- 是 Python 的标准文档规范
"""
# ============================================================
# 【Python 规则9】glob 文件匹配
# glob.glob() 返回匹配模式的所有文件路径列表
# * 匹配任意字符,** 递归匹配子目录
# ============================================================
pattern = os.path.join(self.data_dir, "sysinfo_*.json")
files = sorted(glob.glob(pattern))
for filepath in files:
try:
# ============================================================
# 【Python 规则10】with 语句(上下文管理器)
# with open(...) as f: 自动管理资源
# 文件在 with 块结束时自动关闭,即使发生异常
# 这是 Python 中操作文件的标准方式
# ============================================================
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
self.records.append(data)
except (json.JSONDecodeError, IOError) as e:
# ============================================================
# 【Python 规则11】异常处理
# try...except...else...finally
# except 可以捕获特定异常类型
# 多个 except 可以并列
# else: 没有异常时执行
# finally: 无论是否异常都执行
# ============================================================
print(f"[WARNING] 无法解析文件 {filepath}: {e}")
print(f"[INFO] 成功加载 {len(self.records)} 条数据记录")
return len(self.records)
def analyze_cpu(self) -> Dict[str, Any]:
"""
分析 CPU 使用率
返回:
包含平均值、最大值、最小值、标准差等的字典
【Python 规则12】字典操作
- 创建: d = {"key": value}
- 访问: d["key"] 或 d.get("key", default)
- 添加: d["new_key"] = value
- 推导式: {k: v for k, v in items}
"""
cpu_values = [
r["cpu"]["usage_percent"]
for r in self.records
if "cpu" in r and "usage_percent" in r["cpu"]
]
# ============================================================
# 【Python 规则13】列表推导式(List Comprehension)
# [表达式 for 变量 in 可迭代对象 if 条件]
# 这是 Python 最强大的语法特性之一
# 等价于:
# cpu_values = []
# for r in self.records:
# if "cpu" in r and "usage_percent" in r["cpu"]:
# cpu_values.append(r["cpu"]["usage_percent"])
# ============================================================
if not cpu_values:
return {"error": "无 CPU 数据"}
result = {
"avg": round(statistics.mean(cpu_values), 2),
"max": max(cpu_values),
"min": min(cpu_values),
"stddev": round(statistics.stdev(cpu_values), 2) if len(cpu_values) > 1 else 0,
"median": statistics.median(cpu_values),
"samples": len(cpu_values),
}
# ============================================================
# 【Python 规则14】round() 内置函数
# round(number, ndigits) 四舍五入到指定小数位
# ============================================================
# 检查是否触发告警
if result["max"] >= self.threshold.cpu_critical:
self.alerts.append(Alert(
level="CRITICAL",
metric="cpu_usage",
value=result["max"],
threshold=self.threshold.cpu_critical,
message=f"CPU 使用率峰值 {result['max']}% 超过严重阈值 {self.threshold.cpu_critical}%"
))
elif result["max"] >= self.threshold.cpu_warning:
self.alerts.append(Alert(
level="WARNING",
metric="cpu_usage",
value=result["max"],
threshold=self.threshold.cpu_warning,
message=f"CPU 使用率峰值 {result['max']}% 超过告警阈值 {self.threshold.cpu_warning}%"
))
return result
def analyze_memory(self) -> Dict[str, Any]:
"""分析内存使用情况"""
mem_percentages = [
r["memory"]["usage_percent"]
for r in self.records
if "memory" in r and "usage_percent" in r["memory"]
]
if not mem_percentages:
return {"error": "无内存数据"}
result = {
"avg": round(statistics.mean(mem_percentages), 2),
"max": max(mem_percentages),
"min": min(mem_percentages),
"trend": self._calculate_trend(mem_percentages),
}
if result["max"] >= self.threshold.memory_critical:
self.alerts.append(Alert(
level="CRITICAL",
metric="memory_usage",
value=result["max"],
threshold=self.threshold.memory_critical,
message=f"内存使用率峰值 {result['max']}% 超过严重阈值"
))
return result
def _calculate_trend(self, values: List[float]) -> str:
"""
计算指标趋势方向
【Python 规则15】以 _ 开头的方法表示"私有"方法
Python 没有严格的私有机制,但 _ 前缀是约定俗成的
表示该方法仅供类内部使用
"""
if len(values) < 2:
return "insufficient_data"
# 将数据分为前后两半,比较平均值
mid = len(values) // 2
first_half_avg = statistics.mean(values[:mid])
second_half_avg = statistics.mean(values[mid:])
# ============================================================
# 【Python 规则16】切片操作
# list[start:end:step]
# values[:mid] 从开头到 mid(不含)
# values[mid:] 从 mid 到末尾
# values[::2] 每隔一个取一个
# values[::-1] 反转列表
# ============================================================
diff = second_half_avg - first_half_avg
if diff > 5:
return "rising" # 上升趋势
elif diff < -5:
return "falling" # 下降趋势
else:
return "stable" # 稳定
def analyze_disk(self) -> Dict[str, Any]:
"""分析磁盘使用情况"""
disk_alerts = []
for record in self.records:
if "disk" not in record:
continue
# ============================================================
# 【Python 规则17】遍历列表中的字典
# record["disk"] 可能是一个列表,每个元素是一个字典
# ============================================================
for disk in record["disk"]:
usage = disk.get("percent", 0)
mount = disk.get("mount", "unknown")
if usage >= self.threshold.disk_critical:
disk_alerts.append({
"level": "CRITICAL",
"mount": mount,
"usage": usage
})
elif usage >= self.threshold.disk_warning:
disk_alerts.append({
"level": "WARNING",
"mount": mount,
"usage": usage
})
return {
"alerts": disk_alerts,
"total_checks": len(self.records),
}
def generate_summary(self) -> Dict[str, Any]:
"""
生成综合分析报告
【Python 规则18】方法调用链
依次调用各分析模块,汇总结果
"""
self.summary = {
"report_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"total_records": len(self.records),
"cpu": self.analyze_cpu(),
"memory": self.analyze_memory(),
"disk": self.analyze_disk(),
"alerts": [
{
"level": a.level,
"metric": a.metric,
"message": a.message,
"timestamp": a.timestamp
}
for a in self.alerts
],
# ============================================================
# 【Python 规则19】字典推导式
# {key_expr: value_expr for item in iterable}
# 类似于列表推导式,但生成字典
# ============================================================
"alert_summary": {
level: len([a for a in self.alerts if a.level == level])
for level in ["WARNING", "CRITICAL"]
}
}
return self.summary
def export_json(self, output_path: str) -> None:
"""
将分析结果导出为 JSON 文件
【Python 规则20】json.dump() 与 json.dumps()
- json.dump(obj, file): 写入文件对象
- json.dumps(obj): 返回字符串
- indent: 格式化缩进
- ensure_ascii=False: 允许输出中文
"""
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(self.summary, f, indent=4, ensure_ascii=False)
print(f"[SUCCESS] 分析报告已导出: {output_path}")
# ============================================================
# 【Python 规则21】__name__ == "__main__" 惯用法
# 当脚本被直接运行时,__name__ 等于 "__main__"
# 当脚本被 import 时,__name__ 等于模块名
# 这样可以防止被导入时自动执行代码
# ============================================================
if __name__ == "__main__":
import sys
# ============================================================
# 【Python 规则22】命令行参数
# sys.argv 是一个列表,包含所有命令行参数
# sys.argv[0] 是脚本名,sys.argv[1:] 是参数
# ============================================================
data_directory = sys.argv[1] if len(sys.argv) > 1 else "/tmp/sysmonitor/data"
output_file = sys.argv[2] if len(sys.argv) > 2 else "/tmp/sysmonitor/analysis_report.json"
# 创建分析器实例
analyzer = SystemAnalyzer(data_directory)
# 加载数据
if analyzer.load_data() == 0:
print("[ERROR] 没有可分析的数据")
sys.exit(1)
# 生成报告
summary = analyzer.generate_summary()
# 打印摘要
print("\n" + "=" * 50)
print(" 系统分析摘要")
print("=" * 50)
print(f" 数据记录数: {summary['total_records']}")
if "avg" in summary.get("cpu", {}):
print(f" CPU 平均使用率: {summary['cpu']['avg']}%")
print(f" CPU 峰值: {summary['cpu']['max']}%")
if "avg" in summary.get("memory", {}):
print(f" 内存平均使用率: {summary['memory']['avg']}%")
print(f" 内存趋势: {summary['memory'].get('trend', 'N/A')}")
print(f" 告警总数: {len(summary.get('alerts', []))}")
for level, count in summary.get("alert_summary", {}).items():
print(f" {level}: {count} 条")
# 导出报告
analyzer.export_json(output_file)
6. 告警通知模块 alerter.py
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
告警通知模块 - 支持多种通知渠道
"""
import json
import smtplib
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from abc import ABC, abstractmethod
from typing import List, Dict
from dataclasses import dataclass
# ============================================================
# 【Python 规则23】logging 日志模块
# 比 print 更专业的日志方案
# 支持日志级别、格式化、输出到文件/控制台
# ============================================================
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
# ============================================================
# 【Python 规则24】日志级别
# DEBUG < INFO < WARNING < ERROR < CRITICAL
# 只有 >= 设置级别的日志才会被输出
# ============================================================
# ============================================================
# 【Python 规则25】抽象基类(ABC)
# from abc import ABC, abstractmethod
# 抽象类不能被实例化,子类必须实现所有 @abstractmethod
# 这定义了接口规范,类似 Java 的 interface
# ============================================================
class NotificationChannel(ABC):
"""通知渠道抽象基类"""
@abstractmethod
def send(self, subject: str, body: str) -> bool:
"""
发送通知
参数:
subject: 通知主题
body: 通知内容
返回:
是否发送成功
"""
pass
# ============================================================
# 【Python 规则26】pass 语句
# pass 是空操作,什么都不做
# 用作占位符,当语法上需要一条语句但逻辑上不需要时使用
# ============================================================
class EmailNotifier(NotificationChannel):
"""
邮件通知渠道
【Python 规则27】继承
class 子类(父类):
子类自动拥有父类的所有方法和属性
可以重写(override)父类的方法
"""
def __init__(self, smtp_host: str, smtp_port: int,
username: str, password: str, recipients: List[str]):
self.smtp_host = smtp_host
self.smtp_port = smtp_port
self.username = username
self.password = password
self.recipients = recipients
def send(self, subject: str, body: str) -> bool:
"""发送邮件通知"""
try:
# ============================================================
# 【Python 规则28】MIME 邮件构建
# MIMEMultipart: 创建多部分邮件
# MIMEText: 创建文本内容
# ============================================================
msg = MIMEMultipart()
msg['From'] = self.username
msg['To'] = ', '.join(self.recipients)
msg['Subject'] = subject
msg.attach(MIMEText(body, 'html', 'utf-8'))
# ============================================================
# 【Python 规则29】with 语句管理多个资源
# Python 3.10+ 支持: with (res1 as a, res2 as b):
# Python 3.9 及以下用嵌套 with
# ============================================================
with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
server.starttls() # 启用 TLS 加密
server.login(self.username, self.password)
server.send_message(msg)
logger.info(f"邮件发送成功,收件人: {self.recipients}")
return True
except Exception as e:
logger.error(f"邮件发送失败: {e}")
return False
class ConsoleNotifier(NotificationChannel):
"""控制台通知渠道(用于测试)"""
def send(self, subject: str, body: str) -> bool:
print("\n" + "=" * 60)
print(f"📧 通知主题: {subject}")
print("-" * 60)
print(body)
print("=" * 60 + "\n")
return True
class AlertManager:
"""
告警管理器
【Python 规则30】组合模式
AlertManager 持有一个 NotificationChannel 列表
通过组合(而非继承)来扩展功能
"""
def __init__(self):
self.channels: List[NotificationChannel] = []
def add_channel(self, channel: NotificationChannel) -> None:
"""添加通知渠道"""
self.channels.append(channel)
logger.info(f"已添加通知渠道: {channel.__class__.__name__}")
# ============================================================
# 【Python 规则31】__class__.__name__
# 获取对象的类名(字符串形式)
# ============================================================
def process_alerts(self, alerts: List[Dict]) -> int:
"""
处理告警列表
参数:
alerts: 告警字典列表
返回:
成功发送的通知数量
【Python 规则32】enumerate() 函数
enumerate(iterable, start=0) 同时获取索引和值
for i, item in enumerate(items): # i 是索引,item 是值
"""
if not alerts:
logger.info("没有需要处理的告警")
return 0
# 按级别分组
critical_alerts = [a for a in alerts if a['level'] == 'CRITICAL']
warning_alerts = [a for a in alerts if a['level'] == 'WARNING']
sent_count = 0
# 构建告警邮件内容
if critical_alerts:
subject = f"🔴 [严重告警] {len(critical_alerts)} 个严重问题需要立即处理"
body = self._build_alert_html(critical_alerts, "CRITICAL")
sent_count += self._broadcast(subject, body)
if warning_alerts:
subject = f"🟡 [警告] {len(warning_alerts)} 个警告需要注意"
body = self._build_alert_html(warning_alerts, "WARNING")
sent_count += self._broadcast(subject, body)
return sent_count
def _build_alert_html(self, alerts: List[Dict], level: str) -> str:
"""
构建 HTML 格式的告警内容
【Python 规则33】f-string 多行字符串
用三引号 + f 前缀,可以在多行字符串中使用变量插值
"""
color = "#dc3545" if level == "CRITICAL" else "#ffc107"
html = f"""
<html>
<body style="font-family: Arial, sans-serif;">
<h2 style="color: {color};">{'🔴 严重告警' if level == 'CRITICAL' else '🟡 警告通知'}</h2>
<table border="1" cellpadding="8" cellspacing="0" style="border-collapse: collapse;">
<tr style="background-color: #f8f9fa;">
<th>时间</th><th>级别</th><th>指标</th><th>描述</th>
</tr>
"""
for alert in alerts:
html += f"""
<tr>
<td>{alert.get('timestamp', 'N/A')}</td>
<td style="color: {color};">{alert['level']}</td>
<td>{alert.get('metric', 'N/A')}</td>
<td>{alert.get('message', 'N/A')}</td>
</tr>
"""
html += """
</table>
<p style="color: #666; font-size: 12px;">
此邮件由自动化监控系统发送,请勿直接回复。
</p>
</body>
</html>
"""
return html
def _broadcast(self, subject: str, body: str) -> int:
"""通过所有渠道广播通知"""
success_count = 0
for channel in self.channels:
if channel.send(subject, body):
success_count += 1
return success_count
if __name__ == "__main__":
# 测试告警系统
manager = AlertManager()
manager.add_channel(ConsoleNotifier())
test_alerts = [
{
"level": "CRITICAL",
"metric": "cpu_usage",
"message": "CPU 使用率 97.5% 超过严重阈值 95%",
"timestamp": "2024-01-15 10:30:00"
},
{
"level": "WARNING",
"metric": "memory_usage",
"message": "内存使用率 85.2% 超过告警阈值 80%",
"timestamp": "2024-01-15 10:30:00"
}
]
manager.process_alerts(test_alerts)
7. HTML 报告生成器 report_generator.py
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
报告生成模块 - 生成可视化的 HTML 监控报告
"""
import json
import os
from datetime import datetime
from typing import Dict, Any, List
from pathlib import Path
# ============================================================
# 【Python 规则34】pathlib 模块
# pathlib 是 Python 3 推荐的路径操作库
# 比 os.path 更直观、更面向对象
# Path("/a/b/c") 创建路径对象
# .parent 父目录
# .name 文件名
# .suffix 扩展名
# .exists() 是否存在
# .mkdir(parents=True, exist_ok=True) 创建目录
# ============================================================
class HTMLReportGenerator:
"""HTML 报告生成器"""
def __init__(self, title: str = "服务器监控报告"):
self.title = title
self.sections: List[str] = []
def add_header(self, summary: Dict[str, Any]) -> None:
"""添加报告头部"""
alert_count = len(summary.get("alerts", []))
alert_color = "#dc3545" if alert_count > 0 else "#28a745"
header = f"""
<div class="header">
<h1>🖥️ {self.title}</h1>
<p>报告生成时间: {summary.get('report_time', 'N/A')}</p>
<p>数据记录数: {summary.get('total_records', 0)}</p>
<div class="status-badge" style="background-color: {alert_color};">
{'⚠️ 存在告警 (' + str(alert_count) + ')' if alert_count > 0 else '✅ 一切正常'}
</div>
</div>
"""
self.sections.append(header)
def add_cpu_section(self, cpu_data: Dict[str, Any]) -> None:
"""添加 CPU 分析部分"""
if "error" in cpu_data:
self.sections.append(f'<div class="section"><h2>CPU 分析</h2><p>{cpu_data["error"]}</p></div>')
return
# ============================================================
# 【Python 规则35】字典的 get() 方法
# dict.get(key, default) 安全获取字典值
# 如果 key 不存在,返回 default 而不是抛出 KeyError
# ============================================================
avg = cpu_data.get("avg", 0)
max_val = cpu_data.get("max", 0)
min_val = cpu_data.get("min", 0)
stddev = cpu_data.get("stddev", 0)
# 根据使用率确定颜色
def get_color(value):
"""
【Python 规则36】嵌套函数
在函数内部定义的函数,只能在外层函数内使用
可以访问外层函数的变量(闭包)
"""
if value >= 90:
return "#dc3545" # 红色
elif value >= 70:
return "#ffc107" # 黄色
else:
return "#28a745" # 绿色
cpu_section = f"""
<div class="section">
<h2>📊 CPU 使用率分析</h2>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-value" style="color: {get_color(avg)}">{avg}%</div>
<div class="metric-label">平均使用率</div>
</div>
<div class="metric-card">
<div class="metric-value" style="color: {get_color(max_val)}">{max_val}%</div>
<div class="metric-label">峰值</div>
</div>
<div class="metric-card">
<div class="metric-value">{min_val}%</div>
<div class="metric-label">最低值</div>
</div>
<div class="metric-card">
<div class="metric-value">{stddev}</div>
<div class="metric-label">标准差</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {avg}%; background-color: {get_color(avg)};">
{avg}%
</div>
</div>
</div>
"""
self.sections.append(cpu_section)
def add_alerts_section(self, alerts: List[Dict]) -> None:
"""添加告警部分"""
if not alerts:
self.sections.append("""
<div class="section">
<h2>🔔 告警信息</h2>
<p class="no-alerts">🎉 没有告警,系统运行正常!</p>
</div>
""")
return
rows = ""
for alert in alerts:
level_class = "critical" if alert["level"] == "CRITICAL" else "warning"
rows += f"""
<tr class="{level_class}">
<td>{alert.get('timestamp', 'N/A')}</td>
<td><span class="badge {level_class}">{alert['level']}</span></td>
<td>{alert.get('metric', 'N/A')}</td>
<td>{alert.get('message', 'N/A')}</td>
</tr>
"""
alerts_section = f"""
<div class="section">
<h2>🔔 告警信息 ({len(alerts)} 条)</h2>
<table class="alert-table">
<thead>
<tr>
<th>时间</th><th>级别</th><th>指标</th><th>描述</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
</div>
"""
self.sections.append(alerts_section)
def generate(self, output_path: str) -> None:
"""
生成完整的 HTML 报告
【Python 规则37】字符串 join()
''.join(list) 将列表中的所有字符串用指定分隔符连接
这是拼接大量字符串的高效方式
"""
# ============================================================
# 【Python 规则38】CSS 内联样式
# 将 CSS 嵌入 HTML 中,使报告文件独立可用
# ============================================================
html_template = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{self.title}</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f0f2f5;
color: #333;
line-height: 1.6;
}}
.container {{
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 12px;
margin-bottom: 20px;
text-align: center;
}}
.header h1 {{ font-size: 28px; margin-bottom: 10px; }}
.status-badge {{
display: inline-block;
padding: 8px 20px;
border-radius: 20px;
font-weight: bold;
margin-top: 10px;
}}
.section {{
background: white;
border-radius: 12px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}}
.section h2 {{
font-size: 20px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f2f5;
}}
.metrics-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}}
.metric-card {{
text-align: center;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}}
.metric-value {{
font-size: 32px;
font-weight: bold;
color: #333;
}}
.metric-label {{
font-size: 14px;
color: #666;
margin-top: 5px;
}}
.progress-bar {{
background: #e9ecef;
border-radius: 10px;
height: 30px;
overflow: hidden;
}}
.progress-fill {{
height: 100%;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
transition: width 0.5s ease;
}}
.alert-table {{
width: 100%;
border-collapse: collapse;
}}
.alert-table th, .alert-table td {{
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}}
.alert-table th {{
background: #f8f9fa;
font-weight: 600;
}}
.badge {{
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
color: white;
}}
.badge.critical {{ background: #dc3545; }}
.badge.warning {{ background: #ffc107; color: #333; }}
.no-alerts {{
text-align: center;
font-size: 18px;
padding: 30px;
color: #28a745;
}}
</style>
</head>
<body>
<div class="container">
{''.join(self.sections)}
<div class="section" style="text-align: center; color: #999; font-size: 12px;">
由自动化监控系统生成 | Powered by Python
</div>
</div>
</body>
</html>"""
# ============================================================
# 【Python 规则39】Path.mkdir() 创建目录
# parents=True: 自动创建父目录
# exist_ok=True: 目录已存在时不报错
# ============================================================
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_template)
logger.info(f"HTML 报告已生成: {output_path}")
# 使用示例
if __name__ == "__main__":
# 模拟数据
sample_summary = {
"report_time": "2024-01-15 10:30:00",
"total_records": 100,
"cpu": {"avg": 45.2, "max": 97.5, "min": 5.1, "stddev": 18.3},
"alerts": [
{"level": "CRITICAL", "metric": "cpu", "message": "CPU 峰值 97.5%", "timestamp": "10:25:00"}
]
}
generator = HTMLReportGenerator("我的服务器监控报告")
generator.add_header(sample_summary)
generator.add_cpu_section(sample_summary["cpu"])
generator.add_alerts_section(sample_summary["alerts"])
generator.generate("/tmp/sysmonitor/report.html")
8. REST API 数据接口 api_server.py
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
API 服务模块 - 提供 RESTful 接口供外部系统查询监控数据
【Python 规则40】第三方库的使用
本模块使用了 Flask 框架,需要先安装: pip install flask
导入第三方库与标准库的方式相同
"""
import json
import os
import glob
from datetime import datetime
from typing import Dict, List, Optional
try:
from flask import Flask, jsonify, request, abort
# ============================================================
# 【Python 规则41】try...import 模式
# 用于处理可选依赖
# 如果 flask 未安装,给出友好提示而非直接崩溃
# ============================================================
except ImportError:
print("[ERROR] 请先安装 Flask: pip install flask")
exit(1)
# ============================================================
# 【Python 规则42】Flask 应用创建
# Flask(__name__) 创建应用实例
# __name__ 帮助 Flask 确定模板和静态文件的位置
# ============================================================
app = Flask(__name__)
# 全局配置
DATA_DIR = "/tmp/sysmonitor/data"
CACHE: Dict[str, any] = {}
CACHE_TTL = 60 # 缓存有效期(秒)
# ============================================================
# 【Python 规则43】装饰器(Decorator)
# @app.route('/') 是 Flask 的路由装饰器
# 装饰器本质上是一个高阶函数,它接收一个函数并返回一个新函数
# @decorator 等价于 func = decorator(func)
# ============================================================
@app.route('/')
def index():
"""
API 根路径 - 返回接口文档
【Python 规则44】jsonify()
Flask 提供的函数,将 Python 字典转换为 JSON 响应
自动设置 Content-Type: application/json
"""
return jsonify({
"service": "系统监控 API",
"version": "1.0.0",
"endpoints": {
"/api/latest": "获取最新一条监控数据",
"/api/history": "获取历史数据(支持分页)",
"/api/alerts": "获取告警信息",
"/api/summary": "获取分析摘要",
"/api/health": "健康检查"
}
})
@app.route('/api/health')
def health_check():
"""健康检查接口"""
return jsonify({
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"data_dir_exists": os.path.isdir(DATA_DIR)
})
@app.route('/api/latest')
def get_latest():
"""
获取最新一条监控数据
【Python 规则45】sorted() 函数
sorted(iterable, key=None, reverse=False)
返回排序后的新列表,不修改原列表
key 参数接受一个函数,用于指定排序依据
"""
files = sorted(glob.glob(os.path.join(DATA_DIR, "sysinfo_*.json")))
if not files:
abort(404, description="没有找到监控数据")
# ============================================================
# 【Python 规则46】abort()
# Flask 的 abort() 立即终止请求并返回 HTTP 错误
# abort(404) 返回 404 Not Found
# ============================================================
latest_file = files[-1] # 取最后一个(最新的)
with open(latest_file, 'r', encoding='utf-8') as f:
data = json.load(f)
return jsonify({
"source_file": os.path.basename(latest_file),
"data": data
})
@app.route('/api/history')
def get_history():
"""
获取历史监控数据(支持分页)
【Python 规则47】request 对象
Flask 的 request 对象包含请求的所有信息
request.args: URL 查询参数(?key=value)
request.form: POST 表单数据
request.json: POST JSON 数据
request.headers: 请求头
"""
# 获取分页参数,提供默认值
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
# ============================================================
# 【Python 规则48】request.args.get() 带类型转换
# get(key, default, type=func)
# type=int 会自动将字符串参数转换为整数
# 转换失败时返回 default
# ============================================================
# 参数校验
if page < 1:
page = 1
if per_page < 1 or per_page > 100:
per_page = 10
files = sorted(glob.glob(os.path.join(DATA_DIR, "sysinfo_*.json")))
total = len(files)
# 计算分页
start = (page - 1) * per_page
end = start + per_page
page_files = files[start:end]
# ============================================================
# 【Python 规则49】列表切片用于分页
# list[start:end] 取从 start 到 end(不含)的元素
# 如果 end 超出列表长度,不会报错,只取到末尾
# ============================================================
records = []
for filepath in page_files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
records.append(json.load(f))
except (json.JSONDecodeError, IOError):
continue
return jsonify({
"pagination": {
"page": page,
"per_page": per_page,
"total": total,
"total_pages": (total + per_page - 1) // per_page,
# ============================================================
# 【Python 规则50】// 整除运算符
# a // b 返回商的整数部分(向下取整)
# (total + per_page - 1) // per_page 是向上取整的写法
# ============================================================
},
"records": records
})
@app.route('/api/alerts')
def get_alerts():
"""获取告警信息"""
report_file = os.path.join("/tmp/sysmonitor", "analysis_report.json")
if not os.path.exists(report_file):
return jsonify({"alerts": [], "message": "暂无分析报告"})
with open(report_file, 'r', encoding='utf-8') as f:
report = json.load(f)
# ============================================================
# 【Python 规则51】filter() 函数
# filter(function, iterable) 过滤出使 function 返回 True 的元素
# lambda: 匿名函数,lambda 参数: 表达式
# ============================================================
alerts = report.get("alerts", [])
# 支持按级别过滤
level = request.args.get('level')
if level:
alerts = list(filter(lambda a: a['level'] == level.upper(), alerts))
# ============================================================
# 【Python 规则52】lambda 匿名函数
# lambda 参数列表: 表达式
# 只能包含一个表达式,不能有多条语句
# 常用于简短的回调函数
# ============================================================
return jsonify({
"total": len(alerts),
"alerts": alerts
})
@app.route('/api/summary')
def get_summary():
"""获取分析摘要"""
report_file = os.path.join("/tmp/sysmonitor", "analysis_report.json")
if not os.path.exists(report_file):
abort(404, description="分析报告尚未生成")
with open(report_file, 'r', encoding='utf-8') as f:
report = json.load(f)
return jsonify(report)
# ============================================================
# 【Python 规则53】自定义错误处理
# @app.errorhandler(状态码) 装饰器注册错误处理函数
# ============================================================
@app.errorhandler(404)
def not_found(error):
return jsonify({
"error": "Not Found",
"message": str(error.description)
}), 404
@app.errorhandler(500)
def internal_error(error):
return jsonify({
"error": "Internal Server Error",
"message": "服务器内部错误"
}), 500
if __name__ == "__main__":
# ============================================================
# 【Python 规则54】Flask 应用启动
# host='0.0.0.0': 监听所有网络接口(允许外部访问)
# port=5000: 端口号
# debug=True: 开启调试模式(代码修改后自动重启)
# ⚠️ 生产环境不要使用 debug=True
# ============================================================
print("=" * 50)
print(" 系统监控 API 服务启动")
print(" 地址: http://localhost:5000")
print(" 接口文档: http://localhost:5000/")
print("=" * 50)
app.run(host='0.0.0.0', port=5000, debug=True)