use axum::{ response::{Html, IntoResponse, Response}, Router, routing::get, extract::State, middleware::Next, body, }; use hyper::{StatusCode, Request, header::CONTENT_TYPE}; use lazy_static::lazy_static; use prometheus::{opts, IntCounterVec, TextEncoder, Encoder}; use std::sync::Arc; use tracing::{instrument, log::*}; use crate::{AppState, WebsiteError}; pub mod posts; lazy_static! { pub static ref HIT_COUNTER: IntCounterVec = prometheus::register_int_counter_vec!( opts!("http_requests_total", "Total amount of http requests received"), &["route", "method", "status"] ) .unwrap(); } pub fn routes() -> Router> { Router::new() .route("/", get(index)) .nest("/posts", posts::router()) .route("/healthcheck", get(healthcheck)) .route("/metrics", get(metrics)) .nest_service("/static", tower_http::services::ServeDir::new("./static")) .layer(axum::middleware::from_fn(metrics_middleware)) } #[instrument(skip(state))] pub async fn index( State(state): State>, ) -> std::result::Result, WebsiteError> { let ctx = tera::Context::new(); let res = state.tera.render("index.html", &ctx).map_err(|e| { error!("Failed rendering index: {}", e); WebsiteError::NotFound })?; Ok(Html(res)) } async fn healthcheck() -> &'static str { "OK" } async fn metrics() -> impl IntoResponse { let encoder = TextEncoder::new(); let metric_families = prometheus::gather(); let mut buffer = vec![]; encoder.encode(&metric_families, &mut buffer).unwrap(); Response::builder() .status(200) .header(CONTENT_TYPE, encoder.format_type()) .body(body::boxed(body::Full::from(buffer))) .unwrap() } pub async fn not_found() -> Response { (StatusCode::NOT_FOUND, ()).into_response() } pub async fn metrics_middleware(request: Request, next: Next) -> Response { let path = request.uri().path().to_string(); let method = request.method().to_string(); let response = next.run(request).await; HIT_COUNTER.with_label_values(&[&path, &method, response.status().as_str()]).inc(); response } impl IntoResponse for WebsiteError { fn into_response(self) -> Response { match self { WebsiteError::NotFound => { info!("not found"); (StatusCode::NOT_FOUND, ()).into_response() } WebsiteError::InternalError(e) => { error!("internal error: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, ()).into_response() } } } } #[cfg(test)] mod tests { #[test] fn render_index() { let tera = tera::Tera::new("templates/**/*").unwrap(); let ctx = tera::Context::new(); let _res = tera.render("index.html", &ctx).unwrap(); } }