Compare commits
4 commits
c7ff1a75b2
...
0808fc8099
Author | SHA1 | Date | |
---|---|---|---|
Adrian Hedqvist | 0808fc8099 | ||
Adrian Hedqvist | 6be5525bae | ||
Adrian Hedqvist | baa5cbc344 | ||
Adrian Hedqvist | 11564832e7 |
45
Cargo.lock
generated
45
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
57
src/main.rs
57
src/main.rs
|
@ -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,
|
||||
|
|
|
@ -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 %}
|
Loading…
Reference in a new issue