1
0
Fork 0
This commit is contained in:
Adrian Hedqvist 2024-04-17 19:12:59 +02:00
parent 82e377dbea
commit d8050d2e89
13 changed files with 639 additions and 648 deletions

905
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,16 +6,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
axum = { version = "0.6.20", features = ["http2", "original-uri", "tracing"] } anyhow = { version = "1.0.82", features = ["backtrace"] }
cached = "0.46.1" axum = { version = "0.7.5", features = ["http2", "original-uri", "tracing"] }
cached = "0.49.3"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
color-eyre = "0.6.1" config = "0.14.0"
config = "0.13.3"
glob = "0.3.0" glob = "0.3.0"
opentelemetry = { version = "0.21.0", features = ["metrics"] } opentelemetry = { version = "0.22.0", features = ["trace", "metrics"] }
opentelemetry-jaeger = { version = "0.20.0", features = ["collector_client", "isahc_collector_client"]} opentelemetry-otlp = { version = "0.15.0", features = ["trace", "metrics"] }
opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio", "trace", "metrics"] }
prometheus = { version = "0.13.3", features = ["process"] } prometheus = { version = "0.13.3", features = ["process"] }
pulldown-cmark = "0.9.2" pulldown-cmark = "0.10.2"
regex = "1.7.2" regex = "1.7.2"
serde = "1.0.144" serde = "1.0.144"
serde_derive = "1.0.144" serde_derive = "1.0.144"
@ -25,7 +26,7 @@ tera = { version = "1.19.1", features = ["builtins"] }
tokio = { version = "1.34.0", features = ["full", "tracing"] } tokio = { version = "1.34.0", features = ["full", "tracing"] }
toml = "0.8.8" toml = "0.8.8"
tower = { version = "0.4.13", features = ["full"] } tower = { version = "0.4.13", features = ["full"] }
tower-http = { version = "0.4.4", features = ["full"] } tower-http = { version = "0.5.2", features = ["full"] }
tracing = "0.1.35" tracing = "0.1.35"
tracing-opentelemetry = "0.22.0" tracing-opentelemetry = "0.23.0"
tracing-subscriber = { version = "0.3.17", features = ["fmt", "env-filter", "json", "tracing-log"] } tracing-subscriber = { version = "0.3.17", features = ["fmt", "env-filter", "json", "tracing-log"] }

18
fly.toml Normal file
View file

@ -0,0 +1,18 @@
# fly.toml app configuration file generated for cool-glade-6208 on 2023-06-24T10:46:20+02:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#
app = "cool-glade-6208"
primary_region = "arn"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
[metrics]
port = 8180
path = "/metrics"

View file

