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(state: &Arc) -> Router> {
Router::new()
.route("/", get(index))
.nest("/posts", posts::router())
.merge(posts::alias_router(state.posts.values()))
.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();
}
}