Skip to content

Commit

Permalink
Notification support log&webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
zdz committed Jul 14, 2022
1 parent 73e9dd0 commit 7d49226
Show file tree
Hide file tree
Showing 7 changed files with 444 additions and 11 deletions.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,19 @@
`cppla/ServerStatus` 的威力加强版,保持轻量和简化部署,增加主要特性如下:

- 使用 `rust` 完全重写 `server``client`,单个执行文件部署
- 支持上下线和简单自定义规则告警 (`telegram``wechat``email`)
- 支持上下线和简单自定义规则告警 (`telegram``wechat``email``webhook`)
- 支持 `http` 协议上报,可配合 `cf` 等优化上报链路
- 支持 `vnstat` 统计月流量,重启不丢流量数据
- 支持 `railway` 快速部署
- 支持 `systemd` 开机自启
- 其它功能,如 🗺️ 见 [wiki](https://github.com/zdz/ServerStatus-Rust/wiki)

演示:[ssr.rs](https://d.ssr.rs) | [vercel.app](https://tz-rust.vercel.app)
演示:[ssr.rs](https://ssr.rs)
|
下载:[Releases](https://github.com/zdz/ServerStatus-Rust/releases)
|
[Changelog](https://github.com/zdz/ServerStatus-Rust/releases)
|
反馈:[Discussions](https://github.com/zdz/ServerStatus-Rust/discussions)

📕 完整文档迁移至 [doc.ssr.rs](https://doc.ssr.rs)
Expand Down Expand Up @@ -189,14 +191,16 @@ notify_interval = 30
# https://core.telegram.org/bots/api
# https://jinja.palletsprojects.com/en/3.0.x/templates/#if
[tgbot]
# 开关 true 打开
enabled = false
bot_token = "<tg bot token>"
chat_id = "<chat id>"
# host 可用字段参见 payload.rs 文件 HostStat 结构, {{host.xxx}} 为占位变量
# 例如 host.name 可替换为 host.alias,大家根据喜好来编写通知消息
# 例如 host.name 可替换为 host.alias,大家根据自己的喜好来编写通知消息
# {{ip_info.query}} 主机 ip, {{sys_info.host_name}} 主机 hostname
title = "❗<b>Server Status</b>"
online_tpl = "{{config.title}} \n😆 {{host.location}} {{host.name}} 主机恢复上线啦"
offline_tpl = "{{config.title}} \n😱 {{host.location}} {{host.name}} 主机已经掉线啦"
online_tpl = "{{config.title}} \n😆 {{host.location}} {{host.name}} 主机恢复上线啦"
offline_tpl = "{{config.title}} \n😱 {{host.location}} {{host.name}} 主机已经掉线啦"
# custom 模板置空则停用自定义告警,只保留上下线通知
custom_tpl = """
{% if host.memory_used / host.memory_total > 0.5 %}
Expand Down
155 changes: 154 additions & 1 deletion config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ offline_threshold = 30
admin_user = ""
admin_pass = ""

# hosts 跟 hosts_group 两种配置模式任挑一种配置即可
# name 主机唯一标识,不可重复,alias 为展示名
# 使用 ansible 批量部署时可以用主机 hostname 作为 name,统一密码
# notify = false 单独禁止单台机器的告警,一般针对网络差,频繁上下线
Expand Down Expand Up @@ -44,11 +45,13 @@ notify_interval = 30
# https://core.telegram.org/bots/api
# https://jinja.palletsprojects.com/en/3.0.x/templates/#if
[tgbot]
# 开关 true 打开
enabled = false
bot_token = "<tg bot token>"
chat_id = "<chat id>"
# host 可用字段参见 payload.rs 文件 HostStat 结构, {{host.xxx}} 为占位变量
# 例如 host.name 可替换为 host.alias,大家根据喜好来编写通知消息
# 例如 host.name 可替换为 host.alias,大家根据自己的喜好来编写通知消息
# {{ip_info.query}} 主机 ip, {{sys_info.host_name}} 主机 hostname
title = "❗<b>Server Status</b>"
online_tpl = "{{config.title}} \n😆 {{host.location}} {{host.name}} 主机恢复上线啦"
offline_tpl = "{{config.title}} \n😱 {{host.location}} {{host.name}} 主机已经掉线啦"
Expand All @@ -62,7 +65,9 @@ custom_tpl = """
<pre>😲 {{host.name}} 主机硬盘使用率超50%, 当前{{ (100 * host.hdd_used / host.hdd_total) | round }}% </pre>
{% endif %}
"""
###################### tgbot end ##########################

## 可选 微信通知
[wechat]
enabled = false
corp_id = "<corp id>"
Expand All @@ -80,7 +85,9 @@ custom_tpl = """
😲 {{host.name}} 主机硬盘使用率超80%
{% endif %}
"""
###################### wechat end ##########################

## 可选 邮件通知
[email]
enabled = false
server = "smtp.gmail.com"
Expand All @@ -100,3 +107,149 @@ custom_tpl = """
<pre>😲 {{host.name}} 主机硬盘使用率超80%, 当前{{ (100 * host.hdd_used / host.hdd_total) | round }}% </pre>
{% endif %}
"""

###################### email end ##########################

## 可选 单纯记录 event 到日志文件
[log]
enabled = false
log_dir = "/opt/ServerStatus/logs"
tpl = """{% set obj = dict(event=event, host=host, ip_info=ip_info, sys_info=sys_info) %} {{ obj | tojson}}"""


## 可选 webhook
[webhook]
# 总开关
enabled = false
# 可多个 webhook.receiver
[[webhook.receiver]] # 通用型 webhook
# 局部开关
enabled = false
# https://webhook.site/#!/2b1ad731-45fe-49a8-ae91-614167019db2
url = "https://webhook.site/2b1ad731-45fe-49a8-ae91-614167019db2"
headers = { content-type = "application/json", x-data = "y-data" }
# headers = { content-type = "text/plain" }
# 可选 HTTP Basic Auth
username = "u"
password = "p"
timeout = 5 #s
# 简单发送一个 json 对象,#{} 为 Object 对象, [] 为数组
# 最终结果, 固定结构 [是否发送通知,结果对象]
script = """[true, #{config: config, event: event, host: host, ip_info: ip_info, sys_info:sys_info} ]"""

[[webhook.receiver]] # Discord
enabled = false
# https://discord.com/developers/docs/resources/webhook
url = "https://discord.com/api/webhooks/xxxxxxxxxxxxxxxxxxxxxxx"
headers = { content-type = "application/json" }
timeout = 5 #s
script = """
let message = "";
switch event {
"Custom" => { // 自定义事件
let threshold = 10;
let msgs = [];
let memory_usage = round(host.memory_used * 100.0 / host.memory_total);
if memory_usage > threshold {
msgs.push(`😲 ${host.location} ${host.name} 主机内存使用率超${threshold}%, 当前 ${memory_usage}%`);
}
let hdd_usage = round(host.hdd_used * 100.0 / host.hdd_total);
if hdd_usage > threshold {
msgs.push(`😲 ${host.location} ${host.name} 主机硬盘使用率超${threshold}%, 当前 ${hdd_usage}%`);
}
message = join(msgs, "\\n");
},
"NodeDown" => { // 掉线
message = `😱 ${host.location} ${host.name} 主机已经掉线啦`;
},
"NodeUp" => { // 上线
message = `😆 ${host.location} ${host.name} 主机恢复上线啦`;
}
}
// 返回的 json 结构,#{} 为 Object 对象, [] 为数组
// 最终结果, 固定结构 [是否发送通知,结果对象]
[message.len() > 0, #{ embeds: [ #{
title: ":bell: ServerStatus-Rust :bell:",
fields: [
#{ name: "Event", value: event, },
#{ name: "Datetime", value: now_str(), },
#{ name: "Message", value: message, },
]
}]}]
"""

[[webhook.receiver]] # Slack
enabled = false
# https://api.slack.com/messaging/webhooks
url = "https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxx"
headers = { content-type = "application/json" }
timeout = 5 #s
script = """
let message = "";
switch event {
"Custom" => { // 自定义事件
let threshold = 80;
let msgs = [];
let memory_usage = round(host.memory_used * 100.0 / host.memory_total);
if memory_usage > threshold {
msgs.push(`😲 ${host.location} ${host.name} 主机内存使用率超${threshold}%, 当前 ${memory_usage}%`);
}
let hdd_usage = round(host.hdd_used * 100.0 / host.hdd_total);
if hdd_usage > threshold {
msgs.push(`😲 ${host.location} ${host.name} 主机硬盘使用率超${threshold}%, 当前 ${hdd_usage}%`);
}
message = join(msgs, "\\n");
},
"NodeDown" => { // 掉线
message = `😱 ${host.location} ${host.name} 主机已经掉线啦`;
},
"NodeUp" => { // 上线
message = `😆 ${host.location} ${host.name} 主机恢复上线啦`;
}
}
// 最终结果, 固定结构 [是否发送通知,结果对象]
[message.len() > 0, #{text: message}]
"""

[[webhook.receiver]] # WorkWechat
enabled = false
# https://developer.work.weixin.qq.com/document/path/91770
url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxxxxxxxxxxxxxxxxx"
headers = { content-type = "application/json" }
timeout = 5 #s
script = """
let message = "";
switch event {
"Custom" => { // 自定义事件
let threshold = 80;
let msgs = [];
let memory_usage = round(host.memory_used * 100.0 / host.memory_total);
if memory_usage > threshold {
msgs.push(`😲 ${host.location} ${host.name} 主机内存使用率超${threshold}%, 当前 ${memory_usage}%`);
}
let hdd_usage = round(host.hdd_used * 100.0 / host.hdd_total);
if hdd_usage > threshold {
msgs.push(`😲 ${host.location} ${host.name} 主机硬盘使用率超${threshold}%, 当前 ${hdd_usage}%`);
}
message = join(msgs, "\\n");
},
"NodeDown" => { // 掉线
message = `😱 ${host.location} ${host.name} 主机已经掉线啦`;
},
"NodeUp" => { // 上线
message = `😆 ${host.location} ${host.name} 主机恢复上线啦`;
}
}
// 最终结果, 固定结构 [是否发送通知,结果对象]
[message.len() > 0, #{
msgtype: "text",
text: #{
content: message
}
}]
"""

###################### webhook end ##########################
4 changes: 4 additions & 0 deletions server/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ pub struct Config {
pub wechat: notifier::wechat::Config,
#[serde(default = "Default::default")]
pub email: notifier::email::Config,
#[serde(default = "Default::default")]
pub log: notifier::log::Config,
#[serde(default = "Default::default")]
pub webhook: notifier::webhook::Config,

#[serde(default = "Default::default")]
pub hosts: Vec<Host>,
Expand Down
10 changes: 9 additions & 1 deletion server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ async fn main() -> Result<()> {
eprintln!("✨ run in normal mode, load conf from local file `{}", &args.config);
config::from_file(&args.config)
} {
debug!("{:?}", cfg);
debug!("{}", serde_json::to_string_pretty(&cfg).unwrap());
G_CONFIG.set(cfg).unwrap();
} else {
error!("can't parse config");
Expand All @@ -220,6 +220,14 @@ async fn main() -> Result<()> {
let o = Box::new(notifier::email::Email::new(&cfg.email));
notifies.lock().unwrap().push(o);
}
if cfg.log.enabled {
let o = Box::new(notifier::log::Log::new(&cfg.log));
notifies.lock().unwrap().push(o);
}
if cfg.webhook.enabled {
let o = Box::new(notifier::webhook::Webhook::new(&cfg.webhook));
notifies.lock().unwrap().push(o);
}
// init notifier end

// notify test
Expand Down
94 changes: 94 additions & 0 deletions server/src/notifier/log.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#![deny(warnings)]
use anyhow::Result;
use chrono::Local;
use minijinja::context;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;

use crate::jinja::{add_template, render_template};
use crate::notifier::{Event, HostStat, NOTIFIER_HANDLE};

const KIND: &str = "log";

#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Config {
pub enabled: bool,
pub log_dir: String,
pub tpl: String,
}

pub struct Log {
config: &'static Config,
}

impl Log {
pub fn new(cfg: &'static Config) -> Self {
let o = Self { config: cfg };

add_template(KIND, "tpl", o.config.tpl.to_string());

// build dir
fs::create_dir_all(&cfg.log_dir).unwrap_or_else(|_| panic!("can't create dir `{}", cfg.log_dir));
o
}
}

impl crate::notifier::Notifier for Log {
fn kind(&self) -> &'static str {
KIND
}

fn send_notify(&self, content: String) -> Result<()> {
if content.is_empty() {
return Ok(());
}

let dt = Local::now().format("%Y-%m-%d").to_string();
let log_file = Path::new(&self.config.log_dir)
.join(format!("ssr.log.{}", dt))
.to_string_lossy()
.to_string();

let handle = NOTIFIER_HANDLE.lock().unwrap().as_ref().unwrap().clone();
handle.spawn(async move {
//
let mut file = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(&log_file)
.await
.unwrap_or_else(|_| panic!("can't create log `{}", log_file));

let _ = file
.write(content.as_bytes())
.await
.unwrap_or_else(|_| panic!("can't write log `{}", log_file));

if !content.ends_with('\n') {
let _ = file
.write(b"\n")
.await
.unwrap_or_else(|_| panic!("can't write log `{}", log_file));
}

file.flush()
.await
.unwrap_or_else(|_| panic!("can't flush log `{}", log_file));
});
Ok(())
}

fn notify(&self, e: &Event, stat: &HostStat) -> Result<()> {
render_template(
self.kind(),
"tpl",
context!(event => e, host => stat, config => self.config, ip_info => stat.ip_info, sys_info => stat.sys_info),
true,
)
.map(|content| self.send_notify(content).unwrap())
}
}
11 changes: 7 additions & 4 deletions server/src/notifier/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use anyhow::Result;
use once_cell::sync::Lazy;
use serde::Serialize;
use std::sync::Mutex;
use tokio::runtime::Handle;

use crate::payload::HostStat;

pub mod email;
pub mod log;
pub mod tgbot;
pub mod webhook;
pub mod wechat;

pub static NOTIFIER_HANDLE: Lazy<Mutex<Option<Handle>>> = Lazy::new(Default::default);

#[derive(Debug)]
#[derive(Debug, Serialize, Clone)]
pub enum Event {
NodeUp,
NodeDown,
Expand All @@ -20,9 +23,9 @@ pub enum Event {

fn get_tag(e: &Event) -> &'static str {
match *e {
Event::NodeUp => "online",
Event::NodeDown => "offline",
Event::Custom => "custom",
Event::NodeUp => "NodeUp",
Event::NodeDown => "NodeDown",
Event::Custom => "Custom",
}
}

Expand Down
Loading

0 comments on commit 7d49226

Please sign in to comment.