make metrics into middleware, use .with_state
This commit is contained in:
parent
11564832e7
commit
baa5cbc344
4 changed files with 67 additions and 68 deletions
|
@ -1,42 +1,77 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
response::{Html, IntoResponse, Response},
|
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 lazy_static::lazy_static;
|
||||||
use prometheus::{opts, IntCounterVec};
|
use prometheus::{opts, IntCounterVec, TextEncoder, Encoder};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::{instrument, log::*};
|
use tracing::{instrument, log::*};
|
||||||
|
|
||||||
use crate::{State, WebsiteError};
|
use crate::{AppState, WebsiteError};
|
||||||
|
|
||||||
pub mod posts;
|
pub mod posts;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref HIT_COUNTER: IntCounterVec = prometheus::register_int_counter_vec!(
|
pub static ref HIT_COUNTER: IntCounterVec = prometheus::register_int_counter_vec!(
|
||||||
opts!("page_hits", "Number of hits to various pages"),
|
opts!("http_requests_total", "Total amount of http requests received"),
|
||||||
&["page"]
|
&["route", "method", "status"]
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Router<Arc<AppState>> {
|
||||||
|
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))]
|
#[instrument(skip(state))]
|
||||||
pub async fn index(
|
pub async fn index(
|
||||||
Extension(state): Extension<Arc<State>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> std::result::Result<Html<Vec<u8>>, WebsiteError> {
|
) -> std::result::Result<Html<String>, WebsiteError> {
|
||||||
let ctx = tera::Context::new();
|
let ctx = tera::Context::new();
|
||||||
let res = state.tera.render("index.html", &ctx).map_err(|e| {
|
let res = state.tera.render("index.html", &ctx).map_err(|e| {
|
||||||
error!("Failed rendering index: {}", e);
|
error!("Failed rendering index: {}", e);
|
||||||
WebsiteError::NotFound
|
WebsiteError::NotFound
|
||||||
})?;
|
})?;
|
||||||
HIT_COUNTER.with_label_values(&["/"]).inc();
|
Ok(Html(res))
|
||||||
Ok(Html(res.into()))
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
pub async fn not_found() -> Response {
|
||||||
(StatusCode::NOT_FOUND, ()).into_response()
|
(StatusCode::NOT_FOUND, ()).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> 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 {
|
impl IntoResponse for WebsiteError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
use std::sync::Arc;
|
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 serde_derive::Serialize;
|
||||||
use tracing::{instrument, log::*};
|
use tracing::{instrument, log::*};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
post::{render_post, Post},
|
post::{render_post, Post},
|
||||||
State, WebsiteError,
|
AppState, WebsiteError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::HIT_COUNTER;
|
pub fn router() -> Router<Arc<AppState>> {
|
||||||
|
|
||||||
pub fn router() -> Router {
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.route("/:slug/", get(view))
|
.route("/:slug/", get(view))
|
||||||
|
@ -25,7 +23,7 @@ struct PageContext<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Html<String>, WebsiteError> {
|
pub async fn index(State(state): State<Arc<AppState>>) -> Result<Html<String>, WebsiteError> {
|
||||||
let mut posts: Vec<&Post> = state
|
let mut posts: Vec<&Post> = state
|
||||||
.posts
|
.posts
|
||||||
.values()
|
.values()
|
||||||
|
@ -46,14 +44,13 @@ pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Html<Strin
|
||||||
let res = state.tera
|
let res = state.tera
|
||||||
.render("posts_index.html", &c)?;
|
.render("posts_index.html", &c)?;
|
||||||
|
|
||||||
HIT_COUNTER.with_label_values(&["/posts/"]).inc();
|
|
||||||
Ok(Html(res))
|
Ok(Html(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(state))]
|
#[instrument(skip(state))]
|
||||||
pub async fn view(
|
pub async fn view(
|
||||||
Path(slug): Path<String>,
|
Path(slug): Path<String>,
|
||||||
Extension(state): Extension<Arc<State>>,
|
State(state): State<Arc<AppState>>,
|
||||||
) -> Result<Html<String>, WebsiteError> {
|
) -> Result<Html<String>, WebsiteError> {
|
||||||
let post = state.posts.get(&slug).ok_or(WebsiteError::NotFound)?;
|
let post = state.posts.get(&slug).ok_or(WebsiteError::NotFound)?;
|
||||||
if !post.is_published() {
|
if !post.is_published() {
|
||||||
|
@ -63,9 +60,6 @@ pub async fn view(
|
||||||
|
|
||||||
let res = render_post(&state.tera, post).await?;
|
let res = render_post(&state.tera, post).await?;
|
||||||
|
|
||||||
HIT_COUNTER
|
|
||||||
.with_label_values(&[&format!("/posts/{slug}/")])
|
|
||||||
.inc();
|
|
||||||
Ok(Html(res))
|
Ok(Html(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
54
src/main.rs
54
src/main.rs
|
@ -1,18 +1,18 @@
|
||||||
use std::{collections::HashMap, sync::Arc};
|
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 color_eyre::eyre::{Error, Result};
|
||||||
use hyper::header::CONTENT_TYPE;
|
use hyper::header::CONTENT_TYPE;
|
||||||
use post::Post;
|
use post::Post;
|
||||||
use prometheus::{Encoder, TextEncoder};
|
use prometheus::{Encoder, TextEncoder};
|
||||||
use tera::Tera;
|
use tera::Tera;
|
||||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||||
use tracing::{instrument, log::*};
|
use tracing::log::*;
|
||||||
|
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod post;
|
mod post;
|
||||||
|
|
||||||
pub struct State {
|
pub struct AppState {
|
||||||
posts: HashMap<String, Post>,
|
posts: HashMap<String, Post>,
|
||||||
tera: Tera,
|
tera: Tera,
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,14 @@ async fn main() -> Result<()> {
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
info!("Starting server...");
|
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");
|
info!("Now listening at http://localhost:8180");
|
||||||
|
|
||||||
|
@ -34,45 +41,6 @@ async fn main() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
|
||||||
pub async fn init_app() -> Result<Router> {
|
|
||||||
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)]
|
#[derive(Debug)]
|
||||||
pub enum WebsiteError {
|
pub enum WebsiteError {
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
|
@ -9,14 +9,16 @@
|
||||||
<li>✅ template rendering (tera)</li>
|
<li>✅ template rendering (tera)</li>
|
||||||
<li>✅ markdown rendering (pulldown_cmark)</li>
|
<li>✅ markdown rendering (pulldown_cmark)</li>
|
||||||
<li>✅ post metadata (frontmatter, toml)</li>
|
<li>✅ post metadata (frontmatter, toml)</li>
|
||||||
<li>✅ app metrics</li>
|
<li>✅ app metrics (page hits, etc)</li>
|
||||||
<li>✅ tests</li>
|
<li>✅ tests</li>
|
||||||
<li>⬜ page aliases (redirects, for back-compat with old routes)</li>
|
<li>⬜ page aliases (redirects, for back-compat with old routes)</li>
|
||||||
<li>⬜ sass compilation (rsass? grass?)</li>
|
<li>⬜ sass compilation (using rsass? grass?)</li>
|
||||||
<li>⬜ rss/atom/jsonfeed</li>
|
<li>⬜ rss/atom/jsonfeed</li>
|
||||||
<li>✅ proper error handling (i guess??)</li>
|
<li>✅ proper error handling (i guess??)</li>
|
||||||
<li>⬜ other pages???</li>
|
|
||||||
<li>⬜ opentelemetry?</li>
|
|
||||||
<li>⬜ fancy styling</li>
|
<li>⬜ fancy styling</li>
|
||||||
|
<li>⬜ other pages???</li>
|
||||||
|
<li>⬜ graphviz to svg rendering??</li>
|
||||||
|
<li>⬜ image processing?? (resizing, conversion)</li>
|
||||||
|
<li>⬜ opentelemetry?</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock main %}
|
{% endblock main %}
|
Loading…
Reference in a new issue