diff --git a/.gitignore b/.gitignore index 1b9cd34..7c89042 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /venv .cargo /.idea +web/static/index.html +web/static/assets diff --git a/Cargo.lock b/Cargo.lock index c617b30..aac4a83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2051,6 +2051,27 @@ dependencies = [ "yansi", ] +[[package]] +name = "prometheus" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + [[package]] name = "quote" version = "1.0.33" @@ -2211,6 +2232,7 @@ dependencies = [ "lazy_static", "log", "oauth2", + "prometheus", "rand", "reqwest", "rocket", diff --git a/Cargo.toml b/Cargo.toml index 87e77ac..7197ea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ assets = [ ["conf/default.env", "etc/reminder-rs/config.env", "600"], ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"], ["web/static/**/*", "lib/reminder-rs/static", "644"], + ["reminder-dashboard/dist/static/**/*", "lib/reminder-rs/static", "644"], ["web/templates/**/*", "lib/reminder-rs/templates", "644"], ["healthcheck", "lib/reminder-rs/healthcheck", "755"], ["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"], diff --git a/web/Cargo.toml b/web/Cargo.toml index a274d4b..e10e8ef 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -19,3 +19,4 @@ lazy_static = "1.4.0" rand = "0.8" base64 = "0.13" csv = "1.2" +prometheus = "0.13.3" diff --git a/web/src/lib.rs b/web/src/lib.rs index 7e02265..b340464 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -6,6 +6,7 @@ mod consts; mod macros; mod catchers; mod guards; +mod metrics; mod routes; use std::{env, path::Path}; @@ -25,7 +26,10 @@ use serenity::{ }; use sqlx::{MySql, Pool}; -use crate::consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES}; +use crate::{ + consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES}, + metrics::{init_metrics, MetricProducer}, +}; type Database = MySql; @@ -64,7 +68,10 @@ pub async fn initialize( let static_path = if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" }; + init_metrics(); + rocket::build() + .attach(MetricProducer) .attach(Template::fairing()) .register( "/", @@ -85,12 +92,13 @@ pub async fn initialize( .mount( "/", routes![ - routes::index, routes::cookies, + routes::index, + routes::metrics::metrics, routes::privacy, - routes::terms, - routes::return_to_same_site, routes::report::report_error, + routes::return_to_same_site, + routes::terms, ], ) .mount( diff --git a/web/src/metrics.rs b/web/src/metrics.rs new file mode 100644 index 0000000..13754ae --- /dev/null +++ b/web/src/metrics.rs @@ -0,0 +1,43 @@ +use lazy_static::lazy_static; +use prometheus::{IntCounterVec, Opts, Registry}; +use rocket::{ + fairing::{Fairing, Info, Kind}, + Data, Request, Response, +}; + +lazy_static! { + pub static ref REGISTRY: Registry = Registry::new(); + static ref REQUEST_COUNTER: IntCounterVec = + IntCounterVec::new(Opts::new("requests", "Requests"), &["method", "route"]).unwrap(); + static ref RESPONSE_COUNTER: IntCounterVec = + IntCounterVec::new(Opts::new("responses", "Responses"), &["status", "route"]).unwrap(); +} + +pub fn init_metrics() { + REGISTRY.register(Box::new(REQUEST_COUNTER.clone())).unwrap(); +} + +pub struct MetricProducer; + +#[rocket::async_trait] +impl Fairing for MetricProducer { + fn info(&self) -> Info { + Info { name: "Metrics fairing", kind: Kind::Request } + } + + async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) { + if let Some(route) = req.route() { + REQUEST_COUNTER + .with_label_values(&[req.method().as_str(), &route.uri.to_string()]) + .inc(); + } + } + + async fn on_response<'r>(&self, req: &'r Request<'_>, resp: &mut Response<'r>) { + if let Some(route) = req.route() { + RESPONSE_COUNTER + .with_label_values(&[&resp.status().code.to_string(), &route.uri.to_string()]) + .inc(); + } + } +} diff --git a/web/src/routes/dashboard/api/guild/reminders.rs b/web/src/routes/dashboard/api/guild/reminders.rs index 1cb67d2..9b7c6e2 100644 --- a/web/src/routes/dashboard/api/guild/reminders.rs +++ b/web/src/routes/dashboard/api/guild/reminders.rs @@ -235,6 +235,21 @@ pub async fn edit_reminder( ]); } } + } else { + sqlx::query!( + " + UPDATE reminders + SET interval_seconds = NULL, interval_days = NULL, interval_months = NULL + WHERE uid = ? + ", + reminder.uid + ) + .execute(transaction.executor()) + .await + .map_err(|e| { + warn!("Error updating reminder interval: {:?}", e); + json!({ "reminder": Option::::None, "errors": vec!["Unknown error"] }) + })?; } if reminder.channel > 0 { diff --git a/web/src/routes/dashboard/guild.rs b/web/src/routes/dashboard/guild.rs deleted file mode 100644 index 8b13789..0000000 --- a/web/src/routes/dashboard/guild.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/web/src/routes/dashboard/mod.rs b/web/src/routes/dashboard/mod.rs index 06bd35d..7bc9243 100644 --- a/web/src/routes/dashboard/mod.rs +++ b/web/src/routes/dashboard/mod.rs @@ -1,9 +1,13 @@ -use std::collections::HashMap; +use std::path::Path; use chrono::{naive::NaiveDateTime, Utc}; use rand::{rngs::OsRng, seq::IteratorRandom}; -use rocket::{http::CookieJar, response::Redirect, serde::json::json}; -use rocket_dyn_templates::Template; +use rocket::{ + fs::{relative, NamedFile}, + http::CookieJar, + response::Redirect, + serde::json::json, +}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use serenity::{ client::Context, @@ -27,7 +31,6 @@ use crate::{ pub mod api; pub mod export; -pub mod guild; type Unset = Option; @@ -655,22 +658,26 @@ async fn create_database_channel( } #[get("/")] -pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result { +pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result { if cookies.get_private("userid").is_some() { - let mut map = HashMap::new(); - map.insert("version", env!("CARGO_PKG_VERSION")); - Ok(Template::render("dashboard", &map)) + NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| { + warn!("Couldn't render dashboard: {:?}", e); + + Redirect::to("/login/discord") + }) } else { Err(Redirect::to("/login/discord")) } } #[get("/<_..>")] -pub async fn dashboard(cookies: &CookieJar<'_>) -> Result { +pub async fn dashboard(cookies: &CookieJar<'_>) -> Result { if cookies.get_private("userid").is_some() { - let mut map = HashMap::new(); - map.insert("version", env!("CARGO_PKG_VERSION")); - Ok(Template::render("dashboard", &map)) + NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| { + warn!("Couldn't render dashboard: {:?}", e); + + Redirect::to("/login/discord") + }) } else { Err(Redirect::to("/login/discord")) } diff --git a/web/src/routes/metrics.rs b/web/src/routes/metrics.rs new file mode 100644 index 0000000..0421264 --- /dev/null +++ b/web/src/routes/metrics.rs @@ -0,0 +1,18 @@ +use prometheus; + +use crate::metrics::REGISTRY; + +#[get("/metrics")] +pub async fn metrics() -> String { + let encoder = prometheus::TextEncoder::new(); + let res_custom = encoder.encode_to_string(®ISTRY.gather()); + + match res_custom { + Ok(s) => s, + Err(e) => { + warn!("Error encoding metrics: {:?}", e); + + String::new() + } + } +} diff --git a/web/src/routes/mod.rs b/web/src/routes/mod.rs index b9c871e..568f5f3 100644 --- a/web/src/routes/mod.rs +++ b/web/src/routes/mod.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod dashboard; pub mod login; +pub mod metrics; pub mod report; use std::collections::HashMap; diff --git a/web/static/css/style.css b/web/static/css/style.css index 4fffab7..ecaacfc 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -218,17 +218,16 @@ div.inset-content { margin-right: 10%; } -div.flash-message { +div.flash-container { position: fixed; + width: 100%; + bottom: 0; +} + +div.flash-message { width: calc(100% - 32px); margin: 16px !important; z-index: 99; - bottom: 0; - display: none; -} - -div.flash-message.is-active { - display: block; } body { @@ -633,6 +632,10 @@ li.highlight { display: flex; } +.button-row-edit > button { + margin-right: 4px; +} + .button-row .button-row-reminder { flex-grow: 0; padding: 2px;