@ -1,4 +1,4 @@
use color_eyre::Result; use anyhow::Result;
use serde_derive::Serialize; use serde_derive::Serialize;
use tracing::instrument; use tracing::instrument;
@ -16,7 +16,11 @@ struct FeedContext<'a> {
#[instrument(skip(state))] #[instrument(skip(state))]
pub fn render_atom_feed(state: &AppState) -> Result<String> { pub fn render_atom_feed(state: &AppState) -> Result<String> {
let mut posts: Vec<_> = state.posts.values().filter(|p| !p.draft && p.is_published()).collect(); let mut posts: Vec<_> = state
.posts
.values()
.filter(|p| !p.draft && p.is_published())
.collect();
posts.sort_by_key(|p| &p.date); posts.sort_by_key(|p| &p.date);
posts.reverse(); posts.reverse();

View file

@ -1,7 +1,7 @@
use axum::{ use axum::{
body, body::Body,
extract::State, extract::{Request, State},
http::{header, HeaderMap, Request, StatusCode}, http::{header, HeaderMap, StatusCode},
middleware::Next, middleware::Next,
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
routing::get, routing::get,
@ -14,7 +14,7 @@ use std::sync::Arc;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tracing::{ use tracing::{
instrument, instrument,
log::{error, info, debug}, log::{debug, error, info},
}; };
use crate::{AppState, WebsiteError}; use crate::{AppState, WebsiteError};
@ -22,14 +22,16 @@ use crate::{AppState, WebsiteError};
pub mod posts; pub mod posts;
pub mod tags; pub mod tags;
pub static HIT_COUNTER: Lazy<IntCounterVec> = Lazy::new(|| prometheus::register_int_counter_vec!( pub static HIT_COUNTER: Lazy<IntCounterVec> = Lazy::new(|| {
prometheus::register_int_counter_vec!(
opts!( opts!(
"http_requests_total", "http_requests_total",
"Total amount of http requests received" "Total amount of http requests received"
), ),
&["route", "method", "status"] &["route", "method", "status"]
) )
.unwrap()); .unwrap()
});
pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> { pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
Router::new() Router::new()
@ -38,14 +40,8 @@ pub fn routes(state: &Arc<AppState>) -> Router<Arc<AppState>> {
.merge(tags::router()) .merge(tags::router())
.merge(posts::alias_router(state.posts.values())) .merge(posts::alias_router(state.posts.values()))
.route("/healthcheck", get(healthcheck)) .route("/healthcheck", get(healthcheck))
.route_service( .route_service("/posts/:slug/*path", ServeDir::new("./"))
"/posts/:slug/*path", .route_service("/static/*path", ServeDir::new("./"))
ServeDir::new("./"),
)
.route_service(
"/static/*path",
ServeDir::new("./"),
)
.layer(axum::middleware::from_fn(metrics_middleware)) .layer(axum::middleware::from_fn(metrics_middleware))
.route("/metrics", get(metrics)) .route("/metrics", get(metrics))
} }
@ -86,7 +82,7 @@ async fn metrics() -> impl IntoResponse {
Response::builder() Response::builder()
.status(200) .status(200)
.header(header::CONTENT_TYPE, encoder.format_type()) .header(header::CONTENT_TYPE, encoder.format_type())
.body(body::boxed(body::Full::from(buffer))) .body(Body::from(buffer))
.unwrap() .unwrap()
} }
@ -95,7 +91,7 @@ pub async fn not_found() -> impl IntoResponse {
} }
#[instrument(skip(request, next))] #[instrument(skip(request, next))]
pub async fn metrics_middleware<B>(request: Request<B>, next: Next<B>) -> Response { pub async fn metrics_middleware(request: Request, next: Next) -> Response {
let path = request.uri().path().to_string(); let path = request.uri().path().to_string();
let method = request.method().clone(); let method = request.method().clone();

View file

@ -37,8 +37,8 @@ pub fn alias_router<'a>(posts: impl IntoIterator<Item = &'a Post>) -> Router<Arc
router = router.route( router = router.route(
alias, alias,
get(move || async { get(move || async {
let path = path; let p = path;
Redirect::permanent(&path) Redirect::permanent(&p)
}), }),
); );
} }
@ -56,7 +56,11 @@ pub async fn index(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<Response, WebsiteError> { ) -> Result<Response, WebsiteError> {
let mut posts: Vec<&Post> = state.posts.values().filter(|p| !p.draft && p.is_published()).collect(); let mut posts: Vec<&Post> = state
.posts
.values()
.filter(|p| !p.draft && p.is_published())
.collect();
let last_changed = posts.iter().filter_map(|p| p.last_modified()).max(); let last_changed = posts.iter().filter_map(|p| p.last_modified()).max();
@ -76,15 +80,11 @@ pub async fn index(
let res = state.tera.render("posts_index.html", &c)?; let res = state.tera.render("posts_index.html", &c)?;
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
[( [(
header::LAST_MODIFIED, header::LAST_MODIFIED,
last_changed.map_or_else( last_changed.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822()),
|| state.startup_time.to_rfc2822(),
|d| d.to_rfc2822(),
),
)], )],
Html(res), Html(res),
) )
@ -116,10 +116,7 @@ pub async fn view(
StatusCode::OK, StatusCode::OK,
[( [(
header::LAST_MODIFIED, header::LAST_MODIFIED,
last_changed.map_or_else( last_changed.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822()),
|| state.startup_time.to_rfc2822(),
|d| d.to_rfc2822(),
),
)], )],
Html(res), Html(res),
) )
@ -131,7 +128,11 @@ pub async fn feed(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
headers: HeaderMap, headers: HeaderMap,
) -> Result<Response, WebsiteError> { ) -> Result<Response, WebsiteError> {
let mut posts: Vec<&Post> = state.posts.values().filter(|p| !p.draft && p.is_published()).collect(); let mut posts: Vec<&Post> = state
.posts
.values()
.filter(|p| !p.draft && p.is_published())
.collect();
let last_changed = posts.iter().filter_map(|p| p.last_modified()).max(); let last_changed = posts.iter().filter_map(|p| p.last_modified()).max();
@ -149,10 +150,7 @@ pub async fn feed(
(header::CONTENT_TYPE, "application/atom+xml"), (header::CONTENT_TYPE, "application/atom+xml"),
( (
header::LAST_MODIFIED, header::LAST_MODIFIED,
&last_changed.map_or_else( &last_changed.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822()),
|| state.startup_time.to_rfc2822(),
|d| d.to_rfc2822(),
),
), ),
], ],
crate::feed::render_atom_feed(&state)?, crate::feed::render_atom_feed(&state)?,

