diff --git a/Cargo.lock b/Cargo.lock index cc98bd1..a045b3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,7 +74,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.11", + "syn 2.0.13", ] [[package]] @@ -375,6 +375,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -409,7 +428,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.11", + "syn 2.0.13", ] [[package]] @@ -426,7 +445,7 @@ checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.11", + "syn 2.0.13", ] [[package]] @@ -464,6 +483,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "deunicode" version = "0.4.3" @@ -538,9 +570,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -552,9 +584,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -562,35 +594,58 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] [[package]] name = "futures-io" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.13", +] [[package]] name = "futures-sink" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-core", + "futures-macro", "futures-sink", "futures-task", "pin-project-lite", @@ -968,6 +1023,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.7.0" @@ -1071,6 +1135,52 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +[[package]] +name = "opentelemetry" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d6c3d7288a106c0a363e4b0e8d308058d56902adefb16f4936f417ffef086e" +dependencies = [ + "opentelemetry_api", + "opentelemetry_sdk", +] + +[[package]] +name = "opentelemetry_api" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c24f96e21e7acc813c7a8394ee94978929db2bcc46cf6b5014fc612bf7760c22" +dependencies = [ + "fnv", + "futures-channel", + "futures-util", + "indexmap", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca41c4933371b61c2a2f214bf16931499af4ec90543604ec828f7a625c09113" +dependencies = [ + "async-trait", + "crossbeam-channel", + "dashmap", + "fnv", + "futures-channel", + "futures-executor", + "futures-util", + "once_cell", + "opentelemetry_api", + "percent-encoding", + "rand", + "thiserror", +] + [[package]] name = "overload" version = "0.1.1" @@ -1123,9 +1233,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cbd939b234e95d72bc393d51788aec68aeeb5d51e748ca08ff3aad58cb722f7" +checksum = "7b1403e8401ad5dedea73c626b99758535b342502f8d1e361f4a2dd952749122" dependencies = [ "thiserror", "ucd-trie", @@ -1133,9 +1243,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a81186863f3d0a27340815be8f2078dd8050b14cd71913db9fbda795e5f707d7" +checksum = "be99c4c1d2fc2769b1d00239431d711d08f6efedcecb8b6e30707160aee99c15" dependencies = [ "pest", "pest_generator", @@ -1143,22 +1253,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a1ef20bf3193c15ac345acb32e26b3dc3223aff4d77ae4fc5359567683796b" +checksum = "e56094789873daa36164de2e822b3888c6ae4b4f9da555a1103587658c805b1e" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.13", ] [[package]] name = "pest_meta" -version = "2.5.6" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e3b284b1f13a20dc5ebc90aff59a51b8d7137c221131b52a7260c08cbc1cc80" +checksum = "6733073c7cff3d8459fda0e42f13a047870242aed8b509fe98000928975f359e" dependencies = [ "once_cell", "pest", @@ -1250,9 +1360,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.54" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" +checksum = "1d0dd4be24fcdcfeaa12a432d588dc59bbad6cad3510c67e74a2b6b2fc950564" dependencies = [ "unicode-ident", ] @@ -1364,6 +1474,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.29" @@ -1440,7 +1559,7 @@ checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ "proc-macro2", "quote", - "syn 2.0.11", + "syn 2.0.13", ] [[package]] @@ -1572,9 +1691,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.11" +version = "2.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e3787bb71465627110e7d87ed4faaa36c1f61042ee67badb9e2ef173accc40" +checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" dependencies = [ "proc-macro2", "quote", @@ -1636,7 +1755,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.11", + "syn 2.0.13", ] [[package]] @@ -1686,7 +1805,7 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.11", + "syn 2.0.13", ] [[package]] @@ -1855,18 +1974,49 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ebb87a95ea13271332df069020513ab70bdb5637ca42d6e492dc3bbbad48de" +dependencies = [ + "once_cell", + "opentelemetry", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -2085,6 +2235,7 @@ dependencies = [ "glob", "hyper", "lazy_static", + "opentelemetry", "prometheus", "pulldown-cmark", "regex", @@ -2097,6 +2248,7 @@ dependencies = [ "tower", "tower-http", "tracing", + "tracing-opentelemetry", "tracing-subscriber", ] diff --git a/Cargo.toml b/Cargo.toml index c0b74c0..cf3464d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ color-eyre = "0.6.1" glob = "0.3.0" hyper = { version = "0.14.19", features = ["full"] } lazy_static = "1.4.0" +opentelemetry = { version = "0.18.0", features = ["metrics"] } prometheus = { version = "0.13.3", features = ["process"] } pulldown-cmark = "0.9.2" regex = "1.7.2" @@ -25,4 +26,5 @@ toml = "0.7.3" tower = { version = "0.4.12", features = ["full"] } tower-http = { version = "0.4.0", features = ["full"] } tracing = "0.1.35" -tracing-subscriber = { version = "0.3.11", features = ["fmt"] } +tracing-opentelemetry = "0.18.0" +tracing-subscriber = { version = "0.3.11", features = ["fmt", "env-filter", "json", "tracing-log"] } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index ddeddbd..53ae3b7 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -2,7 +2,7 @@ use axum::{ body, extract::State, middleware::Next, - response::{Html, IntoResponse, Response}, + response::{Html, IntoResponse, Response, Redirect}, routing::get, Router, }; @@ -10,7 +10,7 @@ use hyper::{header::CONTENT_TYPE, Request, StatusCode}; use lazy_static::lazy_static; use prometheus::{opts, Encoder, IntCounterVec, TextEncoder}; use std::sync::Arc; -use tracing::{instrument, log::*}; +use tracing::{info_span, instrument, log::*, Instrument}; use crate::{AppState, WebsiteError}; @@ -31,13 +31,14 @@ lazy_static! { pub fn routes(state: &Arc) -> Router> { Router::new() .route("/", get(index)) - .nest("/posts", posts::router()) - .nest("/tags", tags::router()) + .merge(posts::router()) + .merge(tags::router()) .merge(posts::alias_router(state.posts.values())) + .layer(axum::middleware::from_fn(metrics_middleware)) .route("/healthcheck", get(healthcheck)) .route("/metrics", get(metrics)) - .nest_service("/static", tower_http::services::ServeDir::new("./static")) - .layer(axum::middleware::from_fn(metrics_middleware)) + .route_service("/posts/:slug/*path", tower_http::services::ServeDir::new("./")) + .route_service("/static/*path", tower_http::services::ServeDir::new("./")) } #[instrument(skip(state))] @@ -127,8 +128,6 @@ mod tests { posts, }); - super::routes(&state) - .with_state(state) - .into_make_service(); + super::routes(&state).with_state(state).into_make_service(); } } diff --git a/src/handlers/posts.rs b/src/handlers/posts.rs index a0e0253..2c7e10a 100644 --- a/src/handlers/posts.rs +++ b/src/handlers/posts.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use axum::{ extract::{Path, State}, - response::{Html, Redirect}, + response::{Html, Redirect, IntoResponse}, routing::get, Router, }; @@ -16,10 +16,11 @@ use crate::{ pub fn router() -> Router> { Router::new() - .route("/", get(index)) - .route("/:slug/", get(view)) - .route("/:slug/index.md", get(super::not_found)) - .fallback_service(tower_http::services::ServeDir::new("./posts")) + .route("/posts", get(|| async { Redirect::permanent("/") })) + .route("/posts/", get(index)) + .route("/posts/:slug", get(redirect)) + .route("/posts/:slug/", get(view)) + .route("/posts/:slug/index.md", get(super::not_found)) } pub fn alias_router<'a>(posts: impl IntoIterator) -> Router> { @@ -79,6 +80,19 @@ pub async fn view( Ok(Html(res)) } +#[instrument(skip(state))] +pub async fn redirect( + Path(slug): Path, + State(state): State>, +) -> Result { + if state.posts.contains_key(&slug) { + Ok(Redirect::permanent(&format!("/posts/{slug}/"))) + } + else { + Err(WebsiteError::NotFound) + } +} + #[cfg(test)] mod tests { use chrono::DateTime; diff --git a/src/handlers/tags.rs b/src/handlers/tags.rs index d2ef15f..de250c4 100644 --- a/src/handlers/tags.rs +++ b/src/handlers/tags.rs @@ -1,15 +1,22 @@ use std::sync::Arc; -use axum::{Router, response::Html, routing::get, extract::{State, Path}}; +use axum::{ + extract::{Path, State}, + response::{Html, Redirect}, + routing::get, + Router, +}; use serde_derive::Serialize; use tracing::instrument; -use crate::{AppState, WebsiteError, post::Post}; +use crate::{post::Post, AppState, WebsiteError}; pub fn router() -> Router> { Router::new() - .route("/", get(index)) - .route("/:tag/", get(view)) + .route("/tags", get(|| async { Redirect::permanent("/") })) + .route("/tags/", get(index)) + .route("/tags/:tag", get(redirect)) + .route("/tags/:tag/", get(view)) } #[derive(Serialize, Debug)] @@ -19,7 +26,6 @@ struct PageContext<'a> { #[instrument(skip(state))] pub async fn index(State(state): State>) -> Result, WebsiteError> { - let tags: Vec<_> = state.tags.values().collect(); let ctx = PageContext { title: "Tags" }; @@ -33,8 +39,15 @@ pub async fn index(State(state): State>) -> Result, W } #[instrument(skip(state))] -pub async fn view(Path(tag): Path, State(state): State>) -> Result, WebsiteError> { - let mut posts: Vec<&Post> = state.posts.values().filter(|p| p.is_published() && p.tags.contains(&tag)).collect(); +pub async fn view( + Path(tag): Path, + State(state): State>, +) -> Result, WebsiteError> { + let mut posts: Vec<&Post> = state + .posts + .values() + .filter(|p| p.is_published() && p.tags.contains(&tag)) + .collect(); posts.sort_by_key(|p| &p.date); posts.reverse(); @@ -50,4 +63,17 @@ pub async fn view(Path(tag): Path, State(state): State>) - let res = state.tera.render("tag.html", &c)?; Ok(Html(res)) -} \ No newline at end of file +} + +#[instrument(skip(state))] +pub async fn redirect( + Path(slug): Path, + State(state): State>, +) -> Result { + if state.tags.contains_key(&slug) { + Ok(Redirect::permanent(&format!("/tags/{slug}/"))) + } + else { + Err(WebsiteError::NotFound) + } +} diff --git a/src/main.rs b/src/main.rs index 869a6e3..84506c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,16 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use axum::extract::MatchedPath; use color_eyre::eyre::{Error, Result}; +use hyper::{Body, Request, Response}; use post::Post; use tag::Tag; use tera::Tera; -use tower_http::{compression::CompressionLayer, trace::TraceLayer, cors::CorsLayer}; -use tracing::log::*; +use tower_http::{compression::CompressionLayer, cors::CorsLayer}; +use tracing::{info_span, log::*, Span}; +use tracing_subscriber::{prelude::*, EnvFilter}; mod handlers; mod post; @@ -22,7 +25,8 @@ pub struct AppState { #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; - tracing_subscriber::fmt::init(); + init_tracing(); + info!("Starting server..."); let tera = Tera::new("templates/**/*")?; @@ -32,8 +36,10 @@ async fn main() -> Result<()> { let app = handlers::routes(&state) .layer(CorsLayer::permissive()) - .layer(TraceLayer::new_for_http()) .layer(CompressionLayer::new()) + .layer(tower_http::trace::TraceLayer::new_for_http() + .make_span_with(make_span) + .on_response(on_response)) .with_state(state); info!("Now listening at http://localhost:8180"); @@ -45,6 +51,48 @@ async fn main() -> Result<()> { Ok(()) } +fn init_tracing() { + let filter = EnvFilter::builder() + .with_default_directive("into".parse().unwrap()) + .from_env_lossy() + .add_directive("otel=debug".parse().unwrap()); + + tracing_subscriber::registry() + .with(filter) + .with(tracing_subscriber::fmt::layer()) + .init(); +} + +fn make_span(request: &Request) -> Span { + let uri = request.uri(); + let route = request + .extensions() + .get::() + .map(|mp| mp.as_str()) + .unwrap_or_default(); + let method = request.method().as_str(); + let target = uri + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or_default(); + + let name = format!("{method} {route}"); + + use tracing::field::Empty; + info_span!( + "request", + otel.name = %name, + http.route = %route, + http.method = %method, + http.target = %target, + http.status_code = Empty + ) +} + +fn on_response(response: &Response, _latency: Duration, span: &Span) { + span.record("http.status_code", response.status().as_str()); +} + #[derive(Debug)] pub enum WebsiteError { NotFound, diff --git a/src/post.rs b/src/post.rs index 9fe9113..8ff15ff 100644 --- a/src/post.rs +++ b/src/post.rs @@ -112,7 +112,7 @@ pub async fn load_post(slug: &str) -> color_eyre::eyre::Result { )) } -#[instrument] +#[instrument(skip(src))] fn parse_frontmatter( src: String, ) -> color_eyre::eyre::Result<(Option, Option)> { diff --git a/src/tag.rs b/src/tag.rs index acd4111..0532b56 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -11,20 +11,22 @@ pub struct Tag { pub posts: Vec, } -pub fn get_tags<'a>(posts: impl IntoIterator) -> HashMap { - let mut tags: HashMap = HashMap::new(); +pub fn get_tags<'a>(posts: impl IntoIterator) -> HashMap { + let mut tags: HashMap = HashMap::new(); for post in posts.into_iter() { for key in &post.tags { if let Some(tag) = tags.get_mut(key) { tag.posts.push(post.slug.clone()); - } - else { - tags.insert(key.clone(), Tag { - slug: key.clone(), - absolute_path: format!("/tags/{key}/"), - posts: vec![post.slug.clone()] - }); + } else { + tags.insert( + key.clone(), + Tag { + slug: key.clone(), + absolute_path: format!("/tags/{key}/"), + posts: vec![post.slug.clone()], + }, + ); } } }