diff --git a/Cargo.lock b/Cargo.lock index 33d3789..e16355d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ version = 3 name = "alert2muc" version = "0.1.0" dependencies = [ + "askama", "axum", "futures", "serde", @@ -18,6 +19,54 @@ dependencies = [ "xmpp-parsers", ] +[[package]] +name = "askama" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb98f10f371286b177db5eeb9a6e5396609555686a35e1d4f7b9a9c6d8af0139" +dependencies = [ + "askama_derive", + "askama_escape", + "askama_shared", +] + +[[package]] +name = "askama_derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87bf87e6e8b47264efa9bde63d6225c6276a52e05e91bf37eaa8afd0032d6b71" +dependencies = [ + "askama_shared", + "proc-macro2", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_shared" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf722b94118a07fcbc6640190f247334027685d4e218b794dbfe17c32bf38ed0" +dependencies = [ + "askama_escape", + "humansize", + "mime", + "mime_guess", + "nom", + "num-traits", + "percent-encoding", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", +] + [[package]] name = "async-trait" version = "0.1.59" @@ -503,6 +552,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humansize" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" + [[package]] name = "hyper" version = "0.14.23" @@ -694,6 +749,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minidom" version = "0.15.0" @@ -703,6 +768,12 @@ dependencies = [ "rxml", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "mio" version = "0.8.5" @@ -733,6 +804,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1512,6 +1593,15 @@ dependencies = [ "xmpp-parsers", ] +[[package]] +name = "toml" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +dependencies = [ + "serde", +] + [[package]] name = "tower" version = "0.4.13" @@ -1679,6 +1769,15 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.8" diff --git a/Cargo.toml b/Cargo.toml index 5a92c99..42a2060 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ tokio-xmpp = "3" xmpp-parsers = "0.19" axum = "0.6" systemd = "0.10" +askama = "0.11" diff --git a/src/main.rs b/src/main.rs index c3f032a..81f104f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,5 @@ -use std::{ - net::SocketAddr, - collections::HashMap, - sync::{Arc, Mutex}, -}; +use std::net::SocketAddr; +use askama::Template; use axum::{ extract::State, routing::post, @@ -14,61 +11,59 @@ use serde::Deserialize; mod jabber; -#[derive(Deserialize, Clone, Debug)] -#[allow(non_snake_case)] +#[derive(Debug, Clone, Deserialize)] +struct Payload { + alerts: Vec, +} + +#[derive(Debug, Clone, Deserialize, Template)] +#[template(path="alert.txt", escape="none")] struct Alert { - generatorURL: String, - startsAt: String, + status: String, + labels: AlertLabels, annotations: AlertAnnotations, - labels: HashMap, + #[serde(rename = "generatorURL")] + generator_url: String, } -#[derive(Deserialize, Clone, Debug)] +#[derive(Debug, Clone, Deserialize)] +struct AlertLabels { + alertname: Option, + host: Option, + instance: Option, +} + +#[derive(Debug, Clone, Deserialize)] struct AlertAnnotations { - summary: String, -} - -#[derive(Clone)] -struct AppState { - jabber: jabber::Handle, - alerts: Arc>>, + message: Option, + description: Option, } async fn alerts( - State(state): State, - Json(payload): Json>, + State(jabber): State, + Json(payload): Json, ) -> Response { - let mut message = String::new(); - let mut alerts = state.alerts.lock().unwrap(); - for alert in payload { - let is_old = if let Some(old_alert) = alerts.get(&alert.generatorURL) { - old_alert.startsAt == alert.startsAt - } else { - false - }; + let mut error_message = None; - if ! is_old { - if !message.is_empty() { - message += "\n"; + for alert in payload.alerts { + match alert.render() { + Ok(message) => { + jabber.send_message(message).await; + } + Err(e) => { + error_message = Some(format!("{}", e)); } - let title = match (alert.labels.get("alertname"), alert.labels.get("instance")) { - (Some(name), Some(instance)) => format!("{}@{}", name, instance), - _ => alert.annotations.summary.clone(), - }; - message += &format!("{}: {}", title, alert.generatorURL); - alerts.insert(alert.generatorURL.clone(), alert); } } - drop(alerts); - if !message.is_empty() { - let jabber = state.jabber.clone(); - tokio::spawn(async move { - jabber.send_message(message).await; - }); + match error_message { + None => + StatusCode::OK.into_response(), + Some(error_message) => + (StatusCode::INTERNAL_SERVER_ERROR, + error_message + ).into_response() } - - StatusCode::OK.into_response() } #[derive(Deserialize)] @@ -91,15 +86,11 @@ async fn main() { ).expect("read config") ).expect("parse config"); let jabber = jabber::run(config.jid, config.password, config.muc).await; - let state = AppState { - jabber, - alerts: Arc::new(Mutex::new(HashMap::new())), - }; // build our application with a route let app = Router::new() - .route("/api/v2/alerts", post(alerts)) - .with_state(state); + .route("/alert", post(alerts)) + .with_state(jabber); let addr = SocketAddr::from(([127, 0, 0, 1], config.listen_port)); tracing::debug!("listening on {}", addr); diff --git a/templates/alert.txt b/templates/alert.txt new file mode 100644 index 0000000..d013605 --- /dev/null +++ b/templates/alert.txt @@ -0,0 +1,11 @@ +{{ status|upper -}}: +{% if labels.alertname.as_ref().is_some() %}*{{ labels.alertname.as_ref().unwrap() }}*{% endif -%} +{% if labels.host.as_ref().is_some() -%} +at {{ labels.host.as_ref().unwrap() -}} +{% else if labels.instance.as_ref().is_some() -%} +at {{ labels.instance.as_ref().unwrap() }}{% endif -%} +{% if annotations.message.as_ref().is_some() %} +{{ annotations.message.as_ref().unwrap() }}{% endif %} +{% if annotations.description.as_ref().is_some() %} +{{ annotations.description.as_ref().unwrap() }}{% endif %} +Link: {{ generator_url }}