View file

@ -85,10 +85,7 @@ pub async fn view(
StatusCode::OK, StatusCode::OK,
[( [(
header::LAST_MODIFIED, header::LAST_MODIFIED,
&last_changed.map_or_else( &last_changed.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822()),
|| state.startup_time.to_rfc2822(),
|d| d.to_rfc2822(),
),
)], )],
Html(res), Html(res),
) )
@ -125,10 +122,7 @@ pub async fn feed(
(header::CONTENT_TYPE, "application/atom+xml"), (header::CONTENT_TYPE, "application/atom+xml"),
( (
header::LAST_MODIFIED, header::LAST_MODIFIED,
&last_changed.map_or_else( &last_changed.map_or_else(|| state.startup_time.to_rfc2822(), |d| d.to_rfc2822()),
|| state.startup_time.to_rfc2822(),
|d| d.to_rfc2822(),
),
), ),
], ],
crate::feed::render_atom_tag_feed(tag, &state)?, crate::feed::render_atom_tag_feed(tag, &state)?,

View file

@ -36,23 +36,32 @@ pub fn uri_with_path(uri: &Uri, path: &str) -> Uri {
mod tests { mod tests {
use axum::http::Uri; use axum::http::Uri;
use crate::helpers::uri_with_path; use super::*;
#[test] #[test]
fn uri_with_relative_path() { fn uri_with_relative_path() {
let uri: Uri = "http://localhost/baba/".parse().unwrap(); let uri: Uri = "http://localhost/baba/".parse().unwrap();
assert_eq!(uri_with_path(&uri, "is/you").to_string(), "http://localhost/baba/is/you"); assert_eq!(
uri_with_path(&uri, "is/you").to_string(),
"http://localhost/baba/is/you"
);
} }
#[test] #[test]
fn uri_with_absolute_path() { fn uri_with_absolute_path() {
let uri: Uri = "http://localhost/baba/is/you".parse().unwrap(); let uri: Uri = "http://localhost/baba/is/you".parse().unwrap();
assert_eq!(uri_with_path(&uri, "/keke/is/move").to_string(), "http://localhost/keke/is/move"); assert_eq!(
uri_with_path(&uri, "/keke/is/move").to_string(),
"http://localhost/keke/is/move"
);
} }
#[test] #[test]
fn uri_with_index_relative_path() { fn uri_with_index_relative_path() {
let uri: Uri = "http://localhost/baba/is/you".parse().unwrap(); let uri: Uri = "http://localhost/baba/is/you".parse().unwrap();
assert_eq!(uri_with_path(&uri, "happy").to_string(), "http://localhost/baba/is/happy"); assert_eq!(
uri_with_path(&uri, "happy/and/you").to_string(),
"http://localhost/baba/is/happy/and/you"
);
} }
} }

View file

@ -2,7 +2,7 @@ use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
use tracing::{error, instrument}; use tracing::{error, instrument};
#[instrument(skip(content, lang, theme))] #[instrument(skip(content, lang, theme))]
pub fn hilight(content: &str, lang: &str, theme: Option<&str>) -> color_eyre::Result<String> { pub fn hilight(content: &str, lang: &str, theme: Option<&str>) -> anyhow::Result<String> {
let ss = SyntaxSet::load_defaults_newlines(); let ss = SyntaxSet::load_defaults_newlines();
let s = ss let s = ss
.find_syntax_by_extension(lang) .find_syntax_by_extension(lang)

View file

@ -1,31 +1,26 @@
#![warn(clippy::pedantic)] #![warn(clippy::pedantic)]
#![allow(clippy::unused_async)] // axum handlers needs async, even if no awaiting happens use std::{collections::HashMap, fmt::Display, sync::Arc};
use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration};
use axum::{ use axum::http::Uri;
body::Body,
extract::{MatchedPath, OriginalUri},
http::{uri::PathAndQuery, Request, Uri},
response::Response,
};
use chrono::DateTime; use chrono::DateTime;
use color_eyre::eyre::{Error, Result};
use config::Config; use config::Config;
use post::Post; use post::Post;
use tag::Tag; use tag::Tag;
use tera::Tera; use tera::Tera;
use tower_http::{compression::CompressionLayer, cors::CorsLayer};
use tracing::{field::Empty, info_span, log::info, Span, instrument};
use tracing_subscriber::{prelude::*, EnvFilter}; use tower_http::{compression::CompressionLayer, cors::CorsLayer};
use tracing::{instrument, log::info};
use anyhow::{Error, Result};
mod feed; mod feed;
mod handlers; mod handlers;
mod helpers; mod helpers;
mod hilighting; mod hilighting;
mod markdown; mod markdown;
mod observability;
mod post; mod post;
mod tag; mod tag;
@ -40,28 +35,24 @@ pub struct AppState {
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
color_eyre::install()?;
let cfg = Config::builder() let cfg = Config::builder()
.add_source(config::File::with_name("config.toml")) .add_source(config::File::with_name("config.toml"))
.add_source(config::Environment::with_prefix("WEBSITE")) .add_source(config::Environment::with_prefix("WEBSITE"))
.build()?; .build()?;
init_tracing(&cfg); observability::init_tracing(&cfg);
info!("Starting server..."); info!("Starting server...");
let addr = cfg.get_string("bind_address")?;
let app = init_app(&cfg).await?; let app = init_app(&cfg).await?;
axum::Server::bind(&cfg.get_string("bind_address")?.parse().unwrap()) let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
.serve(app.into_make_service()) axum::serve(listener, app.into_make_service()).await?;
.await?;
Ok(()) Ok(())
} }
#[instrument] #[instrument]
async fn init_app(cfg: &Config) -> Result<axum::routing::Router> { async fn init_app(cfg: &Config) -> Result<axum::routing::Router> {
let base_url: Uri = cfg.get_string("base_url")? let base_url: Uri = cfg.get_string("base_url")?.parse().unwrap();
.parse()
.unwrap();
let tera = Tera::new("templates/**/*")?; let tera = Tera::new("templates/**/*")?;
let mut state = AppState { let mut state = AppState {
@ -83,69 +74,12 @@ async fn init_app(cfg: &Config) -> Result<axum::routing::Router> {
.layer(CompressionLayer::new()) .layer(CompressionLayer::new())
.layer( .layer(
tower_http::trace::TraceLayer::new_for_http() tower_http::trace::TraceLayer::new_for_http()
.make_span_with(make_span) .make_span_with(observability::make_span)
.on_response(on_response), .on_response(observability::on_response),
) )
.with_state(state.clone())) .with_state(state.clone()))
} }
fn init_tracing(cfg: &Config) {
let filter = if let Ok(filter) = cfg.get_string("logging") {
EnvFilter::builder()
.with_default_directive("info".parse().unwrap())
.parse_lossy(filter)
} else {
EnvFilter::builder()
.with_default_directive("info".parse().unwrap())
.from_env_lossy()
};
opentelemetry::global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new());
let tracer = opentelemetry_jaeger::new_agent_pipeline()
.with_service_name("website")
.install_simple().unwrap();
let otel = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(filter)
.with(otel)
.with(tracing_subscriber::fmt::layer())
.init();
}
fn make_span(request: &Request<Body>) -> Span {
let uri = if let Some(OriginalUri(uri)) = request.extensions().get::<OriginalUri>() {
uri
} else {
request.uri()
};
let route = request
.extensions()
.get::<MatchedPath>()
.map_or(uri.path(), axum::extract::MatchedPath::as_str);
let method = request.method().as_str();
let target = uri
.path_and_query()
.map_or(uri.path(), PathAndQuery::as_str);
let name = format!("{method} {route}");
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)] #[derive(Debug)]
pub enum WebsiteError { pub enum WebsiteError {
NotFound, NotFound,
@ -176,8 +110,8 @@ impl From<tera::Error> for WebsiteError {
} }
} }
impl From<color_eyre::Report> for WebsiteError { impl From<Error> for WebsiteError {
fn from(value: color_eyre::Report) -> Self { fn from(value: Error) -> Self {
WebsiteError::InternalError(value) WebsiteError::InternalError(value)
} }
} }

