From a05128d215529422a7424f1265f53010620ace59 Mon Sep 17 00:00:00 2001 From: Jonni Liljamo Date: Mon, 24 Nov 2025 23:41:43 +0200 Subject: [PATCH] feat: alertmanager webhook route --- src/main.rs | 9 +++++ src/routes/alertmanager.rs | 77 ++++++++++++++++++++++++++++++++++++++ src/routes/mod.rs | 1 + 3 files changed, 87 insertions(+) create mode 100644 src/routes/alertmanager.rs diff --git a/src/main.rs b/src/main.rs index 53a98cf..1180a26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,6 +76,15 @@ async fn main() { move |headers, body| routes::message::message(shared_state, headers, body) }), ) + .route( + "/alertmanager/v4", + post({ + let shared_state = Arc::clone(&state); + move |headers, body| { + routes::alertmanager::alertmanager_v4(shared_state, headers, body) + } + }), + ) .layer(TraceLayer::new_for_http()) .with_state(state); diff --git a/src/routes/alertmanager.rs b/src/routes/alertmanager.rs new file mode 100644 index 0000000..a88a011 --- /dev/null +++ b/src/routes/alertmanager.rs @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 Jonni Liljamo + * + * This file is licensed under AGPL-3.0-or-later, see NOTICE and LICENSE for + * more information. + */ + +use std::{collections::HashMap, sync::Arc}; + +use axum::{Json, http::HeaderMap, response::IntoResponse}; +use reqwest::StatusCode; +use serde::Deserialize; + +use crate::{ + routes::{extract_token, message::MessageForm}, + state::State, +}; + +const ALERTMANAGER_JSON_VERSION_4: &str = "4"; + +#[derive(Debug, Deserialize)] +pub struct V4Alert { + annotations: HashMap, +} + +#[derive(Debug, Deserialize)] +pub struct V4Form { + version: String, + alerts: Vec, +} + +impl From for MessageForm { + fn from(value: V4Alert) -> Self { + MessageForm { + title: value + .annotations + .get("summary") + .cloned() + .unwrap_or("Summary N/A".into()), + message: value + .annotations + .get("description") + .cloned() + .unwrap_or("Description N/A".into()), + format_commonmark: false, + } + } +} + +pub async fn alertmanager_v4( + state: Arc, + headers: HeaderMap, + Json(message): Json, +) -> impl IntoResponse { + let token = extract_token!(headers).trim_start_matches("Bearer "); + + let notifier = match state.find_notifier(token) { + Some(n) => n, + None => return (StatusCode::UNAUTHORIZED, "unauthorized"), + }; + + if message.version != ALERTMANAGER_JSON_VERSION_4 { + return ( + StatusCode::BAD_REQUEST, + "wrong alertmanager json version, this API is for version 4", + ); + } + + for alert in message.alerts { + if let Err(err) = state.send_message(notifier, &alert.into()).await { + tracing::error!(?err, "message sending failed"); + return (StatusCode::INTERNAL_SERVER_ERROR, "failed to send message"); + }; + } + + (StatusCode::OK, "") +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index c22fbb6..63cded6 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -5,6 +5,7 @@ * more information. */ +pub mod alertmanager; pub mod message; macro_rules! extract_token { -- 2.44.1