1
0
Fork 0

Compare commits

...

4 commits

5 changed files with 142 additions and 125 deletions

45
Cargo.lock generated
View file

@ -74,7 +74,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.10",
"syn 2.0.11",
]
[[package]]
@ -409,7 +409,7 @@ dependencies = [
"proc-macro2",
"quote",
"scratch",
"syn 2.0.10",
"syn 2.0.11",
]
[[package]]
@ -426,7 +426,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.10",
"syn 2.0.11",
]
[[package]]
@ -600,9 +600,9 @@ dependencies = [
[[package]]
name = "generic-array"
version = "0.14.6"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
@ -1250,9 +1250,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.53"
version = "1.0.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73"
checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534"
dependencies = [
"unicode-ident",
]
@ -1425,29 +1425,29 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
[[package]]
name = "serde"
version = "1.0.158"
version = "1.0.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9"
checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.158"
version = "1.0.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad"
checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.10",
"syn 2.0.11",
]
[[package]]
name = "serde_json"
version = "1.0.94"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744"
dependencies = [
"itoa",
"ryu",
@ -1572,9 +1572,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.10"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40"
checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40"
dependencies = [
"proc-macro2",
"quote",
@ -1636,7 +1636,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.10",
"syn 2.0.11",
]
[[package]]
@ -1661,14 +1661,13 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.26.0"
version = "1.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
"mio",
"num_cpus",
"parking_lot",
@ -1681,13 +1680,13 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.8.2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.11",
]
[[package]]

View file

@ -1,42 +1,86 @@
use axum::{
body,
extract::State,
middleware::Next,
response::{Html, IntoResponse, Response},
Extension,
routing::get,
Router,
};
use hyper::StatusCode;
use hyper::{header::CONTENT_TYPE, Request, StatusCode};
use lazy_static::lazy_static;
use prometheus::{opts, IntCounterVec};
use prometheus::{opts, Encoder, IntCounterVec, TextEncoder};
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(state: &Arc<AppState>) -> Router<Arc<AppState>> {
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(
Extension(state): Extension<Arc<State>>,
) -> std::result::Result<Html<Vec<u8>>, WebsiteError> {
State(state): State<Arc<AppState>>,
) -> std::result::Result<Html<String>, 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<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 {
fn into_response(self) -> Response {
match self {

View file

@ -1,17 +1,20 @@
use std::sync::Arc;
use axum::{extract::Path, response::Html, routing::get, Extension, Router};
use axum::{
extract::{Path, State},
response::{Html, Redirect},
routing::get,
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<Arc<AppState>> {
Router::new()
.route("/", get(index))
.route("/:slug/", get(view))
@ -19,41 +22,51 @@ pub fn router() -> Router {
.fallback_service(tower_http::services::ServeDir::new("./posts"))
}
pub fn alias_router<'a>(posts: impl IntoIterator<Item = &'a Post>) -> Router<Arc<AppState>> {
let mut router = Router::new();
for post in posts {
for alias in &post.aliases {
let path = post.absolute_path.to_owned();
router = router.route(
alias,
get(move || async {
let path = path;
Redirect::permanent(&path)
}),
);
}
}
router
}
#[derive(Serialize, Debug)]
struct PageContext<'a> {
title: &'a str,
}
#[instrument(skip(state))]
pub async fn index(Extension(state): Extension<Arc<State>>) -> Result<Html<String>, WebsiteError> {
let mut posts: Vec<&Post> = state
.posts
.values()
.filter(|p| p.is_published())
.collect();
pub async fn index(State(state): State<Arc<AppState>>) -> Result<Html<String>, WebsiteError> {
let mut posts: Vec<&Post> = state.posts.values().filter(|p| p.is_published()).collect();
posts.sort_by_key(|p| &p.date);
posts.reverse();
let ctx = PageContext {
title: "Posts",
};
let ctx = PageContext { title: "Posts" };
let mut c = tera::Context::new();
c.insert("page", &ctx);
c.insert("posts", &posts);
let res = state.tera
.render("posts_index.html", &c)?;
let res = state.tera.render("posts_index.html", &c)?;
HIT_COUNTER.with_label_values(&["/posts/"]).inc();
Ok(Html(res))
}
#[instrument(skip(state))]
pub async fn view(
Path(slug): Path<String>,
Extension(state): Extension<Arc<State>>,
State(state): State<Arc<AppState>>,
) -> Result<Html<String>, WebsiteError> {
let post = state.posts.get(&slug).ok_or(WebsiteError::NotFound)?;
if !post.is_published() {
@ -63,9 +76,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))
}
@ -79,31 +89,26 @@ mod tests {
#[test]
fn render_index() {
let posts = vec![Post {
title: "test".into(),
slug: "test".into(),
date: Some(DateTime::parse_from_rfc3339("2023-03-26T13:04:01+02:00").unwrap()),
..Default::default()
},
Post {
title: "test2".into(),
slug: "test2".into(),
date: None,
..Default::default()
}];
let page = PageContext {
title: "Posts",
};
let posts = vec![
Post {
title: "test".into(),
slug: "test".into(),
date: Some(DateTime::parse_from_rfc3339("2023-03-26T13:04:01+02:00").unwrap()),
..Default::default()
},
Post {
title: "test2".into(),
slug: "test2".into(),
date: None,
..Default::default()
},
];
let page = PageContext { title: "Posts" };
let mut ctx = tera::Context::new();
ctx.insert("page", &page);
ctx.insert("posts", &posts);
let tera = tera::Tera::new("templates/**/*").unwrap();
let _res = tera
.render(
"posts_index.html",
&ctx,
)
.unwrap();
let _res = tera.render("posts_index.html", &ctx).unwrap();
}
}

View file

@ -1,18 +1,17 @@
use std::{collections::HashMap, sync::Arc};
use axum::{body, response::Response, routing::get, Extension, Router};
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<String, Post>,
tera: Tera,
}
@ -23,7 +22,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(&state)
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.with_state(state);
info!("Now listening at http://localhost:8180");
@ -34,45 +40,6 @@ async fn main() -> Result<()> {
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)]
pub enum WebsiteError {
NotFound,

View file

@ -9,14 +9,16 @@
<li>✅ template rendering (tera)</li>
<li>✅ markdown rendering (pulldown_cmark)</li>
<li>✅ post metadata (frontmatter, toml)</li>
<li>✅ app metrics</li>
<li>✅ app metrics (page hits, etc)</li>
<li>✅ tests</li>
<li> page aliases (redirects, for back-compat with old routes)</li>
<li>⬜ sass compilation (rsass? grass?)</li>
<li> page aliases (redirects, for back-compat with old routes)</li>
<li>⬜ sass compilation (using rsass? grass?)</li>
<li>⬜ rss/atom/jsonfeed</li>
<li>✅ proper error handling (i guess??)</li>
<li>⬜ other pages???</li>
<li>⬜ opentelemetry?</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>
{% endblock main %}