View file

@ -2,8 +2,10 @@ use crate::helpers;
use crate::hilighting; use crate::hilighting;
use axum::http::Uri; use axum::http::Uri;
use cached::once_cell::sync::Lazy; use cached::once_cell::sync::Lazy;
use pulldown_cmark::CodeBlockKind;
use pulldown_cmark::Event; use pulldown_cmark::Event;
use pulldown_cmark::Tag; use pulldown_cmark::Tag;
use pulldown_cmark::TagEnd;
use pulldown_cmark::{Options, Parser}; use pulldown_cmark::{Options, Parser};
use regex::Regex; use regex::Regex;
use tracing::instrument; use tracing::instrument;
@ -24,46 +26,65 @@ pub fn render_markdown_to_html(base_uri: Option<&Uri>, markdown: &str) -> String
let mut content_html = String::new(); let mut content_html = String::new();
let parser = Parser::new_ext(markdown, opt); let parser = Parser::new_ext(markdown, opt);
let mut code_block = false;
let mut code_lang = None; let mut code_lang = None;
let mut code_accumulator = String::new(); let mut code_accumulator = String::new();
let mut events = Vec::new(); let mut events = Vec::new();
for event in parser { for event in parser {
match event { match event {
Event::Text(text) => { Event::Text(text) => {
if code_block { if code_lang.is_some() {
code_accumulator.push_str(&text); code_accumulator.push_str(&text);
} else { } else {
events.push(Event::Text(text)); events.push(Event::Text(text));
} }
} }
Event::Start(Tag::Link(t, mut link, title)) => { Event::Start(Tag::Link {
mut dest_url,
link_type,
title,
id,
}) => {
if let Some(uri) = base_uri { if let Some(uri) = base_uri {
if !link.starts_with('#') && !STARTS_WITH_SCHEMA_RE.is_match(&link) && !EMAIL_RE.is_match(&link) { if !dest_url.starts_with('#')
&& !STARTS_WITH_SCHEMA_RE.is_match(&dest_url)
&& !EMAIL_RE.is_match(&dest_url)
{
// convert relative URIs to absolute URIs // convert relative URIs to absolute URIs
link = helpers::uri_with_path(uri, &link).to_string().into(); dest_url = helpers::uri_with_path(uri, &dest_url).to_string().into();
} }
} }
events.push(Event::Start(Tag::Link(t, link, title))); events.push(Event::Start(Tag::Link {
dest_url,
link_type,
title,
id,
}));
} }
Event::Start(Tag::Image(t, mut link, title)) => { Event::Start(Tag::Image {
link_type,
mut dest_url,
title,
id,
}) => {
if let Some(uri) = base_uri { if let Some(uri) = base_uri {
if !link.contains("://") && !link.contains('@') { if !dest_url.contains("://") && !dest_url.contains('@') {
// convert relative URIs to absolute URIs // convert relative URIs to absolute URIs
link = helpers::uri_with_path(uri, &link).to_string().into(); dest_url = helpers::uri_with_path(uri, &dest_url).to_string().into();
} }
} }
events.push(Event::Start(Tag::Image(t, link, title))); events.push(Event::Start(Tag::Image {
link_type,
dest_url,
title,
id,
}));
} }
Event::Start(Tag::CodeBlock(kind)) => { Event::Start(Tag::CodeBlock(kind)) => {
code_block = true; if let CodeBlockKind::Fenced(lang) = kind {
if let pulldown_cmark::CodeBlockKind::Fenced(lang) = kind {
code_lang = Some(lang); code_lang = Some(lang);
} }
} }
Event::End(Tag::CodeBlock(_)) => { Event::End(TagEnd::CodeBlock) => {
code_block = false;
let lang = code_lang.take().unwrap_or("".into()); let lang = code_lang.take().unwrap_or("".into());
let res = hilighting::hilight(&code_accumulator, &lang, Some("base16-ocean.dark")) let res = hilighting::hilight(&code_accumulator, &lang, Some("base16-ocean.dark"))
.unwrap(); .unwrap();

70
src/observability.rs Normal file
View file

@ -0,0 +1,70 @@
use std::time::Duration;
use axum::{
extract::{MatchedPath, OriginalUri, Request},
http::uri::PathAndQuery,
response::Response,
};
use config::Config;
use opentelemetry::global;
use opentelemetry_sdk::propagation::TraceContextPropagator;
use tracing::{field::Empty, info_span, Span};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
pub fn init_tracing(cfg: &Config) {
let filter = if let Ok(filter) = cfg.get_string("logging") {
EnvFilter::builder()
.with_default_directive("info".parse().unwrap())
.parse_lossy(filter)
} else {
EnvFilter::builder()
.with_default_directive("info".parse().unwrap())
.from_env_lossy()
};
global::set_text_map_propagator(TraceContextPropagator::new());
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(opentelemetry_otlp::new_exporter().tonic())
.install_batch(opentelemetry_sdk::runtime::Tokio)
.unwrap();
let otel = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(filter)
.with(otel)
.with(tracing_subscriber::fmt::layer())
.init();
}
pub fn make_span(request: &Request) -> Span {
let uri = if let Some(OriginalUri(uri)) = request.extensions().get::<OriginalUri>() {
uri
} else {
request.uri()
};
let route = request
.extensions()
.get::<MatchedPath>()
.map_or(uri.path(), axum::extract::MatchedPath::as_str);
let method = request.method().as_str();
let target = uri
.path_and_query()
.map_or(uri.path(), PathAndQuery::as_str);
let name = format!("{method} {route}");
info_span!(
"request",
otel.name = %name,
http.route = %route,
http.method = %method,
http.target = %target,
http.status_code = Empty
)
}
pub fn on_response(response: &Response, _latency: Duration, span: &Span) {
span.record("http.status_code", response.status().as_str());
}

View file

@ -1,5 +1,7 @@
use std::{collections::HashMap, path::Path}; use std::{collections::HashMap, path::Path};
use anyhow::Result;
use cached::once_cell::sync::Lazy; use cached::once_cell::sync::Lazy;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use glob::glob; use glob::glob;
@ -15,10 +17,9 @@ use tracing::{
use crate::{helpers, markdown, AppState, WebsiteError}; use crate::{helpers, markdown, AppState, WebsiteError};
static FRONTMATTER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new( static FRONTMATTER_REGEX: Lazy<Regex> = Lazy::new(|| {
r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))" Regex::new(r"^[\s]*\+{3}(\r?\n(?s).*?(?-s))\+{3}[\s]*(?:$|(?:\r?\n((?s).*(?-s))$))").unwrap()
) });
.unwrap());
#[derive(Deserialize, Debug, Default)] #[derive(Deserialize, Debug, Default)]
pub struct TomlFrontMatter { pub struct TomlFrontMatter {
@ -76,7 +77,7 @@ impl Post {
} }
#[instrument(skip(state))] #[instrument(skip(state))]
pub async fn load_all(state: &AppState) -> color_eyre::eyre::Result<HashMap<String, Post>> { pub async fn load_all(state: &AppState) -> Result<HashMap<String, Post>> {
let mut res = HashMap::<String, Post>::new(); let mut res = HashMap::<String, Post>::new();
for path in glob("posts/**/*.md")? { for path in glob("posts/**/*.md")? {
let path = path.unwrap(); let path = path.unwrap();
@ -101,7 +102,7 @@ pub async fn load_all(state: &AppState) -> color_eyre::eyre::Result<HashMap<Stri
} }
#[instrument(skip(state))] #[instrument(skip(state))]
pub async fn load_post(state: &AppState, slug: &str) -> color_eyre::eyre::Result<Post> { pub async fn load_post(state: &AppState, slug: &str) -> Result<Post> {
debug!("loading post: {slug}"); debug!("loading post: {slug}");
let file_path = Path::new("posts").join(slug); let file_path = Path::new("posts").join(slug);
@ -127,9 +128,7 @@ pub async fn load_post(state: &AppState, slug: &str) -> color_eyre::eyre::Result
} }
#[instrument(skip(src))] #[instrument(skip(src))]
fn parse_frontmatter( fn parse_frontmatter(src: String) -> Result<(Option<TomlFrontMatter>, Option<String>)> {
src: String,
) -> color_eyre::eyre::Result<(Option<TomlFrontMatter>, Option<String>)> {
Ok(if let Some(captures) = FRONTMATTER_REGEX.captures(&src) { Ok(if let Some(captures) = FRONTMATTER_REGEX.captures(&src) {
( (
Some(toml::from_str(captures.get(1).unwrap().as_str())?), Some(toml::from_str(captures.get(1).unwrap().as_str())?),