提交 cc32739d authored 作者: 陈泽健's avatar 陈泽健

feat(monitoring): 增强服务监测脚本的邮件发送功能

- 实现 Markdown 到 HTML 的简单转换功能,提升邮件报告可读性
- 添加对 mailx 和 sendmail 的依赖检查与自动安装
- 改进邮件发送逻辑,支持 HTML 格式邮件发送
- 优化邮件主题格式,统一为"自动化服务监测报告 - 主机名 - 时间"
- 在 PRD 文档中更新平台识别逻辑的描述
- 添加邮件发送的多重备选方案(mailx SMTP、sendmail、mailx 纯文本)
上级 5c4926c5
...@@ -678,53 +678,267 @@ collect_container_info() { ...@@ -678,53 +678,267 @@ collect_container_info() {
log INFO "[容器检测] 已采集容器信息:running=$(echo \"$CONTAINER_RUNNING_LIST\" | wc -l) 行,exited=$(echo \"$CONTAINER_EXITED_LIST\" | wc -l) 行" log INFO "[容器检测] 已采集容器信息:running=$(echo \"$CONTAINER_RUNNING_LIST\" | wc -l) 行,exited=$(echo \"$CONTAINER_EXITED_LIST\" | wc -l) 行"
} }
# 判断 mailx 是否支持用 -a 添加自定义头(s-nail/mailx 常见)
mailx_supports_a_header() {
if ! command -v mailx >/dev/null 2>&1; then
return 1
fi
# 用 -V / -v 尝试识别 s-nail 关键字(不同发行版输出不同,做一个宽松判断)
if mailx -V 2>&1 | grep -qiE 's-nail|heirloom'; then
# s-nail 一般支持 -a header;heirloom 常把 -a 当附件(不可靠)
if mailx -V 2>&1 | grep -qi 's-nail'; then
return 0
fi
return 1
fi
# 兜底:看帮助里是否明确出现 “-a header”
mailx -h 2>&1 | grep -qiE '(\-a[[:space:]]+header|\-a[[:space:]]+.*header)'
}
ensure_sendmail_installed() {
if command -v sendmail >/dev/null 2>&1; then
log INFO "[依赖检查] sendmail 已安装"
return 0
fi
local pkg_mgr
pkg_mgr="$(detect_os_and_pkg_mgr)"
log INFO "[依赖安装] 尝试安装 sendmail/postfix,pkg_mgr=${pkg_mgr}"
if [[ "$pkg_mgr" == "yum" ]]; then
yum install -y postfix >>"$LOG_FILE" 2>&1 || return 1
systemctl enable --now postfix >>"$LOG_FILE" 2>&1 || true
elif [[ "$pkg_mgr" == "apt" ]]; then
apt-get update >>"$LOG_FILE" 2>&1 || true
apt-get install -y postfix >>"$LOG_FILE" 2>&1 || return 1
systemctl enable --now postfix >>"$LOG_FILE" 2>&1 || true
else
log ERROR "[依赖安装] 未识别包管理器,无法自动安装 postfix"
return 1
fi
command -v sendmail >/dev/null 2>&1 && log INFO "[依赖安装] sendmail 安装完成"
}
# 简单把 Markdown 报告转成 HTML(够用:标题/列表/代码/表格)
md_to_html_simple() {
local report_file="$1"
# 用 awk 处理 Markdown 表格 + 普通行,其它用 sed 做基础替换
# 注意:这是“可读性优先”的轻量转换器,不追求完整 Markdown 语法覆盖。
awk '
function html_escape(s, t) {
t=s
gsub(/&/,"&amp;",t); gsub(/</,"&lt;",t); gsub(/>/,"&gt;",t)
return t
}
function trim(s){ sub(/^[ \t\r\n]+/,"",s); sub(/[ \t\r\n]+$/,"",s); return s }
BEGIN{
in_table=0
print "<html><head><meta charset=\"utf-8\"></head>"
print "<body style=\"font-family:Segoe UI,Microsoft YaHei,Arial,sans-serif;color:#111;\">"
print "<div style=\"max-width:980px;margin:0 auto;\">"
}
# 表格分隔行:| --- | --- |
function is_table_sep(line) {
return (line ~ /^\|[ \t]*[-: ]+[ \t]*\|/)
}
function is_table_row(line) {
return (line ~ /^\|.*\|[ \t]*$/)
}
function start_table(header_line) {
in_table=1
print "<table border=\"1\" cellpadding=\"6\" cellspacing=\"0\" style=\"border-collapse:collapse;margin:8px 0;font-size:13px;\">"
print "<thead><tr>"
split(header_line, a, "|")
for (i=2; i<=length(a)-1; i++){
h=trim(a[i])
print "<th style=\"background:#f3f4f6;\">" html_escape(h) "</th>"
}
print "</tr></thead><tbody>"
}
function table_row(line) {
print "<tr>"
split(line, a, "|")
for (i=2; i<=length(a)-1; i++){
c=trim(a[i])
print "<td>" html_escape(c) "</td>"
}
print "</tr>"
}
function end_table() {
if(in_table){
print "</tbody></table>"
in_table=0
}
}
{
line=$0
# 空行
if (line ~ /^[ \t]*$/) {
end_table()
print "<div style=\"height:8px;\"></div>"
next
}
# 表格处理:header + separator + body
if (!in_table && is_table_row(line)) {
header=line
getline sep
if (is_table_sep(sep)) {
start_table(header)
# 读取 body 行(可能 0..n 行)
while (getline body_line) {
if (!is_table_row(body_line)) {
# 非表格行,回退给后续处理:awk 没回退机制,只能先处理并继续
end_table()
line=body_line
break
}
table_row(body_line)
}
# 这里 line 可能是表格后第一行(非表格),继续走下面普通行逻辑
if (in_table==0 && line !~ /^\|.*\|[ \t]*$/) {
# fallthrough
} else {
next
}
} else {
# 不是标准表格,按普通行处理(sep 行也要输出)
end_table()
print "<div style=\"line-height:1.6;\">" html_escape(header) "</div>"
print "<div style=\"line-height:1.6;\">" html_escape(sep) "</div>"
next
}
}
end_table()
# 标题
if (substr(line,1,2)=="# ") {
print "<h1>" html_escape(substr(line,3)) "</h1>"
next
}
if (substr(line,1,3)=="## ") {
print "<h2>" html_escape(substr(line,4)) "</h2>"
next
}
if (substr(line,1,4)=="### ") {
print "<h3>" html_escape(substr(line,5)) "</h3>"
next
}
# 列表项(简单)
if (line ~ /^- /) {
# 开启列表块(用一个很简化的方式:遇到 - 就开始 UL,直到遇到非 - 行或空行)
print "<ul style=\"margin:6px 0 12px 18px;\">"
do {
item=substr(line,3)
print "<li style=\"line-height:1.6;\">" html_escape(item) "</li>"
if (getline nxt) {
if (nxt ~ /^- /) { line=nxt; continue }
else { line=nxt; break }
} else { line=""; break }
} while (1)
print "</ul>"
if (line=="") next
# 继续处理 line(当前是列表后第一行)
}
# 普通段落
print "<div style=\"line-height:1.6;white-space:pre-wrap;\">" html_escape(line) "</div>"
}
END{
end_table()
print "<hr/>"
print "<div style=\"color:#6b7280;font-size:12px;\">说明:本邮件正文为自动化监测报告的 HTML 展示版;原始 Markdown 报告请查看落盘文件。</div>"
print "</div></body></html>"
}' "$report_file"
}
#################### 6. 邮件发送 #################### #################### 6. 邮件发送 ####################
# 尝试发送邮件,把生成的 MD 报告作为正文发送 # 尝试发送邮件,把生成的 MD 报告作为正文发送
send_report_mail() { send_report_mail() {
local report_file="$1" local report_md="$1"
if [[ ! -f "$report_file" ]]; then if [[ ! -f "$report_md" ]]; then
log ERROR "[邮件发送] 报告文件不存在:${report_file}" log ERROR "[邮件发送] 报告文件不存在:$report_md"
return 1 return 1
fi fi
local subject="${MAIL_SUBJECT_PREFIX} - ${HOST_NAME} - $(date '+%Y-%m-%d %H:%M:%S')" local subject="自动化服务监测报告 - ${HOST_NAME} - $(date '+%Y-%m-%d %H:%M:%S')"
local to="${MAIL_TO}"
to="${to//,/ }"
# 优先使用 mail/mailx,如果没有则尝试 sendmail local report_html
if command -v mail >/dev/null 2>&1; then report_html="$(md_to_html_simple "$report_md")"
# mail 命令:正文使用报告内容
log INFO "[邮件发送] 使用 mail 命令发送报告给 ${MAIL_TO}" # 1) 先尝试:mailx(SMTP) 发 HTML(仅在确认支持 header 的情况下)
mail -s "${subject}" "${MAIL_TO}" < "${report_file}" 2>>"$LOG_FILE" || { if command -v mailx >/dev/null 2>&1; then
log ERROR "[邮件发送] mail 命令发送失败" if mailx_supports_a_header; then
return 1 log INFO "[邮件发送] 使用 mailx(SMTP) 发送 HTML 报告给:$to"
} if printf "%s\n" "$report_html" | mailx -s "$subject" -a "Content-Type: text/html; charset=UTF-8" "$to"; then
elif command -v mailx >/dev/null 2>&1; then return 0
log INFO "[邮件发送] 使用 mailx 命令发送报告给 ${MAIL_TO}" fi
mailx -s "${subject}" "${MAIL_TO}" < "${report_file}" 2>>"$LOG_FILE" || { log WARN "[邮件发送] mailx(SMTP) HTML 发送失败,将回退 sendmail HTML"
log ERROR "[邮件发送] mailx 命令发送失败" # 不 return,继续走 sendmail
return 1 else
} log WARN "[邮件发送] 当前 mailx 不支持 '-a header'(-a 被当附件),跳过 mailx HTML,改用 sendmail HTML"
elif command -v sendmail >/dev/null 2>&1; then fi
log INFO "[邮件发送] 使用 sendmail 发送报告给 ${MAIL_TO}" fi
# 2) 回退:sendmail 发送 HTML(MIME 最可控)
if command -v sendmail >/dev/null 2>&1; then
log INFO "[邮件发送] 使用 sendmail 发送 HTML 报告给:$to"
local boundary="BOUNDARY_$(date +%s)_$$"
{ {
echo "To: ${MAIL_TO}" echo "To: ${to}"
echo "Subject: ${subject}" echo "Subject: ${subject}"
echo "Content-Type: text/markdown; charset=utf-8" echo "MIME-Version: 1.0"
echo "Content-Type: multipart/alternative; boundary=\"${boundary}\""
echo echo
cat "${report_file}" echo "--${boundary}"
} | sendmail -t 2>>"$LOG_FILE" || { echo "Content-Type: text/plain; charset=utf-8"
echo
echo "本邮件包含 HTML 版本监测报告。若客户端不支持 HTML,请查看落盘 Markdown:$report_md"
echo
echo "--${boundary}"
echo "Content-Type: text/html; charset=utf-8"
echo "Content-Transfer-Encoding: 8bit"
echo
echo "${report_html}"
echo
echo "--${boundary}--"
} | sendmail -t || {
log ERROR "[邮件发送] sendmail 发送失败" log ERROR "[邮件发送] sendmail 发送失败"
return 1 return 1
} }
else return 0
log ERROR "[邮件发送] 本机未找到 mail/mailx/sendmail 命令,无法发送邮件,请先安装邮件客户端。"
return 1
fi fi
log INFO "[邮件发送] 报告邮件已发送:${report_file} -> ${MAIL_TO}" # 3) 最后兜底:mailx 纯文本
return 0 if command -v mailx >/dev/null 2>&1; then
log WARN "[邮件发送] 未找到 sendmail,回退 mailx 纯文本发送(无法保证 HTML 渲染)"
cat "$report_md" | mailx -s "$subject" "$to" || {
log ERROR "[邮件发送] mailx 纯文本发送失败"
return 1
}
return 0
fi
log ERROR "[邮件发送] 未找到 mailx/sendmail,无法发送邮件"
return 1
} }
#################### 5. 报告输出(md) #################### #################### 5. 报告输出(md) ####################
write_md_report() { write_md_report() {
mkdir -p "$REPORT_DIR" 2>/dev/null || true mkdir -p "$REPORT_DIR" 2>/dev/null || true
...@@ -853,6 +1067,8 @@ main_run_once() { ...@@ -853,6 +1067,8 @@ main_run_once() {
# 0. 环境准备:只处理邮件依赖和配置 # 0. 环境准备:只处理邮件依赖和配置
ensure_mailx_installed || log ERROR "[启动] mailx 安装失败,后续邮件发送可能不可用。" ensure_mailx_installed || log ERROR "[启动] mailx 安装失败,后续邮件发送可能不可用。"
ensure_mailx_smtp_config || log ERROR "[启动] mailx SMTP 配置生成失败,后续邮件发送可能不可用。" ensure_mailx_smtp_config || log ERROR "[启动] mailx SMTP 配置生成失败,后续邮件发送可能不可用。"
# 新增:确保 sendmail 可用(用于发送 HTML 邮件)
ensure_sendmail_installed || log WARN "[启动] sendmail 安装失败,将回退纯文本邮件(排版不可优化)。"
detect_platform detect_platform
detect_systems detect_systems
...@@ -883,7 +1099,7 @@ main_run_once() { ...@@ -883,7 +1099,7 @@ main_run_once() {
local report_file local report_file
report_file="$(write_md_report)" report_file="$(write_md_report)"
# 6. 邮件发送 # 6. 邮件发送(注意:这里要传 report_file,而不是未定义的 REPORT_MD_FILE)
send_report_mail "$report_file" || { send_report_mail "$report_file" || {
log ERROR "[主流程] 报告已生成,但邮件发送失败,请检查日志和邮件客户端配置。" log ERROR "[主流程] 报告已生成,但邮件发送失败,请检查日志和邮件客户端配置。"
} }
......
...@@ -19,7 +19,8 @@ ...@@ -19,7 +19,8 @@
#### 检测需求 #### 检测需求
##### 1、平台识别(✅ 已实现): ##### 1、平台识别(✅ 已实现):
自动检测目标服务器平台类型(检测 /data/services 目录,如果没有则是传统平台) 新统一平台:基于 /data/services 目录存在性检测
传统平台:当 /data/services 目录不存在时自动识别为传统平台
##### 2、系统识别(✅ 已实现): ##### 2、系统识别(✅ 已实现):
自动检测目标服务器的系统类型(检测容器分为三种:ujava、upython、upython_voice,如果有ujava则有会议预定系统、python对应运维集控系统、upython_voice对应转录系统) 自动检测目标服务器的系统类型(检测容器分为三种:ujava、upython、upython_voice,如果有ujava则有会议预定系统、python对应运维集控系统、upython_voice对应转录系统)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论