From baa5cbc344e2b7e723b7fcc5858c273c046d9061 Mon Sep 17 00:00:00 2001 From: Adrian Hedqvist Date: Wed, 29 Mar 2023 18:03:54 +0200 Subject: [PATCH] make metrics into middleware, use .with_state --- src/handlers/mod.rs | 55 +++++++++++++++++++++++++++++++++++-------- src/handlers/posts.rs | 16 ++++--------- src/main.rs | 54 +++++++++--------------------------------- templates/index.html | 10 ++++---- 4 files changed, 67 insertions(+), 68 deletions(-) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 4123959..e4fe2b2 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,42 +1,77 @@ use axum::{ response::{Html, IntoResponse, Response}, - Extension, + Router, routing::get, extract::State, middleware::Next, body, }; -use hyper::StatusCode; +use hyper::{StatusCode, Request, header::CONTENT_TYPE}; use lazy_static::lazy_static; -use prometheus::{opts, IntCounterVec}; +use prometheus::{opts, IntCounterVec, TextEncoder, Encoder}; use std::sync::Arc; use tracing::{instrument, log::*}; -use crate::{State, WebsiteError}; +use crate::{AppState, WebsiteError}; pub mod posts; lazy_static! { pub static ref HIT_COUNTER: IntCounterVec = prometheus::register_int_counter_vec!( - opts!("page_hits", "Number of hits to various pages"), - &["page"] + 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( - Extension(state): Extension>, -) -> std::result::Result>, WebsiteError> { + 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 })?; - HIT_COUNTER.with_label_values(&["/"]).inc(); - Ok(Html(res.into())) + 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 { diff --git a/src/handlers/posts.rs b/src/handlers/posts.rs index d476d8d..c49c8bf 100644 --- a/src/handlers/posts.rs +++ b/src/handlers/posts.rs @@ -1,17 +1,15 @@ use std::sync::Arc; -use axum::{extract::Path, response::Html, routing::get, Extension, Router}; +use axum::{extract::{Path, State}, response::Html, routing::get, Extension, Router}; use serde_derive::Serialize; use tracing::{instrument, log::*}; use crate::{ post::{render_post, Post}, - State, WebsiteError, + AppState, WebsiteError, }; -use super::HIT_COUNTER; - -pub fn router() -> Router { +pub fn router() -> Router> { Router::new() .route("/", get(index)) .route("/:slug/", get(view)) @@ -25,7 +23,7 @@ struct PageContext<'a> { } #[instrument(skip(state))] -pub async fn index(Extension(state): Extension>) -> Result, WebsiteError> { +pub async fn index(State(state): State>) -> Result, WebsiteError> { let mut posts: Vec<&Post> = state .posts .values() @@ -46,14 +44,13 @@ pub async fn index(Extension(state): Extension>) -> Result, - Extension(state): Extension>, + State(state): State>, ) -> Result, WebsiteError> { let post = state.posts.get(&slug).ok_or(WebsiteError::NotFound)?; if !post.is_published() { @@ -63,9 +60,6 @@ pub async fn view( let res = render_post(&state.tera, post).await?; - HIT_COUNTER - .with_label_values(&[&format!("/posts/{slug}/")]) - .inc(); Ok(Html(res)) } diff --git a/src/main.rs b/src/main.rs index eb22655..e7fbbef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,18 @@ use std::{collections::HashMap, sync::Arc}; -use axum::{body, response::Response, routing::get, Extension, Router}; +use axum::{body, response::Response, routing::get, Extension, Router, extract::State}; use color_eyre::eyre::{Error, Result}; use hyper::header::CONTENT_TYPE; use post::Post; use prometheus::{Encoder, TextEncoder}; use tera::Tera; use tower_http::{compression::CompressionLayer, trace::TraceLayer}; -use tracing::{instrument, log::*}; +use tracing::log::*; mod handlers; mod post; -pub struct State { +pub struct AppState { posts: HashMap, tera: Tera, } @@ -23,7 +23,14 @@ async fn main() -> Result<()> { tracing_subscriber::fmt::init(); info!("Starting server..."); - let app = init_app().await?; + let tera = Tera::new("templates/**/*")?; + let posts = post::load_all().await?; + let state = Arc::new(AppState { tera, posts }); + + let app = handlers::routes() + .layer(TraceLayer::new_for_http()) + .layer(CompressionLayer::new()) + .with_state(state); info!("Now listening at http://localhost:8180"); @@ -34,45 +41,6 @@ async fn main() -> Result<()> { Ok(()) } -#[instrument] -pub async fn init_app() -> Result { - let tera = Tera::new("templates/**/*")?; - let posts = post::load_all().await?; - - let state = Arc::new(State { tera, posts }); - - let middleware = tower::ServiceBuilder::new() - .layer(TraceLayer::new_for_http()) - .layer(Extension(state)) - .layer(CompressionLayer::new()); - - let app = Router::new() - .route("/", get(handlers::index)) - .nest("/posts", handlers::posts::router()) - .nest_service("/static", tower_http::services::ServeDir::new("./static")) - .route("/healthcheck", get(healthcheck)) - .route("/metrics", get(metrics)) - .layer(middleware); - - Ok(app) -} - -async fn healthcheck() -> &'static str { - "OK" -} - -async fn metrics() -> Response { - 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() -} - #[derive(Debug)] pub enum WebsiteError { NotFound, diff --git a/templates/index.html b/templates/index.html index 6c76c31..d6b3d4c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,14 +9,16 @@
  • ✅ template rendering (tera)
  • ✅ markdown rendering (pulldown_cmark)
  • ✅ post metadata (frontmatter, toml)
  • -
  • ✅ app metrics
  • +
  • ✅ app metrics (page hits, etc)
  • ✅ tests
  • ⬜ page aliases (redirects, for back-compat with old routes)
  • -
  • ⬜ sass compilation (rsass? grass?)
  • +
  • ⬜ sass compilation (using rsass? grass?)
  • ⬜ rss/atom/jsonfeed
  • ✅ proper error handling (i guess??)
  • -
  • ⬜ other pages???
  • -
  • ⬜ opentelemetry?
  • ⬜ fancy styling
  • +
  • ⬜ other pages???
  • +
  • ⬜ graphviz to svg rendering??
  • +
  • ⬜ image processing?? (resizing, conversion)
  • +
  • ⬜ opentelemetry?
  • {% endblock main %} \ No newline at end of file