自动化服务器运维监控系统(python+shell)

该项目是一个完整的服务器运维监控系统,可以通过一个完整的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)
相关推荐
zhanghaha13141 小时前
Python进阶教程:23_Scrapy 爬虫框架 零基础超详细教程
python·信息可视化
风景的人生1 小时前
虚拟机ip连不上(怀疑是最开始虚拟机复制造成的网络冲突)
linux·运维·服务器
Lumistory1 小时前
企业总部泛光照明运维踩坑?看完这篇少走三年弯路
运维·光照贴图
爱读源码的大都督1 小时前
DeepSeek面试官问:生产RAG系统回答不准确,该如何定位和优化?这样回答,能让面试官当场给你Offer!
java·后端·python
Cx330❀1 小时前
【Linux网络】深入TCP协议:从滑动窗口到拥塞控制与性能优化全景解析
linux·开发语言·网络·tcp/ip·ai·性能优化·ai编程
自动化监测Learner2 小时前
自动化监测数据传不出廊道?大坝通信组网选型实战:RS485、光纤、4G、LoRa、北斗短报文一篇讲透
运维·网络·自动化
代码村新手2 小时前
Linux-将普通用户添加到系统信任文件(sudoers file)白名单中
linux
HAHAXX82 小时前
2026智能自动化落地:通义灵码与Cursor加持,RPA融合生成式AI的工程化实践
人工智能·自动化·rpa
chuntian_tester2 小时前
AI自动化第1步【系统探索】
人工智能·测试工具·ai·自动化