updates
This commit is contained in:
parent
82e377dbea
commit
d8050d2e89
13 changed files with 639 additions and 648 deletions
905
Cargo.lock
generated
905
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
19
Cargo.toml
19
Cargo.toml
|
@ -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
18
fly.toml
Normal 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"
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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)?,
|
||||||
|
|
|
@ -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)?,
|
||||||
|
|
|
@ -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"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
98
src/main.rs
98
src/main.rs
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
70
src/observability.rs
Normal 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());
|
||||||
|
}
|
17
src/post.rs
17
src/post.rs
|
@ -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())?),
|
||||||
|
|
Loading…
Reference in a